From 41119f63a31ea3729b89c844c6aab5d8921f91b3 Mon Sep 17 00:00:00 2001 From: Christopher Monsanto Date: Sun, 16 Aug 2026 18:12:46 -0400 Subject: [PATCH] Split the hashed asset sets into assets.deploy.js spritesheet, minisprites, pmd, and forumsprites now ship as a tar to the __assets tree: __key leads the tar, and the mutable pointer files (manifests, css suffix, minisprites hash) ride in __meta/ instead of being published as immortal assets. xy and xyicons stay in smogon.deploy.js on the rsync path for now. Co-Authored-By: Claude Fable 5 --- assets.deploy.js | 149 +++++++++++++++++++++++++++++++++++++++++++++++ smogon.deploy.js | 65 --------------------- 2 files changed, 149 insertions(+), 65 deletions(-) create mode 100644 assets.deploy.js diff --git a/assets.deploy.js b/assets.deploy.js new file mode 100644 index 00000000..6abe5d40 --- /dev/null +++ b/assets.deploy.js @@ -0,0 +1,149 @@ + +// The upload contract wants __key first in the tar, naming the asset set; +// the tar packer emits ops in queue order, so it has to be the first op. +write("__key", "sprites"); + +function toSmogonAlias(name) { + return name.toLowerCase(). + replace(/[ _]+/, "-"). + replace(/[^a-z0-9-]+/g, ''); +} + +function toPSID(name) { + return name.toLowerCase().replace(/[^a-z0-9]+/g, ''); +} + +// Copy with a content-hash-stamped name and record the unhashed -> hashed +// mapping in `manifest`. +function stampcopy(f, {dir, ext, name}, manifest) { + const key = `${name}.${ext ?? f.ext}`; + // ActionQueue only dedups final dsts; hashed dsts differ even when + // unhashed names collide, so check the manifest key explicitly. + if (manifest[key] !== undefined) { + throw new Error(`duplicate sprite name ${key}`); + } + const h = hash(f); + manifest[key] = `${name}-${h}.${ext ?? f.ext}`; + copy(f, {dir, ext, name: `${name}-${h}`}); +} + +function writeManifest(dst, manifest) { + const sorted = {}; + for (const k of Object.keys(manifest).sort()) { + sorted[k] = manifest[k]; + } + write(dst, JSON.stringify(sorted, null, 4) + "\n"); +} + +function spritecopy(f, {dir, ext}, allowUnknown=false, manifest=null) { + const sn = spritedata.parseFilename(f.name); + let name; + + // Skip asymmetrical for now + if (sn.extra.has("a") || sn.extra.has("b") || sn.extra.has("s")) { + return; + } + + if (sn.extension) { + if (allowUnknown && sn.extension && sn.name === "Unknown") { + name = "unknown" + } else { + // Skip this, we don't use Unknown/Substitute + return; + } + } else { + const sd = spritedata.get(sn.id); + name = toSmogonAlias(sd.base); + if (sd.forme) { + name += `-${toSmogonAlias(sd.forme)}`; + } + } + if (sn.extra.has("f")) { + name += "-f"; + } + if (sn.extra.has("g")) { + name += "-gmax"; + } + + if (manifest) { + stampcopy(f, {dir, ext, name}, manifest); + } else { + copy(f, {dir, ext, name}); + } +} + +// TODO: merge with above +function itemspritecopy(f, {dir, ext}, manifest=null) { + const sn = spritedata.parseFilename(f.name); + const sd = spritedata.get(sn.id); + for (const n of sd.names) { + const name = toSmogonAlias(n); + if (manifest) { + stampcopy(f, {dir, ext, name}, manifest); + } else { + copy(f, {dir, ext, name}); + } + } +} + +function newspritecopy(f, {dir, ext}) { + const sn = spritedata.parseFilename(f.name); + if (sn.extension) { + return + } + const sd = spritedata.get(sn.id); + for (const n of sd.type === 'item' ? sd.names : [sd.base + sd.forme]) { + let name = toPSID(n); + if (sn.extra.has("f")) { + name += "f"; + } + if (sn.extra.has("g")) { + name += "gmax"; + } + copy(f, {dir, ext, name}); + } +} + +// Dex spritesheet assets: hash-stamped css + webp. The css suffix pointer +// rides in __meta/ for the dex to read. +{ + const wh = hash("build/smogon/spritesheet.webp"); + copy("build/smogon/spritesheet.webp", `spritesheet-${wh}.webp`); + const src = read("build/smogon/spritesheet.css"); + const css = src.replaceAll('url("./spritesheet.webp")', `url("./spritesheet-${wh}.webp")`); + if (css === src) { + throw new Error("spritesheet.css: no webp urls rewritten"); + } + // Suffix from source content: the rewritten css is a pure function of + // (css, webp), so this changes exactly when the served bytes change. + const ch = hash("build/smogon/spritesheet.css", "build/smogon/spritesheet.webp"); + write(`spritesheet-${ch}.css`, css); + write("__meta/spritesheet_css_suffix.txt", `-${ch}\n`); +} + +{ + const h = hash(...list("build/smogon/minisprites")); + for (const f of list("build/smogon/minisprites")) { + newspritecopy(f, {dir: "minisprites/" + h}); + } + write("__meta/minisprites-hash.txt", h); +} + +{ + const manifest = {}; + for (const f of list("build/item-minisprites-padded")) { + itemspritecopy(f, {dir: "forumsprites"}, manifest); + } + for (const f of list("build/gen6-minisprites-padded")) { + spritecopy(f, {dir: "forumsprites"}, true, manifest); + } + writeManifest("__meta/forumsprites/manifest.json", manifest); +} + +{ + const manifest = {}; + for (const f of list("src/pmd")) { + spritecopy(f, {dir: "pmd"}, false, manifest); + } + writeManifest("__meta/pmd/manifest.json", manifest); +} diff --git a/smogon.deploy.js b/smogon.deploy.js index 08860214..a7ddb453 100644 --- a/smogon.deploy.js +++ b/smogon.deploy.js @@ -5,10 +5,6 @@ function toSmogonAlias(name) { replace(/[^a-z0-9-]+/g, ''); } -function toPSID(name) { - return name.toLowerCase().replace(/[^a-z0-9]+/g, ''); -} - // Copy with a content-hash-stamped name and record the unhashed -> hashed // mapping in `manifest`. function stampcopy(f, {dir, ext, name}, manifest) { @@ -82,24 +78,6 @@ function itemspritecopy(f, {dir, ext}, manifest=null) { } } -function newspritecopy(f, {dir, ext}) { - const sn = spritedata.parseFilename(f.name); - if (sn.extension) { - return - } - const sd = spritedata.get(sn.id); - for (const n of sd.type === 'item' ? sd.names : [sd.base + sd.forme]) { - let name = toPSID(n); - if (sn.extra.has("f")) { - name += "f"; - } - if (sn.extra.has("g")) { - name += "gmax"; - } - copy(f, {dir, ext, name}); - } -} - let seenModels = new Set; const xyManifest = {}; @@ -147,40 +125,6 @@ for (const f of list("build/item-minisprites-trimmed")) { itemspritecopy(f, {dir: "xyitems"}); } -// Dex spritesheet assets: hash-stamped css + webp, plus the css suffix file -// shipped to /smog2/dex/spritesheet_css_suffix.txt (read by dex render). -{ - const wh = hash("build/smogon/spritesheet.webp"); - copy("build/smogon/spritesheet.webp", `assets/spritesheet-${wh}.webp`); - const src = read("build/smogon/spritesheet.css"); - const css = src.replaceAll('url("./spritesheet.webp")', `url("./spritesheet-${wh}.webp")`); - if (css === src) { - throw new Error("spritesheet.css: no webp urls rewritten"); - } - // Suffix from source content: the rewritten css is a pure function of - // (css, webp), so this changes exactly when the served bytes change. - const ch = hash("build/smogon/spritesheet.css", "build/smogon/spritesheet.webp"); - write(`assets/spritesheet-${ch}.css`, css); - write("spritesheet_css_suffix.txt", `-${ch}\n`); -} - -let h = hash(...list("build/smogon/minisprites")); -for (const f of list("build/smogon/minisprites")) { - newspritecopy(f, {dir: "minisprites/" + h}); -} -write("minisprites/hash.txt", h); - -{ - const manifest = {}; - for (const f of list("build/item-minisprites-padded")) { - itemspritecopy(f, {dir: "forumsprites"}, manifest); - } - for (const f of list("build/gen6-minisprites-padded")) { - spritecopy(f, {dir: "forumsprites"}, true, manifest); - } - writeManifest("forumsprites/manifest.json", manifest); -} - for (const f of list("build/smogon/fbsprites/xy")) { spritecopy(f, {dir: "fbsprites/xy"}); } @@ -188,12 +132,3 @@ for (const f of list("build/smogon/fbsprites/xy")) { for (const f of list("build/smogon/twittersprites/xy")) { spritecopy(f, {dir: "twittersprites/xy"}); } - -{ - const manifest = {}; - for (const f of list("src/pmd")) { - seenModels.add(f.name); - spritecopy(f, {dir: "pmd"}, false, manifest); - } - writeManifest("pmd/manifest.json", manifest); -}