chore: update packages

also accidentally prettified some code along the way
This commit is contained in:
trafficlunar 2026-01-28 19:06:00 +00:00
parent 4656b969d6
commit e05533b19a
6 changed files with 1150 additions and 850 deletions

View file

@ -2,7 +2,7 @@
"name": "tomodachi-share", "name": "tomodachi-share",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"packageManager": "pnpm@10.24.0", "packageManager": "pnpm@10.28.2",
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
@ -16,45 +16,45 @@
"@auth/prisma-adapter": "2.11.1", "@auth/prisma-adapter": "2.11.1",
"@bprogress/next": "^3.2.12", "@bprogress/next": "^3.2.12",
"@hello-pangea/dnd": "^18.0.1", "@hello-pangea/dnd": "^18.0.1",
"@prisma/client": "^6.19.1", "@prisma/client": "^6.19.2",
"bit-buffer": "^0.2.5", "bit-buffer": "^0.3.0",
"canvas-confetti": "^1.9.4", "canvas-confetti": "^1.9.4",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"downshift": "^9.0.13", "downshift": "^9.0.13",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"file-type": "^21.1.1", "file-type": "^21.3.0",
"ioredis": "^5.8.2", "ioredis": "^5.9.2",
"jsqr": "^1.4.0", "jsqr": "^1.4.0",
"next": "16.0.10", "next": "16.1.6",
"next-auth": "5.0.0-beta.30", "next-auth": "5.0.0-beta.30",
"qrcode-generator": "^2.0.4", "qrcode-generator": "^2.0.4",
"react": "^19.2.3", "react": "^19.2.4",
"react-dom": "^19.2.3", "react-dom": "^19.2.4",
"react-dropzone": "^14.3.8", "react-dropzone": "^14.3.8",
"react-webcam": "^7.2.0", "react-webcam": "^7.2.0",
"satori": "^0.18.3", "satori": "^0.19.1",
"seedrandom": "^3.0.5", "seedrandom": "^3.0.5",
"sharp": "^0.34.5", "sharp": "^0.34.5",
"sjcl-with-all": "1.0.8", "sjcl-with-all": "1.0.8",
"swr": "^2.3.7", "swr": "^2.3.8",
"zod": "^4.1.13" "zod": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.3", "@eslint/eslintrc": "^3.3.3",
"@iconify/react": "^6.0.2", "@iconify/react": "^6.0.2",
"@tailwindcss/postcss": "^4.1.18", "@tailwindcss/postcss": "^4.1.18",
"@types/canvas-confetti": "^1.9.0", "@types/canvas-confetti": "^1.9.0",
"@types/node": "^25.0.2", "@types/node": "^25.1.0",
"@types/react": "^19.2.7", "@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@types/seedrandom": "^3.0.8", "@types/seedrandom": "^3.0.8",
"@types/sjcl": "^1.0.34", "@types/sjcl": "^1.0.34",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"eslint-config-next": "16.0.10", "eslint-config-next": "16.1.6",
"prisma": "^6.19.1", "prisma": "^6.19.2",
"schema-dts": "^1.1.5", "schema-dts": "^1.1.5",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vitest": "^4.0.15" "vitest": "^4.0.18"
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -25,7 +25,11 @@ const submitSchema = z.object({
name: nameSchema, name: nameSchema,
tags: tagsSchema, tags: tagsSchema,
description: z.string().trim().max(256).optional(), description: z.string().trim().max(256).optional(),
qrBytesRaw: z.array(z.number(), { error: "A QR code is required" }).length(372, { error: "QR code size is not a valid Tomodachi Life QR code" }), qrBytesRaw: z
.array(z.number(), { error: "A QR code is required" })
.length(372, {
error: "QR code size is not a valid Tomodachi Life QR code",
}),
image1: z.union([z.instanceof(File), z.any()]).optional(), image1: z.union([z.instanceof(File), z.any()]).optional(),
image2: z.union([z.instanceof(File), z.any()]).optional(), image2: z.union([z.instanceof(File), z.any()]).optional(),
image3: z.union([z.instanceof(File), z.any()]).optional(), image3: z.union([z.instanceof(File), z.any()]).optional(),
@ -33,15 +37,19 @@ const submitSchema = z.object({
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
const session = await auth(); const session = await auth();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); if (!session)
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const rateLimit = new RateLimit(request, 2); const rateLimit = new RateLimit(request, 2);
const check = await rateLimit.handle(); const check = await rateLimit.handle();
if (check) return check; if (check) return check;
const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/admin/can-submit`); const response = await fetch(
`${process.env.NEXT_PUBLIC_BASE_URL}/api/admin/can-submit`,
);
const { value } = await response.json(); const { value } = await response.json();
if (!value) return rateLimit.sendResponse({ error: "Submissions are disabled" }, 409); if (!value)
return rateLimit.sendResponse({ error: "Submissions are disabled" }, 409);
// Parse data // Parse data
const formData = await request.formData(); const formData = await request.formData();
@ -52,7 +60,10 @@ export async function POST(request: NextRequest) {
rawTags = JSON.parse(formData.get("tags") as string); rawTags = JSON.parse(formData.get("tags") as string);
rawQrBytesRaw = JSON.parse(formData.get("qrBytesRaw") as string); rawQrBytesRaw = JSON.parse(formData.get("qrBytesRaw") as string);
} catch { } catch {
return rateLimit.sendResponse({ error: "Invalid JSON in tags or QR bytes" }, 400); return rateLimit.sendResponse(
{ error: "Invalid JSON in tags or QR bytes" },
400,
);
} }
const parsed = submitSchema.safeParse({ const parsed = submitSchema.safeParse({
@ -65,13 +76,26 @@ export async function POST(request: NextRequest) {
image3: formData.get("image3"), image3: formData.get("image3"),
}); });
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400); if (!parsed.success)
const { name: uncensoredName, tags: uncensoredTags, description: uncensoredDescription, qrBytesRaw, image1, image2, image3 } = parsed.data; return rateLimit.sendResponse(
{ error: parsed.error.issues[0].message },
400,
);
const {
name: uncensoredName,
tags: uncensoredTags,
description: uncensoredDescription,
qrBytesRaw,
image1,
image2,
image3,
} = parsed.data;
// Censor potential inappropriate words // Censor potential inappropriate words
const name = profanity.censor(uncensoredName); const name = profanity.censor(uncensoredName);
const tags = uncensoredTags.map((t) => profanity.censor(t)); const tags = uncensoredTags.map((t) => profanity.censor(t));
const description = uncensoredDescription && profanity.censor(uncensoredDescription); const description =
uncensoredDescription && profanity.censor(uncensoredDescription);
// Validate image files // Validate image files
const images: File[] = []; const images: File[] = [];
@ -83,7 +107,10 @@ export async function POST(request: NextRequest) {
if (imageValidation.valid) { if (imageValidation.valid) {
images.push(img); images.push(img);
} else { } else {
return rateLimit.sendResponse({ error: imageValidation.error }, imageValidation.status ?? 400); return rateLimit.sendResponse(
{ error: imageValidation.error },
imageValidation.status ?? 400,
);
} }
} }
@ -114,7 +141,10 @@ export async function POST(request: NextRequest) {
}); });
// Ensure directories exist // Ensure directories exist
const miiUploadsDirectory = path.join(uploadsDirectory, miiRecord.id.toString()); const miiUploadsDirectory = path.join(
uploadsDirectory,
miiRecord.id.toString(),
);
await fs.mkdir(miiUploadsDirectory, { recursive: true }); await fs.mkdir(miiUploadsDirectory, { recursive: true });
// Download the image of the Mii // Download the image of the Mii
@ -134,12 +164,17 @@ export async function POST(request: NextRequest) {
await prisma.mii.delete({ where: { id: miiRecord.id } }); await prisma.mii.delete({ where: { id: miiRecord.id } });
console.error("Failed to download Mii image:", error); console.error("Failed to download Mii image:", error);
return rateLimit.sendResponse({ error: "Failed to download Mii image" }, 500); return rateLimit.sendResponse(
{ error: "Failed to download Mii image" },
500,
);
} }
try { try {
// Compress and store // Compress and store
const studioWebpBuffer = await sharp(studioBuffer).webp({ quality: 85 }).toBuffer(); const studioWebpBuffer = await sharp(studioBuffer)
.webp({ quality: 85 })
.toBuffer();
const studioFileLocation = path.join(miiUploadsDirectory, "mii.webp"); const studioFileLocation = path.join(miiUploadsDirectory, "mii.webp");
await fs.writeFile(studioFileLocation, studioWebpBuffer); await fs.writeFile(studioFileLocation, studioWebpBuffer);
@ -156,7 +191,9 @@ export async function POST(request: NextRequest) {
const codeBuffer = Buffer.from(codeBase64, "base64"); const codeBuffer = Buffer.from(codeBase64, "base64");
// Compress and store // Compress and store
const codeWebpBuffer = await sharp(codeBuffer).webp({ quality: 85 }).toBuffer(); const codeWebpBuffer = await sharp(codeBuffer)
.webp({ quality: 85 })
.toBuffer();
const codeFileLocation = path.join(miiUploadsDirectory, "qr-code.webp"); const codeFileLocation = path.join(miiUploadsDirectory, "qr-code.webp");
await fs.writeFile(codeFileLocation, codeWebpBuffer); await fs.writeFile(codeFileLocation, codeWebpBuffer);
@ -166,7 +203,10 @@ export async function POST(request: NextRequest) {
await prisma.mii.delete({ where: { id: miiRecord.id } }); await prisma.mii.delete({ where: { id: miiRecord.id } });
console.error("Error processing Mii files:", error); console.error("Error processing Mii files:", error);
return rateLimit.sendResponse({ error: "Failed to process and store Mii files" }, 500); return rateLimit.sendResponse(
{ error: "Failed to process and store Mii files" },
500,
);
} }
// Compress and store user images // Compress and store user images
@ -175,10 +215,13 @@ export async function POST(request: NextRequest) {
images.map(async (image, index) => { images.map(async (image, index) => {
const buffer = Buffer.from(await image.arrayBuffer()); const buffer = Buffer.from(await image.arrayBuffer());
const webpBuffer = await sharp(buffer).webp({ quality: 85 }).toBuffer(); const webpBuffer = await sharp(buffer).webp({ quality: 85 }).toBuffer();
const fileLocation = path.join(miiUploadsDirectory, `image${index}.webp`); const fileLocation = path.join(
miiUploadsDirectory,
`image${index}.webp`,
);
await fs.writeFile(fileLocation, webpBuffer); await fs.writeFile(fileLocation, webpBuffer);
}) }),
); );
// Update database to tell it how many images exist // Update database to tell it how many images exist
@ -192,7 +235,10 @@ export async function POST(request: NextRequest) {
}); });
} catch (error) { } catch (error) {
console.error("Error storing user images:", error); console.error("Error storing user images:", error);
return rateLimit.sendResponse({ error: "Failed to store user images" }, 500); return rateLimit.sendResponse(
{ error: "Failed to store user images" },
500,
);
} }
return rateLimit.sendResponse({ success: true, id: miiRecord.id }); return rateLimit.sendResponse({ success: true, id: miiRecord.id });

View file

@ -31,12 +31,14 @@ export default function SubmitForm() {
if (files.length >= 3) return; if (files.length >= 3) return;
setFiles((prev) => [...prev, ...acceptedFiles]); setFiles((prev) => [...prev, ...acceptedFiles]);
}, },
[files.length] [files.length],
); );
const [isQrScannerOpen, setIsQrScannerOpen] = useState(false); const [isQrScannerOpen, setIsQrScannerOpen] = useState(false);
const [studioUrl, setStudioUrl] = useState<string | undefined>(); const [studioUrl, setStudioUrl] = useState<string | undefined>();
const [generatedQrCodeUrl, setGeneratedQrCodeUrl] = useState<string | undefined>(); const [generatedQrCodeUrl, setGeneratedQrCodeUrl] = useState<
string | undefined
>();
const [error, setError] = useState<string | undefined>(undefined); const [error, setError] = useState<string | undefined>(undefined);
@ -76,7 +78,7 @@ export default function SubmitForm() {
const { id, error } = await response.json(); const { id, error } = await response.json();
if (!response.ok) { if (!response.ok) {
setError(error); setError(String(error)); // app can crash if error message is not a string
return; return;
} }
@ -127,16 +129,29 @@ export default function SubmitForm() {
<form className="flex justify-center gap-4 w-full max-lg:flex-col max-lg:items-center"> <form className="flex justify-center gap-4 w-full max-lg:flex-col max-lg:items-center">
<div className="flex justify-center"> <div className="flex justify-center">
<div className="w-75 h-min flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-3"> <div className="w-75 h-min flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-3">
<Carousel images={[studioUrl ?? "/loading.svg", generatedQrCodeUrl ?? "/loading.svg", ...files.map((file) => URL.createObjectURL(file))]} /> <Carousel
images={[
studioUrl ?? "/loading.svg",
generatedQrCodeUrl ?? "/loading.svg",
...files.map((file) => URL.createObjectURL(file)),
]}
/>
<div className="p-4 flex flex-col gap-1 h-full"> <div className="p-4 flex flex-col gap-1 h-full">
<h1 className="font-bold text-2xl line-clamp-1" title={name}> <h1 className="font-bold text-2xl line-clamp-1" title={name}>
{name || "Mii name"} {name || "Mii name"}
</h1> </h1>
<div id="tags" className="flex flex-wrap gap-1"> <div id="tags" className="flex flex-wrap gap-1">
{tags.length == 0 && <span className="px-2 py-1 bg-orange-300 rounded-full text-xs">tag</span>} {tags.length == 0 && (
<span className="px-2 py-1 bg-orange-300 rounded-full text-xs">
tag
</span>
)}
{tags.map((tag) => ( {tags.map((tag) => (
<span key={tag} className="px-2 py-1 bg-orange-300 rounded-full text-xs"> <span
key={tag}
className="px-2 py-1 bg-orange-300 rounded-full text-xs"
>
{tag} {tag}
</span> </span>
))} ))}
@ -152,7 +167,9 @@ export default function SubmitForm() {
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex flex-col gap-2 max-w-2xl w-full"> <div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex flex-col gap-2 max-w-2xl w-full">
<div> <div>
<h2 className="text-2xl font-bold">Submit your Mii</h2> <h2 className="text-2xl font-bold">Submit your Mii</h2>
<p className="text-sm text-zinc-500">Share your creation for others to see.</p> <p className="text-sm text-zinc-500">
Share your creation for others to see.
</p>
</div> </div>
{/* Separator */} {/* Separator */}
@ -210,15 +227,26 @@ export default function SubmitForm() {
<QrUpload setQrBytesRaw={setQrBytesRaw} /> <QrUpload setQrBytesRaw={setQrBytesRaw} />
<span>or</span> <span>or</span>
<button type="button" aria-label="Use your camera" onClick={() => setIsQrScannerOpen(true)} className="pill button gap-2"> <button
type="button"
aria-label="Use your camera"
onClick={() => setIsQrScannerOpen(true)}
className="pill button gap-2"
>
<Icon icon="mdi:camera" fontSize={20} /> <Icon icon="mdi:camera" fontSize={20} />
Use your camera Use your camera
</button> </button>
<QrScanner isOpen={isQrScannerOpen} setIsOpen={setIsQrScannerOpen} setQrBytesRaw={setQrBytesRaw} /> <QrScanner
isOpen={isQrScannerOpen}
setIsOpen={setIsQrScannerOpen}
setQrBytesRaw={setQrBytesRaw}
/>
<SubmitTutorialButton /> <SubmitTutorialButton />
<span className="text-xs text-zinc-400">For emulators, aes_keys.txt is required.</span> <span className="text-xs text-zinc-400">
For emulators, aes_keys.txt is required.
</span>
</div> </div>
{/* Separator */} {/* Separator */}
@ -237,14 +265,18 @@ export default function SubmitForm() {
</p> </p>
</Dropzone> </Dropzone>
<span className="text-xs text-zinc-400 mt-2">Animated images currently not supported.</span> <span className="text-xs text-zinc-400 mt-2">
Animated images currently not supported.
</span>
</div> </div>
<ImageList files={files} setFiles={setFiles} /> <ImageList files={files} setFiles={setFiles} />
<hr className="border-zinc-300 my-2" /> <hr className="border-zinc-300 my-2" />
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
{error && <span className="text-red-400 font-bold">Error: {error}</span>} {error && (
<span className="text-red-400 font-bold">Error: {error}</span>
)}
<SubmitButton onClick={handleSubmit} className="ml-auto" /> <SubmitButton onClick={handleSubmit} className="ml-auto" />
</div> </div>

View file

@ -1,4 +1,5 @@
// Stolen from https://github.com/PretendoNetwork/mii-js/ // Based on https://github.com/PretendoNetwork/mii-js/
// Updated to bit-buffer v0.3.0
import { BitStream } from "bit-buffer"; import { BitStream } from "bit-buffer";
@ -11,78 +12,34 @@ export default class ExtendedBitStream extends BitStream {
this.bigEndian = !this.bigEndian; this.bigEndian = !this.bigEndian;
} }
// the type definition for BitStream does not include the _index property
// since it's supposed to be private, but it's needed 4 times here sooo
public alignByte(): void { public alignByte(): void {
// @ts-expect-error _index is private const nextClosestByteIndex = 8 * Math.ceil(this.index / 8);
const nextClosestByteIndex = 8 * Math.ceil(this._index / 8); const bitDistance = nextClosestByteIndex - this.index;
// @ts-expect-error _index is private
const bitDistance = nextClosestByteIndex - this._index;
this.skipBits(bitDistance); this.skipBits(bitDistance);
} }
public bitSeek(bitPos: number): void {
// @ts-expect-error _index is private
this._index = bitPos;
}
public skipBits(bitCount: number): void { public skipBits(bitCount: number): void {
// @ts-expect-error _index is private this.index += bitCount;
this._index += bitCount;
}
public skipBytes(bytes: number): void {
const bits = bytes * 8;
this.skipBits(bits);
} }
public skipBit(): void { public skipBit(): void {
this.skipBits(1); this.skipBits(1);
} }
public skipInt8(): void {
this.skipBytes(1);
}
public skipInt16(): void { public skipInt16(): void {
// Skipping a uint16 is the same as skipping 2 uint8's // Skipping a uint16 is the same as skipping 2 uint8's
this.skipBytes(2); this.skipBits(16);
} }
public readBit(): number { public readBit(): number {
return this.readBits(1); return this.readBits(1, false);
}
public readBytes(length: number): number[] {
return Array(length)
.fill(0)
.map(() => this.readUint8());
} }
public readBuffer(length: number): Buffer { public readBuffer(length: number): Buffer {
return Buffer.from(this.readBytes(length)); return Buffer.from(super.readBytes(length));
} }
public readUTF16String(length: number): string { public readUTF16String(length: number): string {
return this.readBuffer(length).toString("utf16le").replace(/\0.*$/, ""); return this.readBuffer(length).toString("utf16le").replace(/\0.*$/, "");
} }
public writeBit(bit: number): void {
this.writeBits(bit, 1);
}
public writeBuffer(buffer: Buffer): void {
buffer.forEach((byte) => this.writeUint8(byte));
}
public writeUTF16String(string: string): void {
const stringBuffer = Buffer.from(string, "utf16le");
const terminatedBuffer = Buffer.alloc(0x14);
stringBuffer.copy(terminatedBuffer);
this.writeBuffer(terminatedBuffer);
}
} }

View file

@ -66,7 +66,14 @@ const STUDIO_RENDER_CLOTHES_COLORS = [
"black", "black",
]; ];
const STUDIO_RENDER_LIGHT_DIRECTION_MODS = ["none", "zerox", "flipx", "camera", "offset", "set"]; const STUDIO_RENDER_LIGHT_DIRECTION_MODS = [
"none",
"zerox",
"flipx",
"camera",
"offset",
"set",
];
const STUDIO_RENDER_INSTANCE_ROTATION_MODES = ["model", "camera", "both"]; const STUDIO_RENDER_INSTANCE_ROTATION_MODES = ["model", "camera", "both"];
@ -74,6 +81,7 @@ const STUDIO_BG_COLOR_REGEX = /^[0-9A-F]{8}$/; // Mii Studio does not allow lowe
export default class Mii { export default class Mii {
public bitStream: ExtendedBitStream; public bitStream: ExtendedBitStream;
public buffer: Buffer;
// Mii data // Mii data
// can be sure that these are all initialized in decode() // can be sure that these are all initialized in decode()
@ -150,92 +158,292 @@ export default class Mii {
public checksum!: number; public checksum!: number;
constructor(buffer: Buffer) { constructor(buffer: Buffer) {
this.buffer = buffer;
this.bitStream = new ExtendedBitStream(buffer); this.bitStream = new ExtendedBitStream(buffer);
this.decode(); this.decode();
} }
public validate(): void { public validate(): void {
// Size check // Size check
assert.equal(this.bitStream.length / 8, 0x60, `Invalid Mii data size. Got ${this.bitStream.length / 8}, expected 96`); assert.equal(
this.bitStream.length / 8,
0x60,
`Invalid Mii data size. Got ${this.bitStream.length / 8}, expected 96`,
);
// Value range and type checks // Value range and type checks
assert.ok(this.version === 0 || this.version === 3, `Invalid Mii version. Got ${this.version}, expected 0 or 3`); assert.ok(
assert.equal(typeof this.allowCopying, "boolean", `Invalid Mii allow copying. Got ${this.allowCopying}, expected true or false`); this.version === 0 || this.version === 3,
assert.equal(typeof this.profanityFlag, "boolean", `Invalid Mii profanity flag. Got ${this.profanityFlag}, expected true or false`); `Invalid Mii version. Got ${this.version}, expected 0 or 3`,
assert.ok(Util.inRange(this.regionLock, Util.range(4)), `Invalid Mii region lock. Got ${this.regionLock}, expected 0-3`); );
assert.ok(Util.inRange(this.characterSet, Util.range(4)), `Invalid Mii region lock. Got ${this.characterSet}, expected 0-3`); assert.equal(
assert.ok(Util.inRange(this.pageIndex, Util.range(10)), `Invalid Mii page index. Got ${this.pageIndex}, expected 0-9`); typeof this.allowCopying,
assert.ok(Util.inRange(this.slotIndex, Util.range(10)), `Invalid Mii slot index. Got ${this.slotIndex}, expected 0-9`); "boolean",
assert.equal(this.unknown1, 0, `Invalid Mii unknown1. Got ${this.unknown1}, expected 0`); `Invalid Mii allow copying. Got ${this.allowCopying}, expected true or false`,
assert.ok(Util.inRange(this.deviceOrigin, Util.range(1, 5)), `Invalid Mii device origin. Got ${this.deviceOrigin}, expected 1-4`); );
assert.equal(this.systemId.length, 8, `Invalid Mii system ID size. Got ${this.systemId.length}, system IDs must be 8 bytes long`); assert.equal(
assert.equal(typeof this.normalMii, "boolean", `Invalid normal Mii flag. Got ${this.normalMii}, expected true or false`); typeof this.profanityFlag,
assert.equal(typeof this.dsMii, "boolean", `Invalid DS Mii flag. Got ${this.dsMii}, expected true or false`); "boolean",
assert.equal(typeof this.nonUserMii, "boolean", `Invalid non-user Mii flag. Got ${this.nonUserMii}, expected true or false`); `Invalid Mii profanity flag. Got ${this.profanityFlag}, expected true or false`,
assert.equal(typeof this.isValid, "boolean", `Invalid Mii valid flag. Got ${this.isValid}, expected true or false`); );
assert.ok(this.creationTime < 268435456, `Invalid Mii creation time. Got ${this.creationTime}, max value for 28 bit integer is 268,435,456`); assert.ok(
Util.inRange(this.regionLock, Util.range(4)),
`Invalid Mii region lock. Got ${this.regionLock}, expected 0-3`,
);
assert.ok(
Util.inRange(this.characterSet, Util.range(4)),
`Invalid Mii region lock. Got ${this.characterSet}, expected 0-3`,
);
assert.ok(
Util.inRange(this.pageIndex, Util.range(10)),
`Invalid Mii page index. Got ${this.pageIndex}, expected 0-9`,
);
assert.ok(
Util.inRange(this.slotIndex, Util.range(10)),
`Invalid Mii slot index. Got ${this.slotIndex}, expected 0-9`,
);
assert.equal(
this.unknown1,
0,
`Invalid Mii unknown1. Got ${this.unknown1}, expected 0`,
);
assert.ok(
Util.inRange(this.deviceOrigin, Util.range(1, 5)),
`Invalid Mii device origin. Got ${this.deviceOrigin}, expected 1-4`,
);
assert.equal(
this.systemId.length,
8,
`Invalid Mii system ID size. Got ${this.systemId.length}, system IDs must be 8 bytes long`,
);
assert.equal(
typeof this.normalMii,
"boolean",
`Invalid normal Mii flag. Got ${this.normalMii}, expected true or false`,
);
assert.equal(
typeof this.dsMii,
"boolean",
`Invalid DS Mii flag. Got ${this.dsMii}, expected true or false`,
);
assert.equal(
typeof this.nonUserMii,
"boolean",
`Invalid non-user Mii flag. Got ${this.nonUserMii}, expected true or false`,
);
assert.equal(
typeof this.isValid,
"boolean",
`Invalid Mii valid flag. Got ${this.isValid}, expected true or false`,
);
assert.ok(
this.creationTime < 268435456,
`Invalid Mii creation time. Got ${this.creationTime}, max value for 28 bit integer is 268,435,456`,
);
assert.equal( assert.equal(
this.consoleMAC.length, this.consoleMAC.length,
6, 6,
`Invalid Mii console MAC address size. Got ${this.consoleMAC.length}, console MAC addresses must be 6 bytes long` `Invalid Mii console MAC address size. Got ${this.consoleMAC.length}, console MAC addresses must be 6 bytes long`,
);
assert.ok(
Util.inRange(this.gender, Util.range(2)),
`Invalid Mii gender. Got ${this.gender}, expected 0 or 1`,
);
assert.ok(
Util.inRange(this.birthMonth, Util.range(13)),
`Invalid Mii birth month. Got ${this.birthMonth}, expected 0-12`,
);
assert.ok(
Util.inRange(this.birthDay, Util.range(32)),
`Invalid Mii birth day. Got ${this.birthDay}, expected 0-31`,
);
assert.ok(
Util.inRange(this.favoriteColor, Util.range(12)),
`Invalid Mii favorite color. Got ${this.favoriteColor}, expected 0-11`,
);
assert.equal(
typeof this.favorite,
"boolean",
`Invalid favorite Mii flag. Got ${this.favorite}, expected true or false`,
);
assert.ok(
Buffer.from(this.miiName, "utf16le").length <= 0x14,
`Invalid Mii name. Got ${this.miiName}, name may only be up to 10 characters`,
);
assert.ok(
Util.inRange(this.height, Util.range(128)),
`Invalid Mii height. Got ${this.height}, expected 0-127`,
);
assert.ok(
Util.inRange(this.build, Util.range(128)),
`Invalid Mii build. Got ${this.build}, expected 0-127`,
);
assert.equal(
typeof this.disableSharing,
"boolean",
`Invalid disable sharing Mii flag. Got ${this.disableSharing}, expected true or false`,
);
assert.ok(
Util.inRange(this.faceType, Util.range(12)),
`Invalid Mii face type. Got ${this.faceType}, expected 0-11`,
);
assert.ok(
Util.inRange(this.skinColor, Util.range(7)),
`Invalid Mii skin color. Got ${this.skinColor}, expected 0-6`,
);
assert.ok(
Util.inRange(this.wrinklesType, Util.range(12)),
`Invalid Mii wrinkles type. Got ${this.wrinklesType}, expected 0-11`,
);
assert.ok(
Util.inRange(this.makeupType, Util.range(12)),
`Invalid Mii makeup type. Got ${this.makeupType}, expected 0-11`,
);
assert.ok(
Util.inRange(this.hairType, Util.range(132)),
`Invalid Mii hair type. Got ${this.hairType}, expected 0-131`,
); );
assert.ok(Util.inRange(this.gender, Util.range(2)), `Invalid Mii gender. Got ${this.gender}, expected 0 or 1`);
assert.ok(Util.inRange(this.birthMonth, Util.range(13)), `Invalid Mii birth month. Got ${this.birthMonth}, expected 0-12`);
assert.ok(Util.inRange(this.birthDay, Util.range(32)), `Invalid Mii birth day. Got ${this.birthDay}, expected 0-31`);
assert.ok(Util.inRange(this.favoriteColor, Util.range(12)), `Invalid Mii favorite color. Got ${this.favoriteColor}, expected 0-11`);
assert.equal(typeof this.favorite, "boolean", `Invalid favorite Mii flag. Got ${this.favorite}, expected true or false`);
assert.ok(Buffer.from(this.miiName, "utf16le").length <= 0x14, `Invalid Mii name. Got ${this.miiName}, name may only be up to 10 characters`);
assert.ok(Util.inRange(this.height, Util.range(128)), `Invalid Mii height. Got ${this.height}, expected 0-127`);
assert.ok(Util.inRange(this.build, Util.range(128)), `Invalid Mii build. Got ${this.build}, expected 0-127`);
assert.equal(typeof this.disableSharing, "boolean", `Invalid disable sharing Mii flag. Got ${this.disableSharing}, expected true or false`);
assert.ok(Util.inRange(this.faceType, Util.range(12)), `Invalid Mii face type. Got ${this.faceType}, expected 0-11`);
assert.ok(Util.inRange(this.skinColor, Util.range(7)), `Invalid Mii skin color. Got ${this.skinColor}, expected 0-6`);
assert.ok(Util.inRange(this.wrinklesType, Util.range(12)), `Invalid Mii wrinkles type. Got ${this.wrinklesType}, expected 0-11`);
assert.ok(Util.inRange(this.makeupType, Util.range(12)), `Invalid Mii makeup type. Got ${this.makeupType}, expected 0-11`);
assert.ok(Util.inRange(this.hairType, Util.range(132)), `Invalid Mii hair type. Got ${this.hairType}, expected 0-131`);
// assert.ok(Util.inRange(this.hairColor, Util.range(8)), `Invalid Mii hair color. Got ${this.hairColor}, expected 0-7`); // assert.ok(Util.inRange(this.hairColor, Util.range(8)), `Invalid Mii hair color. Got ${this.hairColor}, expected 0-7`);
assert.equal(typeof this.flipHair, "boolean", `Invalid flip hair flag. Got ${this.flipHair}, expected true or false`); assert.equal(
assert.ok(Util.inRange(this.eyeType, Util.range(60)), `Invalid Mii eye type. Got ${this.eyeType}, expected 0-59`); typeof this.flipHair,
assert.ok(Util.inRange(this.eyeColor, Util.range(6)), `Invalid Mii eye color. Got ${this.eyeColor}, expected 0-5`); "boolean",
assert.ok(Util.inRange(this.eyeScale, Util.range(8)), `Invalid Mii eye scale. Got ${this.eyeScale}, expected 0-7`); `Invalid flip hair flag. Got ${this.flipHair}, expected true or false`,
assert.ok(Util.inRange(this.eyeVerticalStretch, Util.range(7)), `Invalid Mii eye vertical stretch. Got ${this.eyeVerticalStretch}, expected 0-6`); );
assert.ok(Util.inRange(this.eyeRotation, Util.range(8)), `Invalid Mii eye rotation. Got ${this.eyeRotation}, expected 0-7`); assert.ok(
assert.ok(Util.inRange(this.eyeSpacing, Util.range(13)), `Invalid Mii eye spacing. Got ${this.eyeSpacing}, expected 0-12`); Util.inRange(this.eyeType, Util.range(60)),
assert.ok(Util.inRange(this.eyeYPosition, Util.range(19)), `Invalid Mii eye Y position. Got ${this.eyeYPosition}, expected 0-18`); `Invalid Mii eye type. Got ${this.eyeType}, expected 0-59`,
assert.ok(Util.inRange(this.eyebrowType, Util.range(25)), `Invalid Mii eyebrow type. Got ${this.eyebrowType}, expected 0-24`); );
assert.ok(
Util.inRange(this.eyeColor, Util.range(6)),
`Invalid Mii eye color. Got ${this.eyeColor}, expected 0-5`,
);
assert.ok(
Util.inRange(this.eyeScale, Util.range(8)),
`Invalid Mii eye scale. Got ${this.eyeScale}, expected 0-7`,
);
assert.ok(
Util.inRange(this.eyeVerticalStretch, Util.range(7)),
`Invalid Mii eye vertical stretch. Got ${this.eyeVerticalStretch}, expected 0-6`,
);
assert.ok(
Util.inRange(this.eyeRotation, Util.range(8)),
`Invalid Mii eye rotation. Got ${this.eyeRotation}, expected 0-7`,
);
assert.ok(
Util.inRange(this.eyeSpacing, Util.range(13)),
`Invalid Mii eye spacing. Got ${this.eyeSpacing}, expected 0-12`,
);
assert.ok(
Util.inRange(this.eyeYPosition, Util.range(19)),
`Invalid Mii eye Y position. Got ${this.eyeYPosition}, expected 0-18`,
);
assert.ok(
Util.inRange(this.eyebrowType, Util.range(25)),
`Invalid Mii eyebrow type. Got ${this.eyebrowType}, expected 0-24`,
);
// assert.ok(Util.inRange(this.eyebrowColor, Util.range(8)), `Invalid Mii eyebrow color. Got ${this.eyebrowColor}, expected 0-7`); // assert.ok(Util.inRange(this.eyebrowColor, Util.range(8)), `Invalid Mii eyebrow color. Got ${this.eyebrowColor}, expected 0-7`);
assert.ok(Util.inRange(this.eyebrowScale, Util.range(9)), `Invalid Mii eyebrow scale. Got ${this.eyebrowScale}, expected 0-8`); assert.ok(
Util.inRange(this.eyebrowScale, Util.range(9)),
`Invalid Mii eyebrow scale. Got ${this.eyebrowScale}, expected 0-8`,
);
assert.ok( assert.ok(
Util.inRange(this.eyebrowVerticalStretch, Util.range(7)), Util.inRange(this.eyebrowVerticalStretch, Util.range(7)),
`Invalid Mii eyebrow vertical stretch. Got ${this.eyebrowVerticalStretch}, expected 0-6` `Invalid Mii eyebrow vertical stretch. Got ${this.eyebrowVerticalStretch}, expected 0-6`,
);
assert.ok(
Util.inRange(this.eyebrowRotation, Util.range(12)),
`Invalid Mii eyebrow rotation. Got ${this.eyebrowRotation}, expected 0-11`,
);
assert.ok(
Util.inRange(this.eyebrowSpacing, Util.range(13)),
`Invalid Mii eyebrow spacing. Got ${this.eyebrowSpacing}, expected 0-12`,
);
assert.ok(
Util.inRange(this.eyebrowYPosition, Util.range(3, 19)),
`Invalid Mii eyebrow Y position. Got ${this.eyebrowYPosition}, expected 3-18`,
);
assert.ok(
Util.inRange(this.noseType, Util.range(18)),
`Invalid Mii nose type. Got ${this.noseType}, expected 0-17`,
);
assert.ok(
Util.inRange(this.noseScale, Util.range(9)),
`Invalid Mii nose scale. Got ${this.noseScale}, expected 0-8`,
);
assert.ok(
Util.inRange(this.noseYPosition, Util.range(19)),
`Invalid Mii nose Y position. Got ${this.noseYPosition}, expected 0-18`,
);
assert.ok(
Util.inRange(this.mouthType, Util.range(36)),
`Invalid Mii mouth type. Got ${this.mouthType}, expected 0-35`,
);
assert.ok(
Util.inRange(this.mouthColor, Util.range(5)),
`Invalid Mii mouth color. Got ${this.mouthColor}, expected 0-4`,
);
assert.ok(
Util.inRange(this.mouthScale, Util.range(9)),
`Invalid Mii mouth scale. Got ${this.mouthScale}, expected 0-8`,
); );
assert.ok(Util.inRange(this.eyebrowRotation, Util.range(12)), `Invalid Mii eyebrow rotation. Got ${this.eyebrowRotation}, expected 0-11`);
assert.ok(Util.inRange(this.eyebrowSpacing, Util.range(13)), `Invalid Mii eyebrow spacing. Got ${this.eyebrowSpacing}, expected 0-12`);
assert.ok(Util.inRange(this.eyebrowYPosition, Util.range(3, 19)), `Invalid Mii eyebrow Y position. Got ${this.eyebrowYPosition}, expected 3-18`);
assert.ok(Util.inRange(this.noseType, Util.range(18)), `Invalid Mii nose type. Got ${this.noseType}, expected 0-17`);
assert.ok(Util.inRange(this.noseScale, Util.range(9)), `Invalid Mii nose scale. Got ${this.noseScale}, expected 0-8`);
assert.ok(Util.inRange(this.noseYPosition, Util.range(19)), `Invalid Mii nose Y position. Got ${this.noseYPosition}, expected 0-18`);
assert.ok(Util.inRange(this.mouthType, Util.range(36)), `Invalid Mii mouth type. Got ${this.mouthType}, expected 0-35`);
assert.ok(Util.inRange(this.mouthColor, Util.range(5)), `Invalid Mii mouth color. Got ${this.mouthColor}, expected 0-4`);
assert.ok(Util.inRange(this.mouthScale, Util.range(9)), `Invalid Mii mouth scale. Got ${this.mouthScale}, expected 0-8`);
assert.ok( assert.ok(
Util.inRange(this.mouthHorizontalStretch, Util.range(7)), Util.inRange(this.mouthHorizontalStretch, Util.range(7)),
`Invalid Mii mouth stretch. Got ${this.mouthHorizontalStretch}, expected 0-6` `Invalid Mii mouth stretch. Got ${this.mouthHorizontalStretch}, expected 0-6`,
);
assert.ok(
Util.inRange(this.mouthYPosition, Util.range(19)),
`Invalid Mii mouth Y position. Got ${this.mouthYPosition}, expected 0-18`,
);
assert.ok(
Util.inRange(this.mustacheType, Util.range(6)),
`Invalid Mii mustache type. Got ${this.mustacheType}, expected 0-5`,
);
assert.ok(
Util.inRange(this.beardType, Util.range(6)),
`Invalid Mii beard type. Got ${this.beardType}, expected 0-5`,
); );
assert.ok(Util.inRange(this.mouthYPosition, Util.range(19)), `Invalid Mii mouth Y position. Got ${this.mouthYPosition}, expected 0-18`);
assert.ok(Util.inRange(this.mustacheType, Util.range(6)), `Invalid Mii mustache type. Got ${this.mustacheType}, expected 0-5`);
assert.ok(Util.inRange(this.beardType, Util.range(6)), `Invalid Mii beard type. Got ${this.beardType}, expected 0-5`);
// assert.ok(Util.inRange(this.facialHairColor, Util.range(8)), `Invalid Mii beard type. Got ${this.facialHairColor}, expected 0-7`); // assert.ok(Util.inRange(this.facialHairColor, Util.range(8)), `Invalid Mii beard type. Got ${this.facialHairColor}, expected 0-7`);
assert.ok(Util.inRange(this.mustacheScale, Util.range(9)), `Invalid Mii mustache scale. Got ${this.mustacheScale}, expected 0-8`); assert.ok(
assert.ok(Util.inRange(this.mustacheYPosition, Util.range(17)), `Invalid Mii mustache Y position. Got ${this.mustacheYPosition}, expected 0-16`); Util.inRange(this.mustacheScale, Util.range(9)),
assert.ok(Util.inRange(this.glassesType, Util.range(9)), `Invalid Mii glassess type. Got ${this.glassesType}, expected 0-8`); `Invalid Mii mustache scale. Got ${this.mustacheScale}, expected 0-8`,
assert.ok(Util.inRange(this.glassesColor, Util.range(6)), `Invalid Mii glassess type. Got ${this.glassesColor}, expected 0-5`); );
assert.ok(Util.inRange(this.glassesScale, Util.range(8)), `Invalid Mii glassess type. Got ${this.glassesScale}, expected 0-7`); assert.ok(
assert.ok(Util.inRange(this.glassesYPosition, Util.range(21)), `Invalid Mii glassess Y position. Got ${this.glassesYPosition}, expected 0-20`); Util.inRange(this.mustacheYPosition, Util.range(17)),
assert.equal(typeof this.moleEnabled, "boolean", `Invalid mole enabled flag. Got ${this.moleEnabled}, expected true or false`); `Invalid Mii mustache Y position. Got ${this.mustacheYPosition}, expected 0-16`,
assert.ok(Util.inRange(this.moleScale, Util.range(9)), `Invalid Mii mole scale. Got ${this.moleScale}, expected 0-8`); );
assert.ok(Util.inRange(this.moleXPosition, Util.range(17)), `Invalid Mii mole X position. Got ${this.moleXPosition}, expected 0-16`); assert.ok(
assert.ok(Util.inRange(this.moleYPosition, Util.range(31)), `Invalid Mii mole Y position. Got ${this.moleYPosition}, expected 0-30`); Util.inRange(this.glassesType, Util.range(9)),
`Invalid Mii glassess type. Got ${this.glassesType}, expected 0-8`,
);
assert.ok(
Util.inRange(this.glassesColor, Util.range(6)),
`Invalid Mii glassess type. Got ${this.glassesColor}, expected 0-5`,
);
assert.ok(
Util.inRange(this.glassesScale, Util.range(8)),
`Invalid Mii glassess type. Got ${this.glassesScale}, expected 0-7`,
);
assert.ok(
Util.inRange(this.glassesYPosition, Util.range(21)),
`Invalid Mii glassess Y position. Got ${this.glassesYPosition}, expected 0-20`,
);
assert.equal(
typeof this.moleEnabled,
"boolean",
`Invalid mole enabled flag. Got ${this.moleEnabled}, expected true or false`,
);
assert.ok(
Util.inRange(this.moleScale, Util.range(9)),
`Invalid Mii mole scale. Got ${this.moleScale}, expected 0-8`,
);
assert.ok(
Util.inRange(this.moleXPosition, Util.range(17)),
`Invalid Mii mole X position. Got ${this.moleXPosition}, expected 0-16`,
);
assert.ok(
Util.inRange(this.moleYPosition, Util.range(31)),
`Invalid Mii mole Y position. Got ${this.moleYPosition}, expected 0-30`,
);
// Sanity checks // Sanity checks
/* /*
@ -251,7 +459,10 @@ export default class Mii {
} }
*/ */
if (this.nonUserMii && (this.creationTime !== 0 || this.isValid || this.dsMii || this.normalMii)) { if (
this.nonUserMii &&
(this.creationTime !== 0 || this.isValid || this.dsMii || this.normalMii)
) {
assert.fail("Non-user Mii's must have all other Mii ID bits set to 0"); assert.fail("Non-user Mii's must have all other Mii ID bits set to 0");
} }
@ -357,10 +568,12 @@ export default class Mii {
} }
public calculateCRC(): number { public calculateCRC(): number {
const view = this.bitStream.view; // #view is inaccessible
const data = new Uint8Array(
// @ts-expect-error _view is private this.buffer.buffer,
const data = view._view.subarray(0, 0x5e); this.buffer.byteOffset,
this.buffer.length,
).subarray(0, 0x5e);
let crc = 0x0000; let crc = 0x0000;
@ -506,7 +719,7 @@ export default class Mii {
instanceCount?: number; instanceCount?: number;
instanceRotationMode?: string; instanceRotationMode?: string;
data?: string; data?: string;
} = STUDIO_RENDER_DEFAULTS } = STUDIO_RENDER_DEFAULTS,
): string { ): string {
const params = { const params = {
...STUDIO_RENDER_DEFAULTS, ...STUDIO_RENDER_DEFAULTS,
@ -514,11 +727,23 @@ export default class Mii {
data: this.encodeStudio().toString("hex"), data: this.encodeStudio().toString("hex"),
}; };
params.type = STUDIO_RENDER_TYPES.includes(params.type as string) ? params.type : STUDIO_RENDER_DEFAULTS.type; params.type = STUDIO_RENDER_TYPES.includes(params.type as string)
params.expression = STUDIO_RENDER_EXPRESSIONS.includes(params.expression as string) ? params.expression : STUDIO_RENDER_DEFAULTS.expression; ? params.type
: STUDIO_RENDER_DEFAULTS.type;
params.expression = STUDIO_RENDER_EXPRESSIONS.includes(
params.expression as string,
)
? params.expression
: STUDIO_RENDER_DEFAULTS.expression;
params.width = Util.clamp(params.width, 512); params.width = Util.clamp(params.width, 512);
params.bgColor = STUDIO_BG_COLOR_REGEX.test(params.bgColor as string) ? params.bgColor : STUDIO_RENDER_DEFAULTS.bgColor; params.bgColor = STUDIO_BG_COLOR_REGEX.test(params.bgColor as string)
params.clothesColor = STUDIO_RENDER_CLOTHES_COLORS.includes(params.clothesColor) ? params.clothesColor : STUDIO_RENDER_DEFAULTS.clothesColor; ? params.bgColor
: STUDIO_RENDER_DEFAULTS.bgColor;
params.clothesColor = STUDIO_RENDER_CLOTHES_COLORS.includes(
params.clothesColor,
)
? params.clothesColor
: STUDIO_RENDER_DEFAULTS.clothesColor;
params.cameraXRotate = Util.clamp(params.cameraXRotate, 359); params.cameraXRotate = Util.clamp(params.cameraXRotate, 359);
params.cameraYRotate = Util.clamp(params.cameraYRotate, 359); params.cameraYRotate = Util.clamp(params.cameraYRotate, 359);
params.cameraZRotate = Util.clamp(params.cameraZRotate, 359); params.cameraZRotate = Util.clamp(params.cameraZRotate, 359);
@ -528,16 +753,25 @@ export default class Mii {
params.lightXDirection = Util.clamp(params.lightXDirection, 359); params.lightXDirection = Util.clamp(params.lightXDirection, 359);
params.lightYDirection = Util.clamp(params.lightYDirection, 359); params.lightYDirection = Util.clamp(params.lightYDirection, 359);
params.lightZDirection = Util.clamp(params.lightZDirection, 359); params.lightZDirection = Util.clamp(params.lightZDirection, 359);
params.lightDirectionMode = STUDIO_RENDER_LIGHT_DIRECTION_MODS.includes(params.lightDirectionMode) params.lightDirectionMode = STUDIO_RENDER_LIGHT_DIRECTION_MODS.includes(
params.lightDirectionMode,
)
? params.lightDirectionMode ? params.lightDirectionMode
: STUDIO_RENDER_DEFAULTS.lightDirectionMode; : STUDIO_RENDER_DEFAULTS.lightDirectionMode;
params.instanceCount = Util.clamp(params.instanceCount, 1, 16); params.instanceCount = Util.clamp(params.instanceCount, 1, 16);
params.instanceRotationMode = STUDIO_RENDER_INSTANCE_ROTATION_MODES.includes(params.instanceRotationMode) params.instanceRotationMode =
? params.instanceRotationMode STUDIO_RENDER_INSTANCE_ROTATION_MODES.includes(
: STUDIO_RENDER_DEFAULTS.instanceRotationMode; params.instanceRotationMode,
)
? params.instanceRotationMode
: STUDIO_RENDER_DEFAULTS.instanceRotationMode;
// converts non-string params to strings // converts non-string params to strings
const query = new URLSearchParams(Object.fromEntries(Object.entries(params).map(([key, value]) => [key, value.toString()]))); const query = new URLSearchParams(
Object.fromEntries(
Object.entries(params).map(([key, value]) => [key, value.toString()]),
),
);
if (params.lightDirectionMode === "none") { if (params.lightDirectionMode === "none") {
query.delete("lightDirectionMode"); query.delete("lightDirectionMode");