diff --git a/src/app/WardrobePage/support/ItemLayerSupportModal.js b/src/app/WardrobePage/support/ItemLayerSupportModal.js index 40f0f0d..6f84431 100644 --- a/src/app/WardrobePage/support/ItemLayerSupportModal.js +++ b/src/app/WardrobePage/support/ItemLayerSupportModal.js @@ -21,6 +21,9 @@ import { Spinner, useDisclosure, useToast, + CheckboxGroup, + VStack, + Checkbox, } from "@chakra-ui/react"; import { ChevronRightIcon, ExternalLinkIcon } from "@chakra-ui/icons"; @@ -45,6 +48,10 @@ function ItemLayerSupportModal({ onClose, }) { const [selectedBodyId, setSelectedBodyId] = React.useState(itemLayer.bodyId); + const [selectedKnownGlitches, setSelectedKnownGlitches] = React.useState( + itemLayer.knownGlitches + ); + const [previewBiology, setPreviewBiology] = React.useState({ speciesId: outfitState.speciesId, colorId: outfitState.colorId, @@ -63,6 +70,7 @@ function ItemLayerSupportModal({ mutation ItemSupportSetLayerBodyId( $layerId: ID! $bodyId: ID! + $knownGlitches: [AppearanceLayerKnownGlitch!]! $supportSecret: String! $outfitSpeciesId: ID! $outfitColorId: ID! @@ -98,6 +106,16 @@ function ItemLayerSupportModal({ } } } + + setLayerKnownGlitches( + layerId: $layerId + knownGlitches: $knownGlitches + supportSecret: $supportSecret + ) { + id + knownGlitches + svgUrl # Affected by OFFICIAL_SVG_IS_INCORRECT + } } ${itemAppearanceFragment} `, @@ -105,6 +123,7 @@ function ItemLayerSupportModal({ variables: { layerId: itemLayer.id, bodyId: selectedBodyId, + knownGlitches: selectedKnownGlitches, supportSecret, outfitSpeciesId: outfitState.speciesId, outfitColorId: outfitState.colorId, @@ -251,6 +270,11 @@ function ItemLayerSupportModal({ onChangeBodyId={setSelectedBodyId} onChangePreviewBiology={setPreviewBiology} /> + + - Pet compatibility + Pet compatibility + Known glitches + + + + Official SVG is incorrect{" "} + + (Will use the PNG instead) + + + + + + ); +} + function ItemLayerSupportModalRemoveButton({ item, itemLayer, diff --git a/src/app/components/useOutfitAppearance.js b/src/app/components/useOutfitAppearance.js index 7d3086f..600d11f 100644 --- a/src/app/components/useOutfitAppearance.js +++ b/src/app/components/useOutfitAppearance.js @@ -169,6 +169,7 @@ export const itemAppearanceFragment = gql` canvasMovieLibraryUrl imageUrl(size: SIZE_600) swfUrl # HACK: This is for Support tools, but other views don't need it + knownGlitches # HACK: This is for Support tools, but other views don't need it bodyId zone { label @client # HACK: This is for Support tools, but other views don't need it diff --git a/src/server/types/AppearanceLayer.js b/src/server/types/AppearanceLayer.js index ef36ec3..3629667 100644 --- a/src/server/types/AppearanceLayer.js +++ b/src/server/types/AppearanceLayer.js @@ -64,6 +64,13 @@ const typeDefs = gql` """ item: Item + """ + Glitches that we know to affect this appearance layer. This can be useful + for changing our behavior to match official behavior, or to alert the user + that our behavior _doesn't_ match official behavior. + """ + knownGlitches: [AppearanceLayerKnownGlitch!]! + """ The zones that this layer restricts, if any. Note that, for item layers, this is generally empty and the restriction is on the ItemAppearance, not @@ -75,6 +82,15 @@ const typeDefs = gql` restrictedZones: [Zone!]! } + enum AppearanceLayerKnownGlitch { + # This glitch means that, while the official manifest declares an SVG + # version of this layer, it is incorrect and does not visually match the + # PNG version that the official pet editor users. + # + # For affected layers, svgUrl will be null, regardless of the manifest. + OFFICIAL_SVG_IS_INCORRECT + } + extend type Query { # Return the number of layers that have been converted to HTML5, optionally # filtered by type. Cache for 30 minutes (we re-sync with Neopets every @@ -138,6 +154,13 @@ const resolvers = { }, svgUrl: async ({ id }, _, { db, swfAssetLoader }) => { const layer = await swfAssetLoader.load(id); + + if ( + layer.knownGlitches.split(",").includes("OFFICIAL_SVG_IS_INCORRECT") + ) { + return null; + } + let manifest = layer.manifest && JSON.parse(layer.manifest); // When the manifest is specifically null, that means we don't know if @@ -254,6 +277,15 @@ const resolvers = { return { id: String(rows[0].parent_id) }; }, + knownGlitches: async ({ id }, _, { swfAssetLoader }) => { + const layer = await swfAssetLoader.load(id); + + if (!layer.knownGlitches) { + return []; + } + + return layer.knownGlitches.split(","); + }, }, Query: { diff --git a/src/server/types/MutationsForSupport.js b/src/server/types/MutationsForSupport.js index 03be98d..06368de 100644 --- a/src/server/types/MutationsForSupport.js +++ b/src/server/types/MutationsForSupport.js @@ -1,13 +1,6 @@ import { gql } from "apollo-server"; import { ManagementClient } from "auth0"; -const auth0 = new ManagementClient({ - domain: "openneo.us.auth0.com", - clientId: process.env.AUTH0_SUPPORT_CLIENT_ID, - clientSecret: process.env.AUTH0_SUPPORT_CLIENT_SECRET, - scope: "read:users update:users", -}); - import { capitalize, getPoseFromPetState, @@ -18,6 +11,13 @@ import { normalizeRow, } from "../util"; +const auth0 = new ManagementClient({ + domain: "openneo.us.auth0.com", + clientId: process.env.AUTH0_SUPPORT_CLIENT_ID, + clientSecret: process.env.AUTH0_SUPPORT_CLIENT_SECRET, + scope: "read:users update:users", +}); + const typeDefs = gql` type RemoveLayerFromItemMutationResult { layer: AppearanceLayer! @@ -47,7 +47,13 @@ const typeDefs = gql` layerId: ID! bodyId: ID! supportSecret: String! - ): AppearanceLayer! + ): AppearanceLayer + + setLayerKnownGlitches( + layerId: ID! + knownGlitches: [AppearanceLayerKnownGlitch!]! + supportSecret: String! + ): AppearanceLayer removeLayerFromItem( layerId: ID! @@ -289,6 +295,21 @@ const resolvers = { assertSupportSecretOrThrow(supportSecret); const oldSwfAsset = await swfAssetLoader.load(layerId); + if (!oldSwfAsset) { + console.warn( + `Skipping setLayerBodyId for unknown layer ID: ${layerId}` + ); + return null; + } + + // Skip the update, and the logging, if there's no change. + if (oldSwfAsset.bodyId === bodyId) { + console.info( + `Skipping setLayerBodyId for ${layerId}: no change. ` + + `(bodyId=${oldSwfAsset.bodyId})` + ); + return { id: layerId }; + } const [ result, @@ -361,6 +382,103 @@ const resolvers = { return { id: layerId }; }, + setLayerKnownGlitches: async ( + _, + { layerId, knownGlitches, supportSecret }, + { + itemLoader, + itemTranslationLoader, + swfAssetLoader, + zoneTranslationLoader, + db, + } + ) => { + assertSupportSecretOrThrow(supportSecret); + + const oldSwfAsset = await swfAssetLoader.load(layerId); + if (!oldSwfAsset) { + console.warn( + `Skipping setLayerKnownGlitches for unknown layer ID: ${layerId}` + ); + return null; + } + + const newKnownGlitchesString = knownGlitches.join(","); + + // Skip the update, and the logging, if there's no change. + if (oldSwfAsset.knownGlitches === newKnownGlitchesString) { + console.info( + `Skipping setLayerKnownGlitches for ${layerId}: no change. ` + + `(knownGlitches=${oldSwfAsset.knownGlitches})` + ); + return { id: layerId }; + } + + const [ + result, + ] = await db.execute( + `UPDATE swf_assets SET known_glitches = ? WHERE id = ? LIMIT 1`, + [newKnownGlitchesString, layerId] + ); + + if (result.affectedRows !== 1) { + throw new Error( + `Expected to affect 1 layer, but affected ${result.affectedRows}` + ); + } + + swfAssetLoader.clear(layerId); // we changed it, so clear it from cache + + if (process.env["SUPPORT_TOOLS_DISCORD_WEBHOOK_URL"]) { + try { + const itemId = await db + .execute( + `SELECT parent_id FROM parents_swf_assets + WHERE swf_asset_id = ? AND parent_type = "Item" LIMIT 1;`, + [layerId] + ) + .then(([rows]) => normalizeRow(rows[0]).parentId); + + const [item, itemTranslation, zoneTranslation] = await Promise.all([ + itemLoader.load(itemId), + itemTranslationLoader.load(itemId), + zoneTranslationLoader.load(oldSwfAsset.zoneId), + ]); + + await logToDiscord({ + embeds: [ + { + title: `🛠 ${itemTranslation.name}`, + thumbnail: { + url: item.thumbnailUrl, + height: 80, + width: 80, + }, + fields: [ + { + name: + `Layer ${layerId} (${zoneTranslation.label}): ` + + `Known glitches`, + value: `${oldSwfAsset.knownGlitches || ""} → **${ + newKnownGlitchesString || "" + }**`, + }, + ], + timestamp: new Date().toISOString(), + url: `https://impress.openneo.net/items/${itemId}`, + }, + ], + }); + } catch (e) { + console.error("Error sending Discord support log", e); + } + } else { + console.warn("No Discord support webhook provided, skipping"); + } + + return { id: layerId }; + }, + removeLayerFromItem: async ( _, { layerId, itemId, supportSecret },