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 },