mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-03-28 11:13:16 +00:00
chore: update packages
also accidentally prettified some code along the way
This commit is contained in:
parent
4656b969d6
commit
e05533b19a
6 changed files with 1150 additions and 850 deletions
32
package.json
32
package.json
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1377
pnpm-lock.yaml
1377
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -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 });
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 =
|
||||||
|
STUDIO_RENDER_INSTANCE_ROTATION_MODES.includes(
|
||||||
|
params.instanceRotationMode,
|
||||||
|
)
|
||||||
? params.instanceRotationMode
|
? params.instanceRotationMode
|
||||||
: STUDIO_RENDER_DEFAULTS.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");
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue