mirror of
https://github.com/smogon/sprites.git
synced 2026-09-27 19:57:47 -05:00
Publish assets under their served prefix and name whole urls
The prefix a manifest value needs isn't knowable here: it lives in the
receiving home's services.toml, and the upload prepends it. So the two
guessed at each other and every reader carried a copy -- the forum's
SMOGON_XF_SPRITE_URL, the dex's hardcoded /__assets/sprites/.
Write the prefix on this side instead: everything served ships under
sprites/ in the tar, the upload rejects a tree that disagrees, and with
the tar path settled a manifest entry can be the whole url. Keys lose
their extension, since a url has nothing left to disambiguate. __meta/
stays at the tar root -- the upload diverts it out of the served tree.
The css pointer becomes spritesheet-css-url.txt, holding the url rather
than a suffix to paste onto one; the rename is deliberate, so a reader
of the old file fails instead of splicing a url into a prefix.
The minisprites webps go: 9d3e9abe shipped them speculatively in 2023
and nothing has read them or __meta/minisprites-hash.txt since, in any
repo. The dex draws minisprites from the spritesheet, and these are
named by PS id where it uses smogon aliases, so they never could have
lined up. newspritecopy() went with them, its only caller.
xy/ and xyicons/ keep the old shape: they ship by rsync and don't know
their url yet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
17
README.md
17
README.md
@@ -96,7 +96,7 @@ for eyeballing what would ship.
|
||||
assets: {
|
||||
buildFile: "assets.build.ts",
|
||||
deploy: [
|
||||
{subset: ["**"], cmd: "smogonctl assets upload sprites1"},
|
||||
{subset: ["**"], cmd: "smogonctl assets upload sprites"},
|
||||
],
|
||||
},
|
||||
ps: {
|
||||
@@ -108,6 +108,21 @@ for eyeballing what would ship.
|
||||
}
|
||||
```
|
||||
|
||||
### The asset upload's tar layout
|
||||
|
||||
`smogonctl assets upload` publishes a tar into a served tree under a prefix
|
||||
named in the receiving home's `services.toml`, which this side can't read. So
|
||||
`assets.build.ts` writes that prefix itself -- everything served ships under
|
||||
`sprites/` -- and the upload rejects a tar whose tree disagrees. The two are
|
||||
checked against each other instead of each guessing, which is what lets the
|
||||
manifests and pointers in `__meta/` name whole urls (`/__assets/sprites/...`)
|
||||
and their readers hold no configuration at all.
|
||||
|
||||
`__meta/` is the exception and stays at the tar root: the upload diverts it to
|
||||
`assets-meta/`, beside the served tree rather than in it, because a served
|
||||
name carries a content hash and something un-stamped has to say which name to
|
||||
ask for.
|
||||
|
||||
## Configuration
|
||||
|
||||
Build settings are configurable in `build.config` (not tracked by git).
|
||||
|
||||
@@ -1,28 +1,22 @@
|
||||
|
||||
import {gen6Padded, itemPadded} from './rules/minisprites.ts';
|
||||
import {Manifest, itemspritecopy, newspritecopy, spritecopy} from './rules/publish.ts';
|
||||
import {forEachRule, rule} from './tools/build/artifact.ts';
|
||||
import {Manifest, itemspritecopy, spritecopy} from './rules/publish.ts';
|
||||
import {rule} from './tools/build/artifact.ts';
|
||||
import {spriteglob} from './tools/build/helpers.ts';
|
||||
import {deploy} from './tools/deploy/api.ts';
|
||||
|
||||
// Smogdex minisprites (webp), shipped under a whole-set content hash with
|
||||
// a pointer in __meta/ for the dex to read.
|
||||
// The tar root maps onto the served tree: sprites/x is served at
|
||||
// /__assets/sprites/x. The upload rejects a tar whose tree disagrees with the
|
||||
// prefix in services.toml, so the two are checked against each other rather
|
||||
// than each guessing -- which is what lets the pointers below name whole urls
|
||||
// and their readers hold no configuration. __meta/ is the exception: the
|
||||
// upload diverts it to assets-meta/, beside the served tree and out of it.
|
||||
|
||||
let ASSETS = 'sprites';
|
||||
let SERVED = '/__assets';
|
||||
|
||||
let minispriteInputs = spriteglob(['src/minisprites/pokemon/gen6/*', 'src/minisprites/items/*'], {a: false});
|
||||
|
||||
let webpMinisprites = forEachRule(minispriteInputs, {
|
||||
display: 'webp minisprite %f',
|
||||
cmds: ['cwebp -z 9 %f -o %o'],
|
||||
}, '%B.webp');
|
||||
|
||||
deploy(async ctx => {
|
||||
let h = await ctx.hash(...webpMinisprites);
|
||||
for (let f of webpMinisprites) {
|
||||
newspritecopy(ctx, f, {dir: 'minisprites/' + h});
|
||||
}
|
||||
ctx.write('__meta/minisprites-hash.txt', h);
|
||||
});
|
||||
|
||||
// Smogdex spritesheet. The sheet tool bakes sprite ids parsed from the %f
|
||||
// filenames into the css, hence nameSensitive.
|
||||
|
||||
@@ -41,11 +35,10 @@ let [sheetPng, sheetCss] = rule(minispriteInputs, {
|
||||
|
||||
let sheetWebp = rule(sheetPng, ['cwebp -z 9 %f -o %o'], 'spritesheet.webp');
|
||||
|
||||
// Hash-stamped css + webp. The css suffix pointer rides in __meta/ for the
|
||||
// dex to read.
|
||||
// Hash-stamped css + webp. The css url rides in __meta/ for the dex to read.
|
||||
deploy(async ctx => {
|
||||
let wh = await ctx.hash(sheetWebp);
|
||||
ctx.copy(sheetWebp, `spritesheet-${wh}.webp`);
|
||||
ctx.copy(sheetWebp, `${ASSETS}/spritesheet-${wh}.webp`);
|
||||
let src = await ctx.read(sheetCss);
|
||||
let css = src.replaceAll('url("./spritesheet.webp")', `url("./spritesheet-${wh}.webp")`);
|
||||
if (css === src) {
|
||||
@@ -55,23 +48,23 @@ deploy(async ctx => {
|
||||
// of (css, webp), so this changes exactly when the served bytes
|
||||
// change.
|
||||
let ch = await ctx.hash(sheetCss, sheetWebp);
|
||||
ctx.write(`spritesheet-${ch}.css`, css);
|
||||
ctx.write('__meta/spritesheet_css_suffix.txt', `-${ch}\n`);
|
||||
ctx.write(`${ASSETS}/spritesheet-${ch}.css`, css);
|
||||
ctx.write('__meta/spritesheet-css-url.txt', `${SERVED}/${ASSETS}/spritesheet-${ch}.css\n`);
|
||||
});
|
||||
|
||||
// Forumsprites: padded minisprites under stamped names, with the
|
||||
// unhashed -> hashed mapping in a manifest.
|
||||
// unhashed -> url mapping in a manifest.
|
||||
|
||||
let forumItems = itemPadded();
|
||||
let forumG6 = gen6Padded();
|
||||
|
||||
deploy(async ctx => {
|
||||
let manifest = new Manifest(ctx);
|
||||
let manifest = new Manifest(ctx, SERVED);
|
||||
for (let f of forumItems) {
|
||||
await itemspritecopy(manifest, f, {dir: 'forumsprites'});
|
||||
await itemspritecopy(manifest, f, {dir: `${ASSETS}/forumsprites`});
|
||||
}
|
||||
for (let f of forumG6) {
|
||||
await spritecopy(manifest, f, {dir: 'forumsprites'}, true);
|
||||
await spritecopy(manifest, f, {dir: `${ASSETS}/forumsprites`}, true);
|
||||
}
|
||||
manifest.write('__meta/forumsprites/manifest.json');
|
||||
});
|
||||
@@ -79,9 +72,9 @@ deploy(async ctx => {
|
||||
// PMD sprites ship as-is, stamped.
|
||||
|
||||
deploy(async ctx => {
|
||||
let manifest = new Manifest(ctx);
|
||||
let manifest = new Manifest(ctx, SERVED);
|
||||
for (let f of await ctx.list('src/pmd')) {
|
||||
await spritecopy(manifest, f, {dir: 'pmd'});
|
||||
await spritecopy(manifest, f, {dir: `${ASSETS}/pmd`});
|
||||
}
|
||||
manifest.write('__meta/pmd/manifest.json');
|
||||
});
|
||||
|
||||
@@ -8,27 +8,38 @@ import type {DeployCtx, SrcFile} from '../tools/deploy/api.ts';
|
||||
// or a raw source file.
|
||||
export type Sprite = Artifact | SrcFile;
|
||||
|
||||
// The unhashed -> hashed name mapping published beside a stamped set.
|
||||
// The unhashed name -> served url mapping published beside a stamped set.
|
||||
// `base` is the url this deploy's tree is served at, so an entry is just that
|
||||
// plus the copy's destination and no consumer needs a prefix of its own. The
|
||||
// rsync sets don't know their url yet and pass none, keeping the older
|
||||
// `name.ext` -> stamped filename shape.
|
||||
export class Manifest {
|
||||
#ctx: DeployCtx;
|
||||
#base: string | null;
|
||||
#entries = new Map<string, string>();
|
||||
|
||||
constructor(ctx: DeployCtx) {
|
||||
constructor(ctx: DeployCtx, base: string | null = null) {
|
||||
this.#ctx = ctx;
|
||||
this.#base = base;
|
||||
}
|
||||
|
||||
// Queue a copy of `f` under `dir` with a content-stamped name and record
|
||||
// the unhashed -> hashed mapping.
|
||||
// the entry that points at it.
|
||||
async copy(f: Sprite, {dir, ext}: Dest, name: string): Promise<void> {
|
||||
let h = await this.#ctx.hash(f);
|
||||
let key = `${name}.${extOf(f, ext)}`;
|
||||
let e = extOf(f, ext);
|
||||
let stamped = `${name}-${h}.${e}`;
|
||||
let dst = `${dir}/${stamped}`;
|
||||
// A url names the whole path, so its key has nothing to disambiguate
|
||||
// with an extension.
|
||||
let key = this.#base === null ? `${name}.${e}` : name;
|
||||
// ActionQueue only dedups final dsts; hashed dsts differ even when
|
||||
// unhashed names collide, so check the key explicitly.
|
||||
if (this.#entries.has(key)) {
|
||||
throw new Error(`duplicate sprite name ${key}`);
|
||||
}
|
||||
this.#entries.set(key, `${name}-${h}.${extOf(f, ext)}`);
|
||||
this.#ctx.copy(f, `${dir}/${name}-${h}.${extOf(f, ext)}`);
|
||||
this.#entries.set(key, this.#base === null ? stamped : `${this.#base}/${dst}`);
|
||||
this.#ctx.copy(f, dst);
|
||||
}
|
||||
|
||||
write(dst: string): void {
|
||||
@@ -114,21 +125,3 @@ export async function itemspritecopy(manifest: Manifest, f: Sprite, dest: Dest):
|
||||
await manifest.copy(f, dest, toSmogonAlias(n));
|
||||
}
|
||||
}
|
||||
|
||||
export function newspritecopy(ctx: DeployCtx, f: Sprite, dest: Dest): void {
|
||||
let sn = spritedata.parseFilename(f.name);
|
||||
if (sn.extension) {
|
||||
return;
|
||||
}
|
||||
let sd = spritedata.get(sn.id);
|
||||
for (let 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';
|
||||
}
|
||||
ctx.copy(f, `${dest.dir}/${name}.${extOf(f, dest.ext)}`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user