UNPKG

@bscotch/sprite-source

Version:

Art pipeline scripting module for GameMaker sprites.

163 lines 7.18 kB
import { pathy } from '@bscotch/pathy'; import { Yy, yySpriteSchema, } from '@bscotch/yy'; import path from 'path'; import { FIO_RETRY_DELAY, MAX_FIO_RETRIES } from './constants.js'; import { readdirSafeWithFileTypes } from './safeFs.js'; import { getPngSize } from './utility.js'; export async function applySpriteAction({ projectYypPath, action, yyp, }) { let trace = []; try { // Ensure the target path exists const targetFolder = pathy(action.dest); // If this is a `create` action, delete the existing sprite // (no effect when there literally is no existing sprite, but // there could be leftover files, or a sprite with the same name // but different type, etc.) if (action.kind === 'create') { trace.push(`Recursively deleting ${targetFolder}`); await targetFolder.delete({ recursive: true, force: true, maxRetries: MAX_FIO_RETRIES, retryDelay: FIO_RETRY_DELAY, }); } trace.push(`Ensuring ${targetFolder}`); await targetFolder.ensureDirectory(); // Get the list of children in the source and destination trace.push(`Reading ${targetFolder.absolute} and ${action.source}`); const [initialDestFileNames, sourceFileNames] = await Promise.all([ readdirSafeWithFileTypes(targetFolder), readdirSafeWithFileTypes(action.source), ]); const initialDestFiles = initialDestFileNames .filter((f) => f.isFile()) .map((f) => pathy(f.name, targetFolder)); const sourceFiles = sourceFileNames .filter((f) => f.isFile()) .map((f) => pathy(f.name, action.source)); const sourcePngs = sourceFiles.filter((f) => f.basename.match(/\.png$/i)); // Get origin info etc trace.push(`Getting origin info`); const size = await getPngSize(sourcePngs[0]); const width = size.width; const height = size.height; const xorigin = Math.floor(width / 2) - 1; const yorigin = Math.floor(height / 2) - 1; // Load the yy file, or populate a default one const yyFile = initialDestFiles.find((f) => f.basename.toLowerCase() === `${action.name}.yy`.toLowerCase()) || pathy(`${action.name}.yy`, targetFolder); if (!(await yyFile.exists())) { trace.push(`Creating ${yyFile.absolute}`); await Yy.write(yyFile.absolute, { name: action.name, type: action.spine ? 2 : 0, width, height, sequence: { xorigin, yorigin }, }, 'sprites', yyp); } trace.push(`Reading yy file ${yyFile}`); let yy = await Yy.read(yyFile.absolute, 'sprites'); // Populate the frames to get UUIDs // Keep the old frameIds if it's a spine sprite (the alternative would be to ensure we rename the GameMaker-generated thumbnail) const frames = action.spine ? yy.frames : []; frames.length = action.spine ? 1 : sourcePngs.length; yy = yySpriteSchema.parse({ ...yy, frames }); if (action.spine) { trace.push('Is Spine action'); // Copy over and rename the skeleton files const uuid = yy.frames[0].name; const ioWaits = []; const keepers = new Set([yyFile.basename.toLowerCase()]); for (const fileType of ['json', 'atlas', 'png']) { const sourceFile = sourceFiles.find((f) => f.hasExtension(fileType)); const destFile = pathy(`${fileType === 'png' ? sourceFile.name : uuid}.${fileType}`, targetFolder); trace.push(`Copying ${sourceFile} to ${destFile}`); ioWaits.push(sourceFile.copy(destFile).catch((reason) => { trace.push(reason); throw reason; })); keepers.add(destFile.basename.toLowerCase()); keepers.add(`${uuid}.${fileType}`.toLowerCase()); } // Delete excess files for (const file of initialDestFiles) { if (!keepers.has(file.basename.toLowerCase())) { trace.push(`Deleting ${file}`); ioWaits.push(file .delete({ force: true, maxRetries: MAX_FIO_RETRIES, retryDelay: FIO_RETRY_DELAY, }) .catch((reason) => { trace.push(reason); throw reason; })); } } trace.push('Awaiting io ops'); await Promise.all(ioWaits); } else { trace.push('Is non-Spine action'); // Ensure the source pngs are sorted by basename sourcePngs.sort((a, b) => a.basename.localeCompare(b.basename, 'en-US')); // Ensure the width & height are still correct yy.width = width; yy.height = height; // Copy over the pngs const ioWaits = []; const keepers = new Set([yyFile.basename.toLowerCase()]); for (let i = 0; i < sourcePngs.length; i++) { const uuid = yy.frames[i].name; const png = sourcePngs[i]; const destFile = pathy(`${uuid}.png`, targetFolder); trace.push(`Copying ${png} to ${destFile}`); ioWaits.push(png.copy(destFile).catch((reason) => { trace.push(reason); throw reason; })); keepers.add(destFile.basename.toLowerCase()); } // Delete excess files for (const file of initialDestFiles) { if (!keepers.has(file.basename.toLowerCase())) { trace.push(`Deleting ${file}`); ioWaits.push(file .delete({ force: true, maxRetries: MAX_FIO_RETRIES, retryDelay: FIO_RETRY_DELAY, }) .catch((reason) => { trace.push(reason); throw reason; })); } } trace.push('Awaiting io ops'); await Promise.all(ioWaits); } // Save the yy file trace.push(`Saving yy file ${yyFile}`); await Yy.write(yyFile.absolute, yy, 'sprites', yyp); // Send back info that can be used to update the project file return { resource: { name: yy.name, path: yyFile.relativeFrom(path.dirname(projectYypPath)), }, folder: { name: yy.parent.name, folderPath: yy.parent.path, }, sourceRoot: action.sourceRoot, }; } catch (err) { trace.push(err); throw trace; } } //# sourceMappingURL=SpriteDest.actions.js.map