diff --git a/apps/api/src/openapi.ts b/apps/api/src/openapi.ts index 70b604d..cc6a1ba 100644 --- a/apps/api/src/openapi.ts +++ b/apps/api/src/openapi.ts @@ -487,13 +487,8 @@ export const LegacyAvatarItemSaves = z.object({ customAvatarItemSavesByAvatarItemDesc: z.record(z.string(), CustomAvatarItemSave), }) -/** - * `GET /outfits/me` — the outfit envelope. Either the outfit stored in slot 0, served - * back exactly as it was saved, or (for a player who has never saved) the brand-new- - * account form, where every field that would carry an outfit is null/empty and - * `DataVersion` is 9. - */ -export const OutfitsMeResponse = z.object({ +/** `GET /outfits/me` — the outfit stored in slot 0, served back exactly as it was saved. */ +export const StoredOutfit = z.object({ LegacyData: z.object({ SelectionsV1: z.string().nullable().describe('Semicolon-delimited legacy descriptors'), SelectionsV2: z.string().nullable().describe('JSON-in-a-string: `{ selections: [...] }`'), @@ -502,7 +497,7 @@ export const OutfitsMeResponse = z.object({ HairColor: z.string().nullable(), }), Selections: JsonArray, - DataVersion: z.int().describe('9 in the new-account envelope; whatever was saved otherwise'), + DataVersion: z.int().describe('The client’s outfit format version, as saved'), CustomizationSettings: z .string() .nullable() @@ -513,6 +508,22 @@ export const OutfitsMeResponse = z.object({ Slot: z.int().describe('0 — the outfit being worn'), }) +/** + * `GET /outfits/me` for a player who has never saved — the brand-new-account envelope. + * Flatter than a stored outfit rather than a nulled-out copy of it: four empty strings and + * nothing else, no `LegacyData`, no `Selections`, no `DataVersion`. `OutfitSelections` is + * the flat field name here, not `SelectionsV1`/`SelectionsV2`. + */ +export const EmptyOutfit = z.object({ + FaceFeatures: z.string(), + HairColor: z.string(), + OutfitSelections: z.string(), + SkinColor: z.string(), +}) + +/** `GET /outfits/me` — the stored outfit, or the empty envelope for a new player. */ +export const OutfitsMeResponse = z.union([StoredOutfit, EmptyOutfit]) + /** * `PUT /outfits/me` JSON body — the outfit the client is saving, in the newer envelope. * The heavy fields are JSON-in-a-string, exactly as the client serialises them: diff --git a/apps/api/src/routes/avatar.ts b/apps/api/src/routes/avatar.ts index cb8d78f..41a04f9 100644 --- a/apps/api/src/routes/avatar.ts +++ b/apps/api/src/routes/avatar.ts @@ -334,8 +334,9 @@ export const avatarRoutes = new Hono({ strict: false }) '`outfit` table — the newer client treats slot 0 as the outfit currently worn — and ' + 'handed back exactly as it was saved, since the payload’s heavy fields are the ' + 'client’s own JSON-in-a-string documents.\n\n' + - 'A player who has never saved gets the brand-new-account envelope: all-null ' + - '`LegacyData`, no `Selections`, `DataVersion` 9.', + 'A player who has never saved gets the brand-new-account envelope, which is a ' + + 'different, flatter shape than a stored outfit: the four empty-string fields ' + + '`FaceFeatures`, `HairColor`, `OutfitSelections` and `SkinColor`, and nothing else.', security: AUTHED, responses: { 200: json(OutfitsMeResponse, 'The stored outfit, or the empty envelope'), @@ -350,20 +351,10 @@ export const avatarRoutes = new Hono({ strict: false }) if (outfit !== null) return c.json(outfit) return c.json({ - LegacyData: { - SelectionsV1: null, - SelectionsV2: null, - FaceFeatures: null, - SkinColor: null, - HairColor: null, - }, - Selections: [], - DataVersion: 9, - CustomizationSettings: null, - ThumbnailFileName: null, - Name: null, - Accessibility: 0, - Slot: 0, + FaceFeatures: '', + HairColor: '', + OutfitSelections: '', + SkinColor: '', }) } ) diff --git a/apps/api/src/test/integration/api.test.ts b/apps/api/src/test/integration/api.test.ts index af2dc9a..a06e115 100644 --- a/apps/api/src/test/integration/api.test.ts +++ b/apps/api/src/test/integration/api.test.ts @@ -670,20 +670,10 @@ describe('public endpoints', () => { const res = await exports.default.fetch(`${ORIGIN}/outfits/me`, { headers: await bearer('77') }) expect(res.status).toBe(200) expect(await res.json()).toEqual({ - LegacyData: { - SelectionsV1: null, - SelectionsV2: null, - FaceFeatures: null, - SkinColor: null, - HairColor: null, - }, - Selections: [], - DataVersion: 9, - CustomizationSettings: null, - ThumbnailFileName: null, - Name: null, - Accessibility: 0, - Slot: 0, + FaceFeatures: '', + HairColor: '', + OutfitSelections: '', + SkinColor: '', }) })