diff --git a/package.json b/package.json index 87f77fc..b4776b0 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "downshift": "^9.3.2", "embla-carousel-react": "^8.6.0", "file-type": "^22.0.1", + "fzstd": "^0.1.1", "jsqr": "^1.4.0", "next": "16.2.3", "next-auth": "5.0.0-beta.30", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04e2d59..304c5d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: file-type: specifier: ^22.0.1 version: 22.0.1 + fzstd: + specifier: ^0.1.1 + version: 0.1.1 jsqr: specifier: ^1.4.0 version: 1.4.0 @@ -2202,6 +2205,9 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + fzstd@0.1.1: + resolution: {integrity: sha512-dkuVSOKKwh3eas5VkJy1AW1vFpet8TA/fGmVA5krThl8YcOVE/8ZIoEA1+U1vEn5ckxxhLirSdY837azmbaNHA==} + generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} @@ -5596,6 +5602,8 @@ snapshots: functions-have-names@1.2.3: {} + fzstd@0.1.1: {} + generator-function@2.0.1: {} gensync@1.0.0-beta.2: {} diff --git a/prisma/migrations/20260409185328_switch_mii_data/migration.sql b/prisma/migrations/20260409185328_switch_mii_data/migration.sql deleted file mode 100644 index a8ba66d..0000000 --- a/prisma/migrations/20260409185328_switch_mii_data/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "miis" ADD COLUMN "miiData" BYTEA; diff --git a/prisma/migrations/20260414195743_switch_save_file/migration.sql b/prisma/migrations/20260414195743_switch_save_file/migration.sql new file mode 100644 index 0000000..7c831e6 --- /dev/null +++ b/prisma/migrations/20260414195743_switch_save_file/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "miis" ADD COLUMN "isFromSaveFile" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 64eb3ba..9abe5b7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -83,7 +83,7 @@ model Mii { gender MiiGender? makeup MiiMakeup? - miiData Bytes? + isFromSaveFile Boolean @default(false) firstName String? lastName String? diff --git a/public/tutorial/switch/adding-mii/step1.jpg b/public/tutorial/switch/adding-mii/manual/step1.jpg similarity index 100% rename from public/tutorial/switch/adding-mii/step1.jpg rename to public/tutorial/switch/adding-mii/manual/step1.jpg diff --git a/public/tutorial/switch/adding-mii/step2.jpg b/public/tutorial/switch/adding-mii/manual/step2.jpg similarity index 100% rename from public/tutorial/switch/adding-mii/step2.jpg rename to public/tutorial/switch/adding-mii/manual/step2.jpg diff --git a/public/tutorial/switch/adding-mii/step3.png b/public/tutorial/switch/adding-mii/manual/step3.png similarity index 100% rename from public/tutorial/switch/adding-mii/step3.png rename to public/tutorial/switch/adding-mii/manual/step3.png diff --git a/public/tutorial/switch/adding-mii/step4.jpg b/public/tutorial/switch/adding-mii/manual/step4.jpg similarity index 100% rename from public/tutorial/switch/adding-mii/step4.jpg rename to public/tutorial/switch/adding-mii/manual/step4.jpg diff --git a/public/tutorial/switch/adding-mii/manual/thumbnail.png b/public/tutorial/switch/adding-mii/manual/thumbnail.png new file mode 100644 index 0000000..1471fbd Binary files /dev/null and b/public/tutorial/switch/adding-mii/manual/thumbnail.png differ diff --git a/public/tutorial/switch/adding-mii/modded/step1.jpg b/public/tutorial/switch/adding-mii/modded/step1.jpg new file mode 100644 index 0000000..a7bacef Binary files /dev/null and b/public/tutorial/switch/adding-mii/modded/step1.jpg differ diff --git a/public/tutorial/switch/adding-mii/modded/step2.png b/public/tutorial/switch/adding-mii/modded/step2.png new file mode 100644 index 0000000..ea36cd9 Binary files /dev/null and b/public/tutorial/switch/adding-mii/modded/step2.png differ diff --git a/public/tutorial/switch/adding-mii/modded/step3.jpg b/public/tutorial/switch/adding-mii/modded/step3.jpg new file mode 100644 index 0000000..9cad088 Binary files /dev/null and b/public/tutorial/switch/adding-mii/modded/step3.jpg differ diff --git a/public/tutorial/switch/adding-mii/modded/thumbnail.png b/public/tutorial/switch/adding-mii/modded/thumbnail.png new file mode 100644 index 0000000..3a05302 Binary files /dev/null and b/public/tutorial/switch/adding-mii/modded/thumbnail.png differ diff --git a/src/app/api/submit/route.ts b/src/app/api/submit/route.ts index 3a567f3..1e8db43 100644 --- a/src/app/api/submit/route.ts +++ b/src/app/api/submit/route.ts @@ -23,6 +23,7 @@ import { SwitchMiiInstructions } from "@/types"; import { minifyInstructions } from "@/lib/switch"; import { settings } from "@/lib/settings"; import { CharInfoEx } from "charinfo-ex"; +import { SwitchTomodachiLifeMii } from "@/lib/switch-tomodachi-life-mii"; const uploadsDirectory = path.join(process.cwd(), "uploads", "mii"); @@ -203,212 +204,15 @@ export async function POST(request: NextRequest) { } const miiDataFileBuffer = miiDataFile ? await miiDataFile.arrayBuffer() : undefined; - const miiDataFileArray = miiDataFileBuffer ? new Uint8Array(miiDataFileBuffer) : undefined; const miiData = miiDataFileBuffer ? CharInfoEx.FromShareMiiFileArrayBuffer(miiDataFileBuffer) : undefined; + let parsedSwitchMii: SwitchTomodachiLifeMii | undefined = undefined; + if (way === "savedata") { - if (!miiData || !miiDataFileBuffer || !miiDataFileArray) return rateLimit.sendResponse({ error: "No valid Mii data provided" }, 400); + if (!miiData || !miiDataFileBuffer) return rateLimit.sendResponse({ error: "No valid Mii data provided" }, 400); - const view = new DataView(miiDataFileBuffer); - - const parse = (index: number): number => view.getUint8(161 + index * 4); - - const age = view.getUint32(0x00e1, true); - const year = view.getUint32(0x00d9, true); - - const dontAge = age !== 0xffffffff; - - const instructions: Partial = { - head: { - type: miiData.facelineType, - skinColor: miiData.facelineColor, - }, - hair: { - set: miiData.hairType, - bangs: miiData.hairTypeFront, - back: miiData.hairTypeBack, - color: miiData.hairColor0, - subColor: miiData.hairColor1, - subColor2: miiData.hairColor0, // TODO: check - style: miiData.hairStyle, - // uh oh, no flipped - isFlipped: false, - }, - eyebrows: { - type: miiData.eyebrowType, - color: miiData.eyebrowColor, - height: miiData.eyebrowY - 10, - distance: miiData.eyebrowX - 4, - rotation: miiData.eyebrowRotate - 6, - size: miiData.eyebrowScale - 4, - stretch: miiData.eyebrowAspect - 3, - }, - eyes: { - main: { - type: miiData.eyeType, - color: miiData.eyeColor, - height: miiData.eyeY - 12, - distance: miiData.eyeX - 2, - rotation: miiData.eyeRotate - 4, - size: miiData.eyeScale - 4, - stretch: miiData.eyeAspect - 3, - }, - eyelashesTop: { - type: miiData.eyelashUpperType, - height: miiData.eyelashUpperY, - distance: miiData.eyelashUpperX, - rotation: miiData.eyelashUpperRotate, - size: miiData.eyelashUpperScale, - stretch: miiData.eyelashUpperAspect, - }, - eyelashesBottom: { - type: miiData.eyelashLowerType, - height: miiData.eyelashLowerY, - distance: miiData.eyelashLowerX, - rotation: miiData.eyelashLowerRotate, - size: miiData.eyelashLowerScale, - stretch: miiData.eyelashLowerAspect, - }, - eyelidTop: { - type: miiData.eyelidUpperType, - height: miiData.eyelidUpperY, - distance: miiData.eyelidUpperX, - rotation: miiData.eyelidUpperRotate, - size: miiData.eyelidUpperScale, - stretch: miiData.eyelidUpperAspect, - }, - eyelidBottom: { - type: miiData.eyelidLowerType, - height: miiData.eyelidLowerY, - distance: miiData.eyelidLowerX, - rotation: miiData.eyelidLowerRotate, - size: miiData.eyelidLowerScale, - stretch: miiData.eyelidLowerAspect, - }, - eyeliner: { - type: miiData.eyeShadowColor != 0, - color: miiData.eyeShadowColor, - }, - pupil: { - type: miiData.eyeHighlightType, - height: miiData.eyeHighlightY, - distance: miiData.eyeHighlightX, - rotation: miiData.eyeHighlightRotate, - size: miiData.eyeHighlightScale, - stretch: miiData.eyeHighlightAspect, - }, - }, - nose: { - type: miiData.noseType, - height: miiData.noseY - 9, - size: miiData.noseScale - 4, - }, - lips: { - type: miiData.mouthType, - color: miiData.mouthColor, - height: miiData.mouthY - 13, - rotation: miiData.mouthRotate, - size: miiData.mouthScale - 4, - stretch: miiData.mouthAspect - 3, - // uh oh, no lipstick - hasLipstick: false, - }, - ears: { - type: miiData.earType, - height: miiData.earY - 4, - size: miiData.earScale - 2, - }, - glasses: { - type: miiData.glassType1, - type2: miiData.glassType2, - ringColor: miiData.glassColor1, - shadesColor: miiData.glassColor2, - height: miiData.glassY - 11, - size: miiData.glassScale - 4, - stretch: miiData.glassAspect - 3, - }, - other: { - wrinkles1: { - type: miiData.wrinkleLowerType, - height: miiData.wrinkleLowerY - 15, - distance: miiData.wrinkleLowerX - 2, - size: miiData.wrinkleLowerScale - 6, - stretch: miiData.wrinkleLowerAspect - 3, - }, - wrinkles2: { - type: miiData.wrinkleUpperType, - height: miiData.wrinkleUpperY - 23, - distance: miiData.wrinkleUpperX - 7, - size: miiData.wrinkleUpperScale - 6, - stretch: miiData.wrinkleUpperAspect - 3, - }, - beard: { - type: miiData.beardType, - color: miiData.beardColor, - }, - moustache: { - type: miiData.mustacheType, - color: miiData.mustacheColor, - height: miiData.mustacheY - 10, - // uh oh, no flipped - isFlipped: false, - size: miiData.mustacheScale - 4, - stretch: miiData.mustacheAspect - 3, - }, - goatee: { - type: miiData.beardShortType, - color: miiData.beardShortColor, - }, - mole: { - type: miiData.moleX != 0, - height: miiData.moleY - 20, - distance: miiData.moleX - 2, - size: miiData.moleScale - 4, - }, - eyeShadow: { - type: miiData.makeup0, - color: miiData.makeup0Color, - height: miiData.makeup0Y - 12, - distance: miiData.makeup0X - 1, - size: miiData.makeup0Scale - 6, - stretch: miiData.makeup0Aspect - 3, - }, - blush: { - type: miiData.makeup1, - color: miiData.makeup1Color, - height: miiData.makeup1Y - 19, - distance: miiData.makeup1X - 6, - size: miiData.makeup1Scale - 5, - stretch: miiData.makeup1Aspect - 3, - }, - }, - height: miiData.height, - weight: miiData.build, - datingPreferences: ([MiiGender.MALE, MiiGender.FEMALE, MiiGender.NONBINARY] as const).filter((_, i) => miiDataFileArray[0x01a9 + i] === 1), - birthday: { - month: parse(17), - day: parse(15), - age: dontAge ? age : new Date().getFullYear() - year, - dontAge, - }, - voice: { - speed: parse(6), - pitch: parse(8), - depth: parse(5), - delivery: Math.max(0, view.getInt8(0xc5)), // why is this an integer?? - tone: parse(7) + 1, - // preset type? - }, - personality: { - movement: parse(4) - 1, - speech: parse(2) - 1, - energy: parse(1) - 1, - thinking: parse(0) - 1, - overall: parse(3) - 1, - }, - }; - - minifiedInstructions = minifyInstructions(instructions); + parsedSwitchMii = new SwitchTomodachiLifeMii(miiDataFileBuffer, miiData); + minifiedInstructions = parsedSwitchMii.toInstructions(); } // Create Mii in database @@ -435,7 +239,7 @@ export async function POST(request: NextRequest) { youtubeId, makeup: makeup ?? "PARTIAL", instructions: minifiedInstructions, - ...(way === "savedata" && { miiData: miiDataFileArray }), + ...(way === "savedata" && { isFromSaveFile: true }), }), }, }); @@ -473,6 +277,20 @@ export async function POST(request: NextRequest) { .toBuffer(); const fileLocation = path.join(miiUploadsDirectory, "features.png"); await fs.writeFile(fileLocation, pngBuffer); + } else if (way === "savedata" && miiDataFileBuffer) { + const fileLocation = path.join(miiUploadsDirectory, "data.ltd"); + await fs.writeFile(fileLocation, Buffer.from(miiDataFileBuffer)); + + // Save face paint image + if (parsedSwitchMii) { + const pngBuffer = await parsedSwitchMii.extractFacePaintImage(); + if (pngBuffer) { + const fileLocation = path.join(miiUploadsDirectory, "features.png"); // Save as features because it isn't used + await fs.writeFile(fileLocation, pngBuffer); + } + } else { + return rateLimit.sendResponse({ error: "Failed to extract Switch Mii data" }, 500); + } } } diff --git a/src/app/mii/[id]/download/route.ts b/src/app/mii/[id]/download/route.ts index ff73020..cf219c0 100644 --- a/src/app/mii/[id]/download/route.ts +++ b/src/app/mii/[id]/download/route.ts @@ -1,10 +1,14 @@ import { NextRequest, NextResponse } from "next/server"; + +import fs from "fs/promises"; +import path from "path"; + import { prisma } from "@/lib/prisma"; import { RateLimit } from "@/lib/rate-limit"; import { idSchema } from "@/lib/schemas"; export async function GET(request: NextRequest, { params }: { params: { id: string } }) { - const rateLimit = new RateLimit(request, 200, "/mii/image"); + const rateLimit = new RateLimit(request, 4, "/mii/download"); const check = await rateLimit.handle(); if (check) return check; @@ -16,17 +20,17 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri const mii = await prisma.mii.findUnique({ where: { id: miiId }, }); + if (!mii) return new NextResponse("Not found", { status: 404 }); - if (!mii || !mii.miiData) { - return new NextResponse("Not found", { status: 404 }); + try { + const buffer = await fs.readFile(path.join(process.cwd(), "uploads", "mii", miiId.toString(), "data.ltd")); + return new NextResponse(buffer, { + headers: { + "Content-Type": "application/octet-stream", + "Content-Disposition": `attachment; filename="${mii.name}.ltd"`, + }, + }); + } catch { + return rateLimit.sendResponse({ error: "File not found" }, 404); } - - const fileName = `${mii.name}.ltd`; - - return new NextResponse(mii.miiData, { - headers: { - "Content-Type": "application/octet-stream", - "Content-Disposition": `attachment; filename="${fileName}"`, - }, - }); } diff --git a/src/app/mii/[id]/page.tsx b/src/app/mii/[id]/page.tsx index 9a01f07..a5ba679 100644 --- a/src/app/mii/[id]/page.tsx +++ b/src/app/mii/[id]/page.tsx @@ -164,7 +164,15 @@ export default async function MiiPage({ params }: Props) { /> ) : ( - !mii.miiData && ( + <> + {mii.isFromSaveFile && ( +
+
+ Face Paint Texture +
+
+ )} + - ) + )}
@@ -374,7 +382,7 @@ export default async function MiiPage({ params }: Props) {
- {mii.miiData && ( + {mii.isFromSaveFile && ( Download @@ -400,7 +408,7 @@ export default async function MiiPage({ params }: Props) {

All instructions are based off of the default Male Mii.
- {mii.miiData && "If you're on modded/emulator, you can download the .ltd file above."} + {mii.isFromSaveFile && "If you're on modded/emulator, you can download the .ltd file above."}

@@ -416,7 +424,7 @@ export default async function MiiPage({ params }: Props) { > )} - } isUsingSaveFile={mii.miiData !== null} /> + } isUsingSaveFile={mii.isFromSaveFile} /> )} diff --git a/src/components/mii/list/mii-grid.tsx b/src/components/mii/list/mii-grid.tsx index 206f48a..1f9e30b 100644 --- a/src/components/mii/list/mii-grid.tsx +++ b/src/components/mii/list/mii-grid.tsx @@ -10,6 +10,7 @@ import { Icon } from "@iconify/react"; import LikeButton from "@/components/like-button"; import DeleteMiiButton from "../delete-mii-button"; import Carousel from "@/components/carousel"; +import Image from "next/image"; interface Props { miis: Prisma.MiiGetPayload<{ include: { user: { select: { id: true; name: true } }; _count: { select: { likedBy: true } } } }>[]; @@ -44,20 +45,16 @@ export default function MiiGrid({ miis, userId, parentPage }: Props) { )} - `/mii/${mii.id}/image?type=image${index}`), - ]} - /> + + mii image +
-
+
{mii.name} -
+
{mii.platform === "SWITCH" ? ( ) : ( diff --git a/src/components/submit-form/index.tsx b/src/components/submit-form/index.tsx index 7585a1c..cb2dd8d 100644 --- a/src/components/submit-form/index.tsx +++ b/src/components/submit-form/index.tsx @@ -429,16 +429,14 @@ export default function SubmitForm({ inQueueMiisCount }: Props) {
-

Click on a way to see tutorials for them

+

Select a method above and click 'How to?' to view the tutorial.

{/* (Switch Only) Mii Screenshots */} @@ -503,7 +501,7 @@ export default function SubmitForm({ inQueueMiisCount }: Props) { {way === "manual" && ( <> - +

A tutorial on how to screenshot the features is above.

)} @@ -538,12 +536,13 @@ export default function SubmitForm({ inQueueMiisCount }: Props) {

- Save Data + ShareMii File
+ {/* YouTube */}
@@ -599,7 +598,7 @@ export default function SubmitForm({ inQueueMiisCount }: Props) {
- + Mii editor may be inaccurate. Instructions are recommended, but not required - you do not have to add every instruction. diff --git a/src/components/tutorial/index.tsx b/src/components/tutorial/index.tsx index 19fa3d8..59016f4 100644 --- a/src/components/tutorial/index.tsx +++ b/src/components/tutorial/index.tsx @@ -5,11 +5,13 @@ import { useEffect, useState } from "react"; import useEmblaCarousel from "embla-carousel-react"; import { Icon } from "@iconify/react"; import confetti from "canvas-confetti"; +import Link from "next/link"; interface Slide { // step is never used, undefined is assumed as a step type?: "start" | "step" | "finish"; text?: string; + link?: string; imageSrc?: string; } @@ -159,7 +161,13 @@ export default function Tutorial({ tutorials, isOpen, setIsOpen }: Props) {
) : ( <> -

{slide.text}

+ {slide.link ? ( + + {slide.text} + + ) : ( +

{slide.text}

+ )} view.getUint8(161 + index * 4); + + const age = view.getUint32(0x00e1, true); + const year = view.getUint32(0x00d9, true); + const dontAge = age !== 0xffffffff; + + this.datingPreferences = ([MiiGender.MALE, MiiGender.FEMALE, MiiGender.NONBINARY] as const).filter((_, i) => bytes[0x01a9 + i] === 1); + this.birthday = { + month: parse(17), + day: parse(15), + age: dontAge ? age : new Date().getFullYear() - year, + dontAge, + }; + this.voice = { + speed: parse(6), + pitch: parse(8), + depth: parse(5), + delivery: Math.max(0, view.getInt8(0xc5)), // why is this an integer?? + tone: parse(7) + 1, + // TODO: add voice preset to instructions type? + }; + this.personality = { + movement: parse(4) - 1, + speech: parse(2) - 1, + energy: parse(1) - 1, + thinking: parse(0) - 1, + overall: parse(3) - 1, + }; + } + + // There's a UGC Texture image but we're ignoring it + public async extractFacePaintImage(): Promise { + try { + const buf = Buffer.from(this.buffer); + + const canvasMarker = Buffer.from([0xa3, 0xa3, 0xa3, 0xa3]); + const ugcMarker = Buffer.from([0xa4, 0xa4, 0xa4, 0xa4]); + + const canvasStart = buf.indexOf(canvasMarker); + if (canvasStart === -1) return null; + + const ugcStart = buf.indexOf(ugcMarker); + const canvasData = buf.subarray(canvasStart + 4, ugcStart === -1 ? undefined : ugcStart); + + const decompressed = Buffer.from(fzstd.decompress(canvasData)); + const deswizzled = new BytesDeswizzle(decompressed, [256, 256], [1, 1], 4, 4).deswizzle(); + + return await sharp(deswizzled, { + raw: { width: 256, height: 256, channels: 4 }, + }) + .png() + .toBuffer(); + } catch (err) { + console.error("extractFacePaintImage failed:", err); + return null; + } + } + + public toInstructions() { + const instructions: Partial = { + head: { + type: this.data.facelineType, + skinColor: this.data.facelineColor, + }, + hair: { + set: this.data.hairType, + bangs: this.data.hairTypeFront, + back: this.data.hairTypeBack, + color: this.data.hairColor0, + subColor: this.data.hairColor1, + subColor2: this.data.hairColor0, // TODO: check + style: this.data.hairStyle, + isFlipped: (this.data.faceFlags & (1 << 2)) !== 0, // bangsSide + }, + eyebrows: { + type: this.data.eyebrowType, + color: this.data.eyebrowColor, + height: this.data.eyebrowY - 10, + distance: this.data.eyebrowX - 4, + rotation: this.data.eyebrowRotate - 6, + size: this.data.eyebrowScale - 4, + stretch: this.data.eyebrowAspect - 3, + }, + eyes: { + main: { + type: this.data.eyeType, + color: this.data.eyeColor, + height: this.data.eyeY - 12, + distance: this.data.eyeX - 2, + rotation: this.data.eyeRotate - 4, + size: this.data.eyeScale - 4, + stretch: this.data.eyeAspect - 3, + }, + eyelashesTop: { + type: this.data.eyelashUpperType, + height: this.data.eyelashUpperY, + distance: this.data.eyelashUpperX, + rotation: this.data.eyelashUpperRotate, + size: this.data.eyelashUpperScale, + stretch: this.data.eyelashUpperAspect, + }, + eyelashesBottom: { + type: this.data.eyelashLowerType, + height: this.data.eyelashLowerY, + distance: this.data.eyelashLowerX, + rotation: this.data.eyelashLowerRotate, + size: this.data.eyelashLowerScale, + stretch: this.data.eyelashLowerAspect, + }, + eyelidTop: { + type: this.data.eyelidUpperType, + height: this.data.eyelidUpperY, + distance: this.data.eyelidUpperX, + rotation: this.data.eyelidUpperRotate, + size: this.data.eyelidUpperScale, + stretch: this.data.eyelidUpperAspect, + }, + eyelidBottom: { + type: this.data.eyelidLowerType, + height: this.data.eyelidLowerY, + distance: this.data.eyelidLowerX, + rotation: this.data.eyelidLowerRotate, + size: this.data.eyelidLowerScale, + stretch: this.data.eyelidLowerAspect, + }, + eyeliner: { + type: (this.data.faceFlags & (1 << 4)) !== 0, // eyeShadowEnabled + color: this.data.eyeShadowColor, + }, + pupil: { + type: this.data.eyeHighlightType, + height: this.data.eyeHighlightY, + distance: this.data.eyeHighlightX, + rotation: this.data.eyeHighlightRotate, + size: this.data.eyeHighlightScale, + stretch: this.data.eyeHighlightAspect, + }, + }, + nose: { + type: this.data.noseType, + height: this.data.noseY - 9, + size: this.data.noseScale - 4, + }, + lips: { + type: this.data.mouthType, + color: this.data.mouthColor, + height: this.data.mouthY - 13, + rotation: this.data.mouthRotate, + size: this.data.mouthScale - 4, + stretch: this.data.mouthAspect - 3, + hasLipstick: (this.data.faceFlags & (1 << 5)) !== 0, // mouthInvert + }, + ears: { + type: this.data.earType, + height: this.data.earY - 4, + size: this.data.earScale - 2, + }, + glasses: { + type: this.data.glassType1, + type2: this.data.glassType2, + ringColor: this.data.glassColor1, + shadesColor: this.data.glassColor2, + height: this.data.glassY - 11, + size: this.data.glassScale - 4, + stretch: this.data.glassAspect - 3, + }, + other: { + wrinkles1: { + type: this.data.wrinkleLowerType, + height: this.data.wrinkleLowerY - 15, + distance: this.data.wrinkleLowerX - 2, + size: this.data.wrinkleLowerScale - 6, + stretch: this.data.wrinkleLowerAspect - 3, + }, + wrinkles2: { + type: this.data.wrinkleUpperType, + height: this.data.wrinkleUpperY - 23, + distance: this.data.wrinkleUpperX - 7, + size: this.data.wrinkleUpperScale - 6, + stretch: this.data.wrinkleUpperAspect - 3, + }, + beard: { + type: this.data.beardType, + color: this.data.beardColor, + }, + moustache: { + type: this.data.mustacheType, + color: this.data.mustacheColor, + height: this.data.mustacheY - 10, + isFlipped: (this.data.faceFlags & (1 << 6)) !== 0, // mustacheInverted + size: this.data.mustacheScale - 4, + stretch: this.data.mustacheAspect - 3, + }, + goatee: { + type: this.data.beardShortType, + color: this.data.beardShortColor, + }, + mole: { + type: this.data.moleX != 0, + height: this.data.moleY - 20, + distance: this.data.moleX - 2, + size: this.data.moleScale - 4, + }, + eyeShadow: { + type: this.data.makeup0, + color: this.data.makeup0Color, + height: this.data.makeup0Y - 12, + distance: this.data.makeup0X - 1, + size: this.data.makeup0Scale - 6, + stretch: this.data.makeup0Aspect - 3, + }, + blush: { + type: this.data.makeup1, + color: this.data.makeup1Color, + height: this.data.makeup1Y - 19, + distance: this.data.makeup1X - 6, + size: this.data.makeup1Scale - 5, + stretch: this.data.makeup1Aspect - 3, + }, + }, + height: this.data.height, + weight: this.data.build, + datingPreferences: this.datingPreferences, + birthday: this.birthday, + voice: this.voice, + personality: this.personality, + }; + + return minifyInstructions(instructions); + } +}