@bscotch/sprite-source
Version:
Art pipeline scripting module for GameMaker sprites.
139 lines • 5.45 kB
JavaScript
import { pathy } from '@bscotch/pathy';
import { computePngChecksums } from '@bscotch/pixel-checksum';
import { cacheVersion, spritesInfoSchema, } from './SpriteCache.schemas.js';
import { SpriteDir } from './SpriteDir.js';
import { computeStringChecksum } from './checksum.js';
import { retryOptions, spriteCacheFilename } from './constants.js';
import { SpriteSourceError, getDirs } from './utility.js';
export class SpriteCache {
maxDepth;
issues = [];
logs = [];
spritesRoot;
/**
* @param spritesRoot The path to the root directory containing
* sprites. For a SpriteSource, this is the root of a set
* of nested folders-of-images. For a SpriteDest (a project),
* this is the `{project}/sprites` folder.
* @param maxDepth The maximum depth to search for sprites. For a SpriteSource this is probably Infinity. For a SpriteDest, this should be 1.
*/
constructor(spritesRoot, maxDepth = Infinity) {
this.maxDepth = maxDepth;
this.spritesRoot = pathy(spritesRoot);
}
get stitchDir() {
return this.spritesRoot.join('.stitch');
}
get cacheFile() {
return this.stitchDir
.join(spriteCacheFilename)
.withValidator(spritesInfoSchema);
}
async getSpriteDirs(dirs) {
const waits = [];
const spriteDirs = [];
for (const dir of dirs) {
waits.push(SpriteDir.from(dir, this.logs, this.issues)
.then((sprite) => {
if (sprite) {
spriteDirs.push(sprite);
}
})
.catch((err) => {
this.issues.push(new SpriteSourceError(`Error processing "${dir.relative}"`, err));
}));
}
await Promise.all(waits);
return spriteDirs;
}
async loadCache() {
let cache;
try {
cache = await this.cacheFile.read({
fallback: {},
...retryOptions,
});
if (cache.version !== cacheVersion) {
cache = {
version: cacheVersion,
info: {},
};
this.issues.push(new SpriteSourceError(`Sprite cache version is out of date. Will rebuild.`));
}
}
catch (err) {
cache = {
version: cacheVersion,
info: {},
};
this.issues.push(new SpriteSourceError(`Could not load sprite cache. Will rebuild.`, err));
}
return cache;
}
/**
* Update the sprite-info cache.
*/
async updateSpriteInfo(ignore) {
const ignorePatterns = ignore?.map((pattern) => new RegExp(pattern));
// Load the current cache and sprite dirs
const [cache, allSpriteDirs] = await Promise.all([
this.loadCache(),
getDirs(this.spritesRoot.absolute, this.maxDepth).then((dirs) => this.getSpriteDirs(dirs)),
]);
// Filter out ignored spriteDirs
const spriteDirs = !ignore?.length
? allSpriteDirs
: allSpriteDirs.filter((dir) => !ignorePatterns?.some((pattern) => dir.path.relative.match(pattern)));
// For each sprite, update the cache with its size, frames (checksums, changedAt, etc)
const waits = [];
for (const sprite of spriteDirs) {
waits.push(sprite.updateCache(cache));
}
await Promise.all(waits);
// Add any missing checksums
const checksumsToCompute = [];
for (const [sprite, info] of Object.entries(cache.info)) {
if (info.spine)
continue;
for (const [frame, frameInfo] of Object.entries(info.frames)) {
if (frameInfo.checksum)
continue;
checksumsToCompute.push([
sprite,
frame,
this.spritesRoot.join(sprite, frame).absolute,
]);
}
}
const checksums = computePngChecksums(checksumsToCompute.map(([_s, _f, framePath]) => framePath));
checksumsToCompute.forEach(([sprite, frame], idx) => {
const spriteCache = cache.info[sprite];
if (spriteCache.spine)
return; // Shouldn't happen
spriteCache.frames[frame].checksum = checksums[idx];
spriteCache.checksum = ''; // Will be recomputed below
});
// Ensure cumulative checksums for all non-spine sprites
for (const [, spriteCache] of Object.entries(cache.info)) {
if (spriteCache.spine || spriteCache.checksum)
continue;
const frameChecksums = Object.values(spriteCache.frames)
.map((f) => f.checksum)
.sort();
spriteCache.checksum = computeStringChecksum(frameChecksums.join('-'));
}
// Remove any sprite info that no longer exists
const existingSpriteDirs = new Set(spriteDirs.map((dir) => dir.path.relative));
for (const spriteDir of Object.keys(cache.info)) {
if (!existingSpriteDirs.has(spriteDir)) {
delete cache.info[spriteDir];
}
}
// Save and return the updated cache
await this.cacheFile.write(cache, {
...retryOptions,
});
return cache;
}
}
//# sourceMappingURL=SpriteCache.js.map