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",
"version": "0.1.0",
"private": true,
"packageManager": "pnpm@10.24.0",
"packageManager": "pnpm@10.28.2",
"scripts": {
"dev": "next dev",
"build": "next build",
@ -16,45 +16,45 @@
"@auth/prisma-adapter": "2.11.1",
"@bprogress/next": "^3.2.12",
"@hello-pangea/dnd": "^18.0.1",
"@prisma/client": "^6.19.1",
"bit-buffer": "^0.2.5",
"@prisma/client": "^6.19.2",
"bit-buffer": "^0.3.0",
"canvas-confetti": "^1.9.4",
"dayjs": "^1.11.19",
"downshift": "^9.0.13",
"embla-carousel-react": "^8.6.0",
"file-type": "^21.1.1",
"ioredis": "^5.8.2",
"file-type": "^21.3.0",
"ioredis": "^5.9.2",
"jsqr": "^1.4.0",
"next": "16.0.10",
"next": "16.1.6",
"next-auth": "5.0.0-beta.30",
"qrcode-generator": "^2.0.4",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-dropzone": "^14.3.8",
"react-webcam": "^7.2.0",
"satori": "^0.18.3",
"satori": "^0.19.1",
"seedrandom": "^3.0.5",
"sharp": "^0.34.5",
"sjcl-with-all": "1.0.8",
"swr": "^2.3.7",
"zod": "^4.1.13"
"swr": "^2.3.8",
"zod": "^4.3.6"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.3",
"@iconify/react": "^6.0.2",
"@tailwindcss/postcss": "^4.1.18",
"@types/canvas-confetti": "^1.9.0",
"@types/node": "^25.0.2",
"@types/react": "^19.2.7",
"@types/node": "^25.1.0",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
"@types/seedrandom": "^3.0.8",
"@types/sjcl": "^1.0.34",
"eslint": "^9.39.2",
"eslint-config-next": "16.0.10",
"prisma": "^6.19.1",
"eslint-config-next": "16.1.6",
"prisma": "^6.19.2",
"schema-dts": "^1.1.5",
"tailwindcss": "^4.1.18",
"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,
tags: tagsSchema,
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(),
image2: 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) {
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 check = await rateLimit.handle();
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();
if (!value) return rateLimit.sendResponse({ error: "Submissions are disabled" }, 409);
if (!value)
return rateLimit.sendResponse({ error: "Submissions are disabled" }, 409);
// Parse data
const formData = await request.formData();
@ -52,7 +60,10 @@ export async function POST(request: NextRequest) {
rawTags = JSON.parse(formData.get("tags") as string);
rawQrBytesRaw = JSON.parse(formData.get("qrBytesRaw") as string);
} 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({
@ -65,13 +76,26 @@ export async function POST(request: NextRequest) {
image3: formData.get("image3"),
});
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
const { name: uncensoredName, tags: uncensoredTags, description: uncensoredDescription, qrBytesRaw, image1, image2, image3 } = parsed.data;
if (!parsed.success)
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
const name = profanity.censor(uncensoredName);
const tags = uncensoredTags.map((t) => profanity.censor(t));
const description = uncensoredDescription && profanity.censor(uncensoredDescription);
const description =
uncensoredDescription && profanity.censor(uncensoredDescription);
// Validate image files
const images: File[] = [];
@ -83,7 +107,10 @@ export async function POST(request: NextRequest) {
if (imageValidation.valid) {
images.push(img);
} 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
const miiUploadsDirectory = path.join(uploadsDirectory, miiRecord.id.toString());
const miiUploadsDirectory = path.join(
uploadsDirectory,
miiRecord.id.toString(),
);
await fs.mkdir(miiUploadsDirectory, { recursive: true });
// Download the image of the Mii
@ -134,12 +164,17 @@ export async function POST(request: NextRequest) {
await prisma.mii.delete({ where: { id: miiRecord.id } });
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 {
// 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");
await fs.writeFile(studioFileLocation, studioWebpBuffer);
@ -156,7 +191,9 @@ export async function POST(request: NextRequest) {
const codeBuffer = Buffer.from(codeBase64, "base64");
// 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");
await fs.writeFile(codeFileLocation, codeWebpBuffer);
@ -166,7 +203,10 @@ export async function POST(request: NextRequest) {
await prisma.mii.delete({ where: { id: miiRecord.id } });
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
@ -175,10 +215,13 @@ export async function POST(request: NextRequest) {
images.map(async (image, index) => {
const buffer = Buffer.from(await image.arrayBuffer());
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);
})
}),
);
// Update database to tell it how many images exist
@ -192,7 +235,10 @@ export async function POST(request: NextRequest) {
});
} catch (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 });

View file

@ -31,12 +31,14 @@ export default function SubmitForm() {
if (files.length >= 3) return;
setFiles((prev) => [...prev, ...acceptedFiles]);
},
[files.length]
[files.length],
);
const [isQrScannerOpen, setIsQrScannerOpen] = useState(false);
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);
@ -76,7 +78,7 @@ export default function SubmitForm() {
const { id, error } = await response.json();
if (!response.ok) {
setError(error);
setError(String(error)); // app can crash if error message is not a string
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">
<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">
<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">
<h1 className="font-bold text-2xl line-clamp-1" title={name}>
{name || "Mii name"}
</h1>
<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) => (
<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}
</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>
<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>
{/* Separator */}
@ -210,15 +227,26 @@ export default function SubmitForm() {
<QrUpload setQrBytesRaw={setQrBytesRaw} />
<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} />
Use your camera
</button>
<QrScanner isOpen={isQrScannerOpen} setIsOpen={setIsQrScannerOpen} setQrBytesRaw={setQrBytesRaw} />
<QrScanner
isOpen={isQrScannerOpen}
setIsOpen={setIsQrScannerOpen}
setQrBytesRaw={setQrBytesRaw}
/>
<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>
{/* Separator */}
@ -237,14 +265,18 @@ export default function SubmitForm() {
</p>
</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>
<ImageList files={files} setFiles={setFiles} />
<hr className="border-zinc-300 my-2" />
<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" />
</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";
@ -11,78 +12,34 @@ export default class ExtendedBitStream extends BitStream {
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 {
// @ts-expect-error _index is private
const nextClosestByteIndex = 8 * Math.ceil(this._index / 8);
// @ts-expect-error _index is private
const bitDistance = nextClosestByteIndex - this._index;
const nextClosestByteIndex = 8 * Math.ceil(this.index / 8);
const bitDistance = nextClosestByteIndex - this.index;
this.skipBits(bitDistance);
}
public bitSeek(bitPos: number): void {
// @ts-expect-error _index is private
this._index = bitPos;
}
public skipBits(bitCount: number): void {
// @ts-expect-error _index is private
this._index += bitCount;
}
public skipBytes(bytes: number): void {
const bits = bytes * 8;
this.skipBits(bits);
this.index += bitCount;
}
public skipBit(): void {
this.skipBits(1);
}
public skipInt8(): void {
this.skipBytes(1);
}
public skipInt16(): void {
// Skipping a uint16 is the same as skipping 2 uint8's
this.skipBytes(2);
this.skipBits(16);
}
public readBit(): number {
return this.readBits(1);
}
public readBytes(length: number): number[] {
return Array(length)
.fill(0)
.map(() => this.readUint8());
return this.readBits(1, false);
}
public readBuffer(length: number): Buffer {
return Buffer.from(this.readBytes(length));
return Buffer.from(super.readBytes(length));
}
public readUTF16String(length: number): string {
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",
];
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"];
@ -74,6 +81,7 @@ const STUDIO_BG_COLOR_REGEX = /^[0-9A-F]{8}$/; // Mii Studio does not allow lowe
export default class Mii {
public bitStream: ExtendedBitStream;
public buffer: Buffer;
// Mii data
// can be sure that these are all initialized in decode()
@ -150,92 +158,292 @@ export default class Mii {
public checksum!: number;
constructor(buffer: Buffer) {
this.buffer = buffer;
this.bitStream = new ExtendedBitStream(buffer);
this.decode();
}
public validate(): void {
// 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
assert.ok(this.version === 0 || this.version === 3, `Invalid Mii version. Got ${this.version}, expected 0 or 3`);
assert.equal(typeof this.allowCopying, "boolean", `Invalid Mii allow copying. Got ${this.allowCopying}, expected true or false`);
assert.equal(typeof this.profanityFlag, "boolean", `Invalid Mii profanity flag. Got ${this.profanityFlag}, expected true or false`);
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.ok(
this.version === 0 || this.version === 3,
`Invalid Mii version. Got ${this.version}, expected 0 or 3`,
);
assert.equal(
typeof this.allowCopying,
"boolean",
`Invalid Mii allow copying. Got ${this.allowCopying}, expected true or false`,
);
assert.equal(
typeof this.profanityFlag,
"boolean",
`Invalid Mii profanity flag. Got ${this.profanityFlag}, expected true or false`,
);
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(
this.consoleMAC.length,
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.equal(typeof this.flipHair, "boolean", `Invalid flip hair flag. Got ${this.flipHair}, expected true or false`);
assert.ok(Util.inRange(this.eyeType, Util.range(60)), `Invalid Mii eye type. Got ${this.eyeType}, expected 0-59`);
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.equal(
typeof this.flipHair,
"boolean",
`Invalid flip hair flag. Got ${this.flipHair}, expected true or false`,
);
assert.ok(
Util.inRange(this.eyeType, Util.range(60)),
`Invalid Mii eye type. Got ${this.eyeType}, expected 0-59`,
);
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.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(
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(
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.mustacheScale, Util.range(9)), `Invalid Mii mustache scale. Got ${this.mustacheScale}, expected 0-8`);
assert.ok(Util.inRange(this.mustacheYPosition, Util.range(17)), `Invalid Mii mustache Y position. Got ${this.mustacheYPosition}, expected 0-16`);
assert.ok(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`);
assert.ok(
Util.inRange(this.mustacheScale, Util.range(9)),
`Invalid Mii mustache scale. Got ${this.mustacheScale}, expected 0-8`,
);
assert.ok(
Util.inRange(this.mustacheYPosition, Util.range(17)),
`Invalid Mii mustache Y position. Got ${this.mustacheYPosition}, expected 0-16`,
);
assert.ok(
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
/*
@ -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");
}
@ -357,10 +568,12 @@ export default class Mii {
}
public calculateCRC(): number {
const view = this.bitStream.view;
// @ts-expect-error _view is private
const data = view._view.subarray(0, 0x5e);
// #view is inaccessible
const data = new Uint8Array(
this.buffer.buffer,
this.buffer.byteOffset,
this.buffer.length,
).subarray(0, 0x5e);
let crc = 0x0000;
@ -506,7 +719,7 @@ export default class Mii {
instanceCount?: number;
instanceRotationMode?: string;
data?: string;
} = STUDIO_RENDER_DEFAULTS
} = STUDIO_RENDER_DEFAULTS,
): string {
const params = {
...STUDIO_RENDER_DEFAULTS,
@ -514,11 +727,23 @@ export default class Mii {
data: this.encodeStudio().toString("hex"),
};
params.type = STUDIO_RENDER_TYPES.includes(params.type as string) ? params.type : STUDIO_RENDER_DEFAULTS.type;
params.expression = STUDIO_RENDER_EXPRESSIONS.includes(params.expression as string) ? params.expression : STUDIO_RENDER_DEFAULTS.expression;
params.type = STUDIO_RENDER_TYPES.includes(params.type as string)
? 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.bgColor = STUDIO_BG_COLOR_REGEX.test(params.bgColor as string) ? params.bgColor : STUDIO_RENDER_DEFAULTS.bgColor;
params.clothesColor = STUDIO_RENDER_CLOTHES_COLORS.includes(params.clothesColor) ? params.clothesColor : STUDIO_RENDER_DEFAULTS.clothesColor;
params.bgColor = STUDIO_BG_COLOR_REGEX.test(params.bgColor as string)
? 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.cameraYRotate = Util.clamp(params.cameraYRotate, 359);
params.cameraZRotate = Util.clamp(params.cameraZRotate, 359);
@ -528,16 +753,25 @@ export default class Mii {
params.lightXDirection = Util.clamp(params.lightXDirection, 359);
params.lightYDirection = Util.clamp(params.lightYDirection, 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
: STUDIO_RENDER_DEFAULTS.lightDirectionMode;
params.instanceCount = Util.clamp(params.instanceCount, 1, 16);
params.instanceRotationMode = STUDIO_RENDER_INSTANCE_ROTATION_MODES.includes(params.instanceRotationMode)
params.instanceRotationMode =
STUDIO_RENDER_INSTANCE_ROTATION_MODES.includes(
params.instanceRotationMode,
)
? params.instanceRotationMode
: STUDIO_RENDER_DEFAULTS.instanceRotationMode;
// 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") {
query.delete("lightDirectionMode");