From 1ff38232094507547c41e19904fe7d43e59cbea5 Mon Sep 17 00:00:00 2001 From: trafficlunar Date: Mon, 13 Apr 2026 17:39:04 +0100 Subject: [PATCH] feat: get dating prefs, birthday, voice, personality --- src/app/api/submit/route.ts | 36 +++++++++++++++++++++++++--- src/app/mii/[id]/download/route.ts | 32 +++++++++++++++++++++++++ src/app/mii/[id]/page.tsx | 14 +++++++++-- src/components/mii/instructions.tsx | 1 + src/components/submit-form/index.tsx | 2 +- 5 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 src/app/mii/[id]/download/route.ts diff --git a/src/app/api/submit/route.ts b/src/app/api/submit/route.ts index c8abe19..3a567f3 100644 --- a/src/app/api/submit/route.ts +++ b/src/app/api/submit/route.ts @@ -48,7 +48,7 @@ const submitSchema = z.object({ // Save data way miiDataFile: z .instanceof(File) - .refine((blob) => blob.size < 1024 * 30, "File too large") // TODO: actual size + .refine((blob) => blob.size < 1024 * 1024 * 1.5, "File too large") // TODO: actual size .optional(), // Manual way @@ -207,7 +207,16 @@ export async function POST(request: NextRequest) { const miiData = miiDataFileBuffer ? CharInfoEx.FromShareMiiFileArrayBuffer(miiDataFileBuffer) : undefined; if (way === "savedata") { - if (!miiData) return rateLimit.sendResponse({ error: "No mii data provided" }, 400); + if (!miiData || !miiDataFileBuffer || !miiDataFileArray) 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: { @@ -375,7 +384,28 @@ export async function POST(request: NextRequest) { }, height: miiData.height, weight: miiData.build, - // uh oh, no dating prefs, birthday, voice, personality + 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); diff --git a/src/app/mii/[id]/download/route.ts b/src/app/mii/[id]/download/route.ts new file mode 100644 index 0000000..ff73020 --- /dev/null +++ b/src/app/mii/[id]/download/route.ts @@ -0,0 +1,32 @@ +import { NextRequest, NextResponse } from "next/server"; +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 check = await rateLimit.handle(); + if (check) return check; + + const { id: slugId } = await params; + const parsed = idSchema.safeParse(slugId); + if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400); + const miiId = parsed.data; + + const mii = await prisma.mii.findUnique({ + where: { id: miiId }, + }); + + if (!mii || !mii.miiData) { + return new NextResponse("Not found", { status: 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 73efc43..9a01f07 100644 --- a/src/app/mii/[id]/page.tsx +++ b/src/app/mii/[id]/page.tsx @@ -371,9 +371,15 @@ export default async function MiiPage({ params }: Props) { {/* Buttons */} -
+
+ {mii.miiData && ( + + + Download + + )} @@ -391,7 +397,11 @@ export default async function MiiPage({ params }: Props) { Instructions -

All instructions are based off of the default Male Mii.

+

+ 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.youtubeId && ( diff --git a/src/components/mii/instructions.tsx b/src/components/mii/instructions.tsx index eb95d3e..a50b115 100644 --- a/src/components/mii/instructions.tsx +++ b/src/components/mii/instructions.tsx @@ -252,6 +252,7 @@ export default function MiiInstructions({ instructions, isUsingSaveFile }: Props {(height || weight || datingPreferences || voice || personality) && (

Misc

+

These contain sliders: 0 is middle, positive is to the right, negative is to the left

diff --git a/src/components/submit-form/index.tsx b/src/components/submit-form/index.tsx index 1beb7e5..7585a1c 100644 --- a/src/components/submit-form/index.tsx +++ b/src/components/submit-form/index.tsx @@ -433,7 +433,7 @@ export default function SubmitForm({ inQueueMiisCount }: Props) { type="button" className={`flex flex-col justify-center items-center rounded-xl p-4 shadow-md border-2 cursor-pointer text-center text-sm transition hover:scale-[1.03] ${way === "savedata" ? "bg-cyan-100 border-cyan-600" : "bg-zinc-50 border-zinc-300 hover:bg-cyan-100 hover:border-cyan-600"}`} > - .ltd file + .ltd file (Modded)