hooray, animated items seem to be working?! ^w^

This commit is contained in:
Emi Matchu 2020-09-22 03:53:48 -07:00
parent a4d159381c
commit d27395bda2
5 changed files with 134 additions and 397 deletions

View file

@ -6,6 +6,7 @@ import { CSSTransition, TransitionGroup } from "react-transition-group";
import OutfitCanvas, { import OutfitCanvas, {
OutfitCanvasImage, OutfitCanvasImage,
OutfitCanvasMovie,
loadImage, loadImage,
useEaselDependenciesLoader, useEaselDependenciesLoader,
} from "./OutfitCanvas"; } from "./OutfitCanvas";
@ -152,13 +153,21 @@ export function OutfitLayers({
!loadingEasel && ( !loadingEasel && (
<FullScreenCenter> <FullScreenCenter>
<OutfitCanvas width={canvasSize} height={canvasSize}> <OutfitCanvas width={canvasSize} height={canvasSize}>
{visibleLayers.map((layer) => ( {visibleLayers.map((layer) =>
layer.canvasMovieLibraryUrl ? (
<OutfitCanvasMovie
key={layer.id}
librarySrc={layer.canvasMovieLibraryUrl}
zIndex={layer.zone.depth}
/>
) : (
<OutfitCanvasImage <OutfitCanvasImage
key={layer.id} key={layer.id}
src={getBestImageUrlForLayer(layer)} src={getBestImageUrlForLayer(layer)}
zIndex={layer.zone.depth} zIndex={layer.zone.depth}
/> />
))} )
)}
</OutfitCanvas> </OutfitCanvas>
</FullScreenCenter> </FullScreenCenter>
) )

View file

@ -144,6 +144,7 @@ export const itemAppearanceFragment = gql`
id id
remoteId # HACK: This is for Support tools, but other views don't need it remoteId # HACK: This is for Support tools, but other views don't need it
svgUrl svgUrl
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
bodyId bodyId
@ -166,6 +167,7 @@ export const petAppearanceFragment = gql`
layers { layers {
id id
svgUrl svgUrl
canvasMovieLibraryUrl
imageUrl(size: SIZE_600) imageUrl(size: SIZE_600)
zone { zone {
id id

View file

@ -65,7 +65,14 @@ describe("Item", () => {
const res = await query({ const res = await query({
query: gql` query: gql`
query { query {
items(ids: ["38912", "38911", "37375"]) { items(
ids: [
"38912" # Zafara Agent Robe
"38911" # Zafara Agent Hood
"37375" # Moon and Stars Background
"78244" # Bubbles on Water Foreground
]
) {
id id
name name
@ -75,6 +82,7 @@ describe("Item", () => {
remoteId remoteId
imageUrl(size: SIZE_600) imageUrl(size: SIZE_600)
svgUrl svgUrl
canvasMovieLibraryUrl
zone { zone {
id id
depth depth
@ -96,11 +104,12 @@ describe("Item", () => {
expect(getDbCalls()).toMatchInlineSnapshot(` expect(getDbCalls()).toMatchInlineSnapshot(`
Array [ Array [
Array [ Array [
"SELECT * FROM item_translations WHERE item_id IN (?,?,?) AND locale = \\"en\\"", "SELECT * FROM item_translations WHERE item_id IN (?,?,?,?) AND locale = \\"en\\"",
Array [ Array [
"38912", "38912",
"38911", "38911",
"37375", "37375",
"78244",
], ],
], ],
Array [ Array [
@ -115,7 +124,7 @@ describe("Item", () => {
INNER JOIN parents_swf_assets rel ON INNER JOIN parents_swf_assets rel ON
rel.parent_type = \\"Item\\" AND rel.parent_type = \\"Item\\" AND
rel.swf_asset_id = sa.id rel.swf_asset_id = sa.id
WHERE (rel.parent_id = ? AND (sa.body_id = ? OR sa.body_id = 0)) OR (rel.parent_id = ? AND (sa.body_id = ? OR sa.body_id = 0)) OR (rel.parent_id = ? AND (sa.body_id = ? OR sa.body_id = 0))", WHERE (rel.parent_id = ? AND (sa.body_id = ? OR sa.body_id = 0)) OR (rel.parent_id = ? AND (sa.body_id = ? OR sa.body_id = 0)) OR (rel.parent_id = ? AND (sa.body_id = ? OR sa.body_id = 0)) OR (rel.parent_id = ? AND (sa.body_id = ? OR sa.body_id = 0))",
Array [ Array [
"38912", "38912",
"180", "180",
@ -123,30 +132,35 @@ describe("Item", () => {
"180", "180",
"37375", "37375",
"180", "180",
"78244",
"180",
], ],
], ],
Array [ Array [
"SELECT * FROM items WHERE id IN (?,?,?)", "SELECT * FROM items WHERE id IN (?,?,?,?)",
Array [ Array [
"38912", "38912",
"38911", "38911",
"37375", "37375",
"78244",
], ],
], ],
Array [ Array [
"SELECT * FROM zones WHERE id IN (?,?,?)", "SELECT * FROM zones WHERE id IN (?,?,?,?)",
Array [ Array [
"26", "26",
"40", "40",
"3", "3",
"45",
], ],
], ],
Array [ Array [
"SELECT * FROM zone_translations WHERE zone_id IN (?,?,?) AND locale = \\"en\\"", "SELECT * FROM zone_translations WHERE zone_id IN (?,?,?,?) AND locale = \\"en\\"",
Array [ Array [
"26", "26",
"40", "40",
"3", "3",
"45",
], ],
], ],
] ]

View file

@ -7,8 +7,9 @@ Object {
"appearanceOn": Object { "appearanceOn": Object {
"layers": Array [ "layers": Array [
Object { Object {
"canvasMovieLibraryUrl": null,
"id": "37128", "id": "37128",
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/object/000/000/014/14856/600x600.png?v2-1587653266000", "imageUrl": "https://impress-asset-images.s3.amazonaws.com/object/000/000/014/14856/600x600.png?v2-1577836800000",
"remoteId": "14856", "remoteId": "14856",
"svgUrl": null, "svgUrl": null,
"zone": Object { "zone": Object {
@ -34,8 +35,9 @@ Object {
"appearanceOn": Object { "appearanceOn": Object {
"layers": Array [ "layers": Array [
Object { Object {
"canvasMovieLibraryUrl": null,
"id": "37129", "id": "37129",
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/object/000/000/014/14857/600x600.png?v2-0", "imageUrl": "https://impress-asset-images.s3.amazonaws.com/object/000/000/014/14857/600x600.png?v2-1577836800000",
"remoteId": "14857", "remoteId": "14857",
"svgUrl": null, "svgUrl": null,
"zone": Object { "zone": Object {
@ -61,8 +63,9 @@ Object {
"appearanceOn": Object { "appearanceOn": Object {
"layers": Array [ "layers": Array [
Object { Object {
"canvasMovieLibraryUrl": null,
"id": "30203", "id": "30203",
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/object/000/000/006/6829/600x600.png?v2-1598519675000", "imageUrl": "https://impress-asset-images.s3.amazonaws.com/object/000/000/006/6829/600x600.png?v2-1577836800000",
"remoteId": "6829", "remoteId": "6829",
"svgUrl": null, "svgUrl": null,
"zone": Object { "zone": Object {
@ -77,366 +80,28 @@ Object {
"id": "37375", "id": "37375",
"name": "Moon and Stars Background", "name": "Moon and Stars Background",
}, },
],
}
`;
exports[`Item loads canonical appearance for all-species Maraquan item: db 1`] = `
Array [
Array [
"SELECT pet_types.body_id, pet_types.species_id, items.id AS item_id
FROM items
INNER JOIN parents_swf_assets ON
items.id = parents_swf_assets.parent_id AND
parents_swf_assets.parent_type = \\"Item\\"
INNER JOIN swf_assets ON
parents_swf_assets.swf_asset_id = swf_assets.id
INNER JOIN pet_types ON
pet_types.body_id = swf_assets.body_id OR swf_assets.body_id = 0
INNER JOIN colors ON
pet_types.color_id = colors.id
WHERE items.id IN (?)
GROUP BY pet_types.body_id
ORDER BY
pet_types.species_id,
colors.standard DESC",
Array [
"77530",
],
],
Array [
"SELECT sa.*, rel.parent_id FROM swf_assets sa
INNER JOIN parents_swf_assets rel ON
rel.parent_type = \\"Item\\" AND
rel.swf_asset_id = sa.id
WHERE (rel.parent_id = ? AND (sa.body_id = ? OR sa.body_id = 0))",
Array [
"77530",
"112",
],
],
Array [
Object { Object {
"nestTables": true, "appearanceOn": Object {
"sql": " "layers": Array [
SELECT pet_states.*, pet_types.* FROM pet_states
INNER JOIN pet_types ON pet_types.id = pet_states.pet_type_id
WHERE pet_types.body_id = ?
ORDER BY
pet_types.color_id = 8 DESC, -- Prefer Blue
pet_states.mood_id = 1 DESC, -- Prefer Happy
pet_states.female = ? DESC, -- Prefer given gender
pet_states.id DESC, -- Prefer recent models (like in the app)
pet_states.glitched ASC -- Prefer not glitched (like in the app)
LIMIT 1",
"values": Array [
"112",
false,
],
},
Array [
"112",
false,
],
],
Array [
"SELECT sa.*, rel.parent_id FROM swf_assets sa
INNER JOIN parents_swf_assets rel ON
rel.parent_type = \\"PetState\\" AND
rel.swf_asset_id = sa.id
WHERE rel.parent_id IN (?)",
Array [
"5233",
],
],
Array [
"SELECT * FROM color_translations
WHERE color_id IN (?) AND locale = \\"en\\"",
Array [
"44",
],
],
Array [
"SELECT * FROM species_translations
WHERE species_id IN (?) AND locale = \\"en\\"",
Array [
"1",
],
],
]
`;
exports[`Item loads canonical appearance for all-species Maraquan item: item layers 1`] = `
Array [
Object { Object {
"id": "442864", "canvasMovieLibraryUrl": "http://images.neopets.com/cp/items/data/000/000/564/564507_fc3216b9b8/all-item_foreground_lower.js",
"id": "468155",
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/object/000/000/564/564507/600x600.png?v2-1577836800000",
"remoteId": "564507",
"svgUrl": null,
"zone": Object {
"depth": 50,
"id": "45",
"label": "Lower Foreground Item",
}, },
]
`;
exports[`Item loads canonical appearance for all-species Maraquan item: pet layers 1`] = `
Array [
Object {
"id": "2652",
}, },
Object { ],
"id": "2653", "restrictedZones": Array [],
}, },
Object { "id": "78244",
"id": "2654", "name": "Bubbles on Water Foreground",
}, },
Object {
"id": "2656",
},
Object {
"id": "2663",
},
]
`;
exports[`Item loads canonical appearance for all-species item: db 1`] = `
Array [
Array [
"SELECT pet_types.body_id, pet_types.species_id, items.id AS item_id
FROM items
INNER JOIN parents_swf_assets ON
items.id = parents_swf_assets.parent_id AND
parents_swf_assets.parent_type = \\"Item\\"
INNER JOIN swf_assets ON
parents_swf_assets.swf_asset_id = swf_assets.id
INNER JOIN pet_types ON
pet_types.body_id = swf_assets.body_id OR swf_assets.body_id = 0
INNER JOIN colors ON
pet_types.color_id = colors.id
WHERE items.id IN (?)
GROUP BY pet_types.body_id
ORDER BY
pet_types.species_id,
colors.standard DESC",
Array [
"74967",
], ],
],
Array [
"SELECT sa.*, rel.parent_id FROM swf_assets sa
INNER JOIN parents_swf_assets rel ON
rel.parent_type = \\"Item\\" AND
rel.swf_asset_id = sa.id
WHERE (rel.parent_id = ? AND (sa.body_id = ? OR sa.body_id = 0))",
Array [
"74967",
"93",
],
],
Array [
"SELECT * FROM species_translations
WHERE species_id IN (?) AND locale = \\"en\\"",
Array [
"1",
],
],
Array [
Object {
"nestTables": true,
"sql": "
SELECT pet_states.*, pet_types.* FROM pet_states
INNER JOIN pet_types ON pet_types.id = pet_states.pet_type_id
WHERE pet_types.body_id = ?
ORDER BY
pet_types.color_id = 8 DESC, -- Prefer Blue
pet_states.mood_id = 1 DESC, -- Prefer Happy
pet_states.female = ? DESC, -- Prefer given gender
pet_states.id DESC, -- Prefer recent models (like in the app)
pet_states.glitched ASC -- Prefer not glitched (like in the app)
LIMIT 1",
"values": Array [
"93",
true,
],
},
Array [
"93",
true,
],
],
]
`;
exports[`Item loads canonical appearance for all-species item: item layers 1`] = `
Array [
Object {
"id": "395679",
},
]
`;
exports[`Item loads canonical appearance for all-species item: pet layers 1`] = `
Object {
"id": "5161",
}
`;
exports[`Item loads canonical appearance for bodyId=0 item: db 1`] = `
Array [
Array [
"SELECT pet_types.body_id, pet_types.species_id, items.id AS item_id
FROM items
INNER JOIN parents_swf_assets ON
items.id = parents_swf_assets.parent_id AND
parents_swf_assets.parent_type = \\"Item\\"
INNER JOIN swf_assets ON
parents_swf_assets.swf_asset_id = swf_assets.id
INNER JOIN pet_types ON
pet_types.body_id = swf_assets.body_id OR swf_assets.body_id = 0
INNER JOIN colors ON
pet_types.color_id = colors.id
WHERE items.id IN (?)
GROUP BY pet_types.body_id
ORDER BY
pet_types.species_id,
colors.standard DESC",
Array [
"37375",
],
],
Array [
"SELECT sa.*, rel.parent_id FROM swf_assets sa
INNER JOIN parents_swf_assets rel ON
rel.parent_type = \\"Item\\" AND
rel.swf_asset_id = sa.id
WHERE (rel.parent_id = ? AND (sa.body_id = ? OR sa.body_id = 0))",
Array [
"37375",
"93",
],
],
Array [
"SELECT * FROM species_translations
WHERE species_id IN (?) AND locale = \\"en\\"",
Array [
"1",
],
],
Array [
Object {
"nestTables": true,
"sql": "
SELECT pet_states.*, pet_types.* FROM pet_states
INNER JOIN pet_types ON pet_types.id = pet_states.pet_type_id
WHERE pet_types.body_id = ?
ORDER BY
pet_types.color_id = 8 DESC, -- Prefer Blue
pet_states.mood_id = 1 DESC, -- Prefer Happy
pet_states.female = ? DESC, -- Prefer given gender
pet_states.id DESC, -- Prefer recent models (like in the app)
pet_states.glitched ASC -- Prefer not glitched (like in the app)
LIMIT 1",
"values": Array [
"93",
true,
],
},
Array [
"93",
true,
],
],
]
`;
exports[`Item loads canonical appearance for bodyId=0 item: item layers 1`] = `
Array [
Object {
"id": "30203",
},
]
`;
exports[`Item loads canonical appearance for bodyId=0 item: pet layers 1`] = `
Object {
"id": "5161",
}
`;
exports[`Item loads canonical appearance for single-species item: db 1`] = `
Array [
Array [
"SELECT pet_types.body_id, pet_types.species_id, items.id AS item_id
FROM items
INNER JOIN parents_swf_assets ON
items.id = parents_swf_assets.parent_id AND
parents_swf_assets.parent_type = \\"Item\\"
INNER JOIN swf_assets ON
parents_swf_assets.swf_asset_id = swf_assets.id
INNER JOIN pet_types ON
pet_types.body_id = swf_assets.body_id OR swf_assets.body_id = 0
INNER JOIN colors ON
pet_types.color_id = colors.id
WHERE items.id IN (?)
GROUP BY pet_types.body_id
ORDER BY
pet_types.species_id,
colors.standard DESC",
Array [
"38911",
],
],
Array [
"SELECT sa.*, rel.parent_id FROM swf_assets sa
INNER JOIN parents_swf_assets rel ON
rel.parent_type = \\"Item\\" AND
rel.swf_asset_id = sa.id
WHERE (rel.parent_id = ? AND (sa.body_id = ? OR sa.body_id = 0))",
Array [
"38911",
"180",
],
],
Array [
"SELECT * FROM species_translations
WHERE species_id IN (?) AND locale = \\"en\\"",
Array [
"54",
],
],
Array [
Object {
"nestTables": true,
"sql": "
SELECT pet_states.*, pet_types.* FROM pet_states
INNER JOIN pet_types ON pet_types.id = pet_states.pet_type_id
WHERE pet_types.body_id = ?
ORDER BY
pet_types.color_id = 8 DESC, -- Prefer Blue
pet_states.mood_id = 1 DESC, -- Prefer Happy
pet_states.female = ? DESC, -- Prefer given gender
pet_states.id DESC, -- Prefer recent models (like in the app)
pet_states.glitched ASC -- Prefer not glitched (like in the app)
LIMIT 1",
"values": Array [
"180",
false,
],
},
Array [
"180",
false,
],
],
]
`;
exports[`Item loads canonical appearance for single-species item: item layers 1`] = `
Array [
Object {
"id": "37129",
},
]
`;
exports[`Item loads canonical appearance for single-species item: pet layers 1`] = `
Object {
"id": "17861",
} }
`; `;

View file

@ -38,6 +38,14 @@ const typeDefs = gql`
""" """
swfUrl: String swfUrl: String
"""
This layer as an HTML canvas library JS file, if available.
This will be empty for layers that don't animate, and might also be empty
for animated layers not yet converted by Neopets.
"""
canvasMovieLibraryUrl: String
""" """
This layer can fit on PetAppearances with the same bodyId. "0" is a This layer can fit on PetAppearances with the same bodyId. "0" is a
special body ID that indicates it fits all PetAppearances. special body ID that indicates it fits all PetAppearances.
@ -111,30 +119,7 @@ const resolvers = {
// 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
// it exists yet. Load it to find out! // it exists yet. Load it to find out!
if (manifest === null) { if (manifest === null) {
manifest = await loadAssetManifest(layer.url); manifest = await loadAndCacheAssetManifest(db, layer);
// Then, write the new manifest. We make sure to write an empty string
// if there was no manifest, to signify that it doesn't exist, so we
// don't need to bother looking it up again.
//
// TODO: Someday the manifests will all exist, right? So we'll want to
// reload all the missing ones at that time.
manifest = manifest || "";
const [
result,
] = await db.execute(
`UPDATE swf_assets SET manifest = ? WHERE id = ? LIMIT 1;`,
[manifest, layer.id]
);
if (result.affectedRows !== 1) {
throw new Error(
`Expected to affect 1 asset, but affected ${result.affectedRows}`
);
}
console.log(
`Loaded and saved manifest for ${layer.type} ${layer.remoteId}. ` +
`DTI ID: ${layer.id}. Exists?: ${Boolean(manifest)}`
);
} }
if (!manifest) { if (!manifest) {
@ -163,6 +148,39 @@ const resolvers = {
const url = new URL(assetDatum.path, "http://images.neopets.com"); const url = new URL(assetDatum.path, "http://images.neopets.com");
return url.toString(); return url.toString();
}, },
canvasMovieLibraryUrl: async ({ id }, _, { db, swfAssetLoader }) => {
const layer = await swfAssetLoader.load(id);
let manifest = layer.manifest && JSON.parse(layer.manifest);
// When the manifest is specifically null, that means we don't know if
// it exists yet. Load it to find out!
if (manifest === null) {
manifest = await loadAndCacheAssetManifest(db, layer);
}
if (!manifest) {
return null;
}
if (manifest.assets.length !== 1) {
return null;
}
const asset = manifest.assets[0];
if (asset.format !== "lod") {
return null;
}
const jsAssetDatum = asset.assetData.find((ad) =>
ad.path.endsWith(".js")
);
if (!jsAssetDatum) {
return null;
}
const url = new URL(jsAssetDatum.path, "http://images.neopets.com");
return url.toString();
},
item: async ({ id }, _, { db }) => { item: async ({ id }, _, { db }) => {
// TODO: If this becomes a popular request, we'll definitely need to // TODO: If this becomes a popular request, we'll definitely need to
// loaderize this! I'm cheating for now because it's just Support, one at // loaderize this! I'm cheating for now because it's just Support, one at
@ -207,6 +225,35 @@ async function loadAssetManifest(swfUrl) {
}; };
} }
async function loadAndCacheAssetManifest(db, layer) {
let manifest = await loadAssetManifest(layer.url);
// Then, write the new manifest. We make sure to write an empty string
// if there was no manifest, to signify that it doesn't exist, so we
// don't need to bother looking it up again.
//
// TODO: Someday the manifests will all exist, right? So we'll want to
// reload all the missing ones at that time.
manifest = manifest || "";
const [
result,
] = await db.execute(
`UPDATE swf_assets SET manifest = ? WHERE id = ? LIMIT 1;`,
[manifest, layer.id]
);
if (result.affectedRows !== 1) {
throw new Error(
`Expected to affect 1 asset, but affected ${result.affectedRows}`
);
}
console.log(
`Loaded and saved manifest for ${layer.type} ${layer.remoteId}. ` +
`DTI ID: ${layer.id}. Exists?: ${Boolean(manifest)}`
);
return manifest;
}
const SWF_URL_PATTERN = /^http:\/\/images\.neopets\.com\/cp\/(.+?)\/swf\/(.+?)\.swf$/; const SWF_URL_PATTERN = /^http:\/\/images\.neopets\.com\/cp\/(.+?)\/swf\/(.+?)\.swf$/;
function convertSwfUrlToManifestUrl(swfUrl) { function convertSwfUrlToManifestUrl(swfUrl) {