From 98eb14853cc0194a8b5cae425cc190a5c67864c2 Mon Sep 17 00:00:00 2001 From: Emi Matchu Date: Thu, 8 Feb 2024 10:51:52 -0800 Subject: [PATCH] Support alt styles in outfit thumbnails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Oops, before this change, outfits with alt styles would still show the outfit as if no alt style were applied! Now, we have the `Outfit` GraphQL type be internally aware of alt styles, and set its `petAppearance` and `body` and `itemAppearances` fields accordingly. No change was required to the actual `/api/outfitImage` endpoint, once the GraphQL started returning the right thing! …because of that, I'm honestly kinda surprised that there's no obvious issues arising with the Impress 2020 outfit interface itself? But it seems to be correctly just, not showing alt styles at all, in the way I intended because I never added support to it. So, okay, cool! --- src/server/loaders.js | 24 +++++++++++++ src/server/types/Outfit.js | 59 ++++++++++++++++++------------- src/server/types/PetAppearance.js | 42 ++++++++++++++++++---- 3 files changed, 95 insertions(+), 30 deletions(-) diff --git a/src/server/loaders.js b/src/server/loaders.js index 9d6083f..cca9df9 100644 --- a/src/server/loaders.js +++ b/src/server/loaders.js @@ -1039,6 +1039,29 @@ const buildPetSwfAssetLoader = (db, loaders) => ); }); +const buildAltStyleSwfAssetLoader = (db, loaders) => + new DataLoader(async (altStyleIds) => { + const qs = altStyleIds.map((_) => "?").join(","); + const [rows] = await db.execute( + `SELECT sa.*, rel.parent_id FROM swf_assets sa + INNER JOIN parents_swf_assets rel ON + rel.parent_type = "AltStyle" AND + rel.swf_asset_id = sa.id + WHERE rel.parent_id IN (${qs})`, + altStyleIds, + ); + + const entities = rows.map(normalizeRow); + + for (const swfAsset of entities) { + loaders.swfAssetLoader.prime(swfAsset.id, swfAsset); + } + + return altStyleIds.map((altStyleId) => + entities.filter((e) => e.parentId === altStyleId), + ); + }); + const buildNeopetsConnectionLoader = (db) => new DataLoader(async (ids) => { const qs = ids.map((_) => "?").join(", "); @@ -1490,6 +1513,7 @@ function buildLoaders(db) { loaders.swfAssetByRemoteIdLoader = buildSwfAssetByRemoteIdLoader(db); loaders.itemSwfAssetLoader = buildItemSwfAssetLoader(db, loaders); loaders.petSwfAssetLoader = buildPetSwfAssetLoader(db, loaders); + loaders.altStyleSwfAssetLoader = buildAltStyleSwfAssetLoader(db, loaders); loaders.neopetsConnectionLoader = buildNeopetsConnectionLoader(db); loaders.outfitLoader = buildOutfitLoader(db); loaders.itemOutfitRelationshipsLoader = diff --git a/src/server/types/Outfit.js b/src/server/types/Outfit.js index 55f52ba..b032825 100644 --- a/src/server/types/Outfit.js +++ b/src/server/types/Outfit.js @@ -62,9 +62,9 @@ const resolvers = { const outfit = await outfitLoader.load(id); return outfit.name; }, - petAppearance: async ({ id }, _, { outfitLoader }) => { + petAppearance: async ({ id }, _, { outfitLoader, altStyleLoader }) => { const outfit = await outfitLoader.load(id); - return { id: outfit.petStateId }; + return { id: outfit.petStateId, altStyleId: outfit.altStyleId }; }, itemAppearances: async ( { id }, @@ -73,15 +73,26 @@ const resolvers = { outfitLoader, petStateLoader, petTypeLoader, + altStyleLoader, itemOutfitRelationshipsLoader, - } + }, ) => { - const [petType, relationships] = await Promise.all([ - outfitLoader - .load(id) - .then((outfit) => petStateLoader.load(outfit.petStateId)) - .then((petState) => petTypeLoader.load(petState.petTypeId)), - itemOutfitRelationshipsLoader.load(id), + const relationshipsPromise = itemOutfitRelationshipsLoader.load(id); + + const outfit = await outfitLoader.load(id); + const bodyIdPromise = + outfit.altStyleId != null + ? altStyleLoader + .load(outfit.altStyleId) + .then((altStyle) => altStyle.bodyId) + : petStateLoader + .load(outfit.petStateId) + .then((petState) => petTypeLoader.load(petState.petTypeId)) + .then((petType) => petType.bodyId); + + const [bodyId, relationships] = await Promise.all([ + bodyIdPromise, + relationshipsPromise, ]); const wornItemIds = relationships @@ -90,7 +101,7 @@ const resolvers = { return wornItemIds.map((itemId) => ({ item: { id: itemId }, - bodyId: petType.bodyId, + bodyId, })); }, wornItems: async ({ id }, _, { itemOutfitRelationshipsLoader }) => { @@ -157,11 +168,11 @@ const resolvers = { outfitLoader, petTypeBySpeciesAndColorLoader, petStatesForPetTypeLoader, - } + }, ) => { if (!currentUserId) { throw new Error( - "saveOutfit requires login for now. This might change!" + "saveOutfit requires login for now. This might change!", ); } @@ -172,7 +183,7 @@ const resolvers = { } if (outfit.userId !== currentUserId) { throw new Error( - `user ${currentUserId} does not own outfit ${outfit.id}` + `user ${currentUserId} does not own outfit ${outfit.id}`, ); } } @@ -181,7 +192,7 @@ const resolvers = { // suffixes. const baseName = (rawName || "Untitled outfit").replace( /\s*\([0-9]+\)\s*$/, - "" + "", ); const namePlaceholder = baseName.trim().replace(/_%/g, "\\$0") + "%"; @@ -192,10 +203,10 @@ const resolvers = { ` SELECT name FROM outfits WHERE user_id = ? AND name LIKE ? AND id != ?; `, - [currentUserId, namePlaceholder, id || ""] + [currentUserId, namePlaceholder, id || ""], ); const existingOutfitNames = new Set( - outfitRows.map(({ name }) => name.trim()) + outfitRows.map(({ name }) => name.trim()), ); // Then, get the unique name to use for this outfit: try the provided @@ -213,7 +224,7 @@ const resolvers = { }); if (!petType) { throw new Error( - `could not find pet type for species=${speciesId}, color=${colorId}` + `could not find pet type for species=${speciesId}, color=${colorId}`, ); } // TODO: We could query for this more directly, instead of loading all @@ -222,7 +233,7 @@ const resolvers = { const petState = petStates.find((ps) => getPoseFromPetState(ps) === pose); if (!petState) { throw new Error( - `could not find appearance for species=${speciesId}, color=${colorId}, pose=${pose}` + `could not find appearance for species=${speciesId}, color=${colorId}, pose=${pose}`, ); } @@ -242,7 +253,7 @@ const resolvers = { updated_at = CURRENT_TIMESTAMP() WHERE id = ?; `, - [name, petState.id, id] + [name, petState.id, id], ) : await connection.execute( ` @@ -250,7 +261,7 @@ const resolvers = { (name, pet_state_id, user_id, created_at, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP(), CURRENT_TIMESTAMP()); `, - [name, petState.id, currentUserId] + [name, petState.id, currentUserId], ); newOutfitId = id || String(result.insertId); @@ -264,14 +275,14 @@ const resolvers = { if (id) { await connection.execute( `DELETE FROM item_outfit_relationships WHERE outfit_id = ?;`, - [id] + [id], ); } if (wornItemIds.length > 0 || closetedItemIds.length > 0) { const itemRowPlaceholders = [ [...wornItemIds, ...closetedItemIds].map( - (_) => `(?, ?, ?, CURRENT_TIMESTAMP(), CURRENT_TIMESTAMP())` + (_) => `(?, ?, ?, CURRENT_TIMESTAMP(), CURRENT_TIMESTAMP())`, ), ].join(", "); const itemRowValues = [ @@ -286,7 +297,7 @@ const resolvers = { (outfit_id, item_id, is_worn, created_at, updated_at) VALUES ${itemRowPlaceholders}; `, - itemRowValues + itemRowValues, ); } @@ -314,7 +325,7 @@ const resolvers = { } if (outfit.userId !== currentUserId) { throw new Error( - `user ${currentUserId} does not own outfit ${outfit.id}` + `user ${currentUserId} does not own outfit ${outfit.id}`, ); } diff --git a/src/server/types/PetAppearance.js b/src/server/types/PetAppearance.js index 69b2e86..47336e5 100644 --- a/src/server/types/PetAppearance.js +++ b/src/server/types/PetAppearance.js @@ -279,13 +279,29 @@ const resolvers = { const petType = await petTypeLoader.load(petState.petTypeId); return { id: petType.speciesId }; }, - body: async ({ id }, _, { petStateLoader, petTypeLoader }) => { + body: async ( + { id, altStyleId }, + _, + { petStateLoader, petTypeLoader, altStyleLoader }, + ) => { const petState = await petStateLoader.load(id); + if (altStyleId != null) { + const altStyle = await altStyleLoader.load(altStyleId); + return { id: altStyle.bodyId }; + } const petType = await petTypeLoader.load(petState.petTypeId); return { id: petType.bodyId }; }, - bodyId: async ({ id }, _, { petStateLoader, petTypeLoader }) => { + bodyId: async ( + { id, altStyleId }, + _, + { petStateLoader, petTypeLoader, altStyleLoader }, + ) => { const petState = await petStateLoader.load(id); + if (altStyleId != null) { + const altStyle = await altStyleLoader.load(altStyleId); + return altStyle.bodyId; + } const petType = await petTypeLoader.load(petState.petTypeId); return petType.bodyId; }, @@ -293,14 +309,28 @@ const resolvers = { const petState = await petStateLoader.load(id); return getPoseFromPetState(petState); }, - layers: async ({ id }, _, { petSwfAssetLoader }) => { - const swfAssets = await petSwfAssetLoader.load(id); + layers: async ( + { id, altStyleId }, + _, + { petSwfAssetLoader, altStyleSwfAssetLoader }, + ) => { + const swfAssets = + altStyleId != null + ? await altStyleSwfAssetLoader.load(altStyleId) + : await petSwfAssetLoader.load(id); return swfAssets; }, - restrictedZones: async ({ id }, _, { petSwfAssetLoader }) => { + restrictedZones: async ( + { id, altStyleId }, + _, + { petSwfAssetLoader, altStyleSwfAssetLoader }, + ) => { // The restricted zones are defined on the layers. Load them and aggegate // the zones, then uniquify and sort them for ease of use. - const swfAssets = await petSwfAssetLoader.load(id); + const swfAssets = + altStyleId != null + ? await altStyleSwfAssetLoader.load(altStyleId) + : await petSwfAssetLoader.load(id); let restrictedZoneIds = swfAssets .map((sa) => getRestrictedZoneIds(sa.zonesRestrict)) .flat();