Add Support tool for OFFICIAL_SVG_IS_INCORRECT

Inspired by the "Flying in an Airplane" bug (item 82287), where the official SVG (and I think SWF) were visually glitched and included both zones in the image, but the official PNG was correct.

This flag lets us use the PNG, like the official player does—but only for this item, while still keeping SVGs for everyone else!
This commit is contained in:
Emi Matchu 2021-03-12 04:01:35 -08:00
parent 15d4a27657
commit 0aaf1adb29
4 changed files with 205 additions and 9 deletions

View file

@ -21,6 +21,9 @@ import {
Spinner, Spinner,
useDisclosure, useDisclosure,
useToast, useToast,
CheckboxGroup,
VStack,
Checkbox,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { ChevronRightIcon, ExternalLinkIcon } from "@chakra-ui/icons"; import { ChevronRightIcon, ExternalLinkIcon } from "@chakra-ui/icons";
@ -45,6 +48,10 @@ function ItemLayerSupportModal({
onClose, onClose,
}) { }) {
const [selectedBodyId, setSelectedBodyId] = React.useState(itemLayer.bodyId); const [selectedBodyId, setSelectedBodyId] = React.useState(itemLayer.bodyId);
const [selectedKnownGlitches, setSelectedKnownGlitches] = React.useState(
itemLayer.knownGlitches
);
const [previewBiology, setPreviewBiology] = React.useState({ const [previewBiology, setPreviewBiology] = React.useState({
speciesId: outfitState.speciesId, speciesId: outfitState.speciesId,
colorId: outfitState.colorId, colorId: outfitState.colorId,
@ -63,6 +70,7 @@ function ItemLayerSupportModal({
mutation ItemSupportSetLayerBodyId( mutation ItemSupportSetLayerBodyId(
$layerId: ID! $layerId: ID!
$bodyId: ID! $bodyId: ID!
$knownGlitches: [AppearanceLayerKnownGlitch!]!
$supportSecret: String! $supportSecret: String!
$outfitSpeciesId: ID! $outfitSpeciesId: ID!
$outfitColorId: 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} ${itemAppearanceFragment}
`, `,
@ -105,6 +123,7 @@ function ItemLayerSupportModal({
variables: { variables: {
layerId: itemLayer.id, layerId: itemLayer.id,
bodyId: selectedBodyId, bodyId: selectedBodyId,
knownGlitches: selectedKnownGlitches,
supportSecret, supportSecret,
outfitSpeciesId: outfitState.speciesId, outfitSpeciesId: outfitState.speciesId,
outfitColorId: outfitState.colorId, outfitColorId: outfitState.colorId,
@ -251,6 +270,11 @@ function ItemLayerSupportModal({
onChangeBodyId={setSelectedBodyId} onChangeBodyId={setSelectedBodyId}
onChangePreviewBiology={setPreviewBiology} onChangePreviewBiology={setPreviewBiology}
/> />
<Box height="8" />
<ItemLayerSupportKnownGlitchesFields
selectedKnownGlitches={selectedKnownGlitches}
onChange={setSelectedKnownGlitches}
/>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<ItemLayerSupportModalRemoveButton <ItemLayerSupportModalRemoveButton
@ -325,7 +349,7 @@ function ItemLayerSupportPetCompatibilityFields({
return ( return (
<FormControl isInvalid={error || !selectedBiology.isValid ? true : false}> <FormControl isInvalid={error || !selectedBiology.isValid ? true : false}>
<FormLabel>Pet compatibility</FormLabel> <FormLabel fontWeight="bold">Pet compatibility</FormLabel>
<RadioGroup <RadioGroup
colorScheme="green" colorScheme="green"
value={selectedBodyId} value={selectedBodyId}
@ -397,6 +421,27 @@ function ItemLayerSupportPetCompatibilityFields({
); );
} }
function ItemLayerSupportKnownGlitchesFields({
selectedKnownGlitches,
onChange,
}) {
return (
<FormControl>
<FormLabel fontWeight="bold">Known glitches</FormLabel>
<CheckboxGroup value={selectedKnownGlitches} onChange={onChange}>
<VStack spacing="2" align="flex-start">
<Checkbox value="OFFICIAL_SVG_IS_INCORRECT">
Official SVG is incorrect{" "}
<Box display="inline" color="gray.400" fontSize="sm">
(Will use the PNG instead)
</Box>
</Checkbox>
</VStack>
</CheckboxGroup>
</FormControl>
);
}
function ItemLayerSupportModalRemoveButton({ function ItemLayerSupportModalRemoveButton({
item, item,
itemLayer, itemLayer,

View file

@ -169,6 +169,7 @@ export const itemAppearanceFragment = gql`
canvasMovieLibraryUrl canvasMovieLibraryUrl
imageUrl(size: SIZE_600) imageUrl(size: SIZE_600)
swfUrl # HACK: This is for Support tools, but other views don't need it 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 bodyId
zone { zone {
label @client # HACK: This is for Support tools, but other views don't need it label @client # HACK: This is for Support tools, but other views don't need it

View file

@ -64,6 +64,13 @@ const typeDefs = gql`
""" """
item: Item 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, 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 this is generally empty and the restriction is on the ItemAppearance, not
@ -75,6 +82,15 @@ const typeDefs = gql`
restrictedZones: [Zone!]! 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 { extend type Query {
# Return the number of layers that have been converted to HTML5, optionally # 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 # filtered by type. Cache for 30 minutes (we re-sync with Neopets every
@ -138,6 +154,13 @@ const resolvers = {
}, },
svgUrl: async ({ id }, _, { db, swfAssetLoader }) => { svgUrl: async ({ id }, _, { db, swfAssetLoader }) => {
const layer = await swfAssetLoader.load(id); 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); let manifest = layer.manifest && JSON.parse(layer.manifest);
// When the manifest is specifically null, that means we don't know if // 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) }; 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: { Query: {

View file

@ -1,13 +1,6 @@
import { gql } from "apollo-server"; import { gql } from "apollo-server";
import { ManagementClient } from "auth0"; 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 { import {
capitalize, capitalize,
getPoseFromPetState, getPoseFromPetState,
@ -18,6 +11,13 @@ import {
normalizeRow, normalizeRow,
} from "../util"; } 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` const typeDefs = gql`
type RemoveLayerFromItemMutationResult { type RemoveLayerFromItemMutationResult {
layer: AppearanceLayer! layer: AppearanceLayer!
@ -47,7 +47,13 @@ const typeDefs = gql`
layerId: ID! layerId: ID!
bodyId: ID! bodyId: ID!
supportSecret: String! supportSecret: String!
): AppearanceLayer! ): AppearanceLayer
setLayerKnownGlitches(
layerId: ID!
knownGlitches: [AppearanceLayerKnownGlitch!]!
supportSecret: String!
): AppearanceLayer
removeLayerFromItem( removeLayerFromItem(
layerId: ID! layerId: ID!
@ -289,6 +295,21 @@ const resolvers = {
assertSupportSecretOrThrow(supportSecret); assertSupportSecretOrThrow(supportSecret);
const oldSwfAsset = await swfAssetLoader.load(layerId); 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 [ const [
result, result,
@ -361,6 +382,103 @@ const resolvers = {
return { id: layerId }; 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 || "<none>"} → **${
newKnownGlitchesString || "<none>"
}**`,
},
],
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 ( removeLayerFromItem: async (
_, _,
{ layerId, itemId, supportSecret }, { layerId, itemId, supportSecret },