diff --git a/README.md b/README.md index acdae0b2..eb04fe96 100644 --- a/README.md +++ b/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). diff --git a/assets.build.ts b/assets.build.ts index cbaca497..4139e3f3 100644 --- a/assets.build.ts +++ b/assets.build.ts @@ -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'); }); diff --git a/rules/publish.ts b/rules/publish.ts index ef4d5f4e..409c03c8 100644 --- a/rules/publish.ts +++ b/rules/publish.ts @@ -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(); - 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 { 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)}`); - } -}