UNPKG

@bscotch/stitch

Version:

Stitch: The GameMaker Studio 2 Asset Pipeline Development Kit.

231 lines 8.24 kB
import { pathy, Pathy } from '@bscotch/pathy'; import { assert, toJson } from '@bscotch/utility'; import { logger } from './assetSource.lib.js'; import { AssetSourceFileAudio } from './AssetSourceFile.js'; import { audioSourceConfigSchema, configFileSchema, } from './assetSource.types.js'; export class AssetSourceConfig { dir; config; constructor(dir, config) { this.dir = dir; this.config = config; } get id() { return this.config.id; } get type() { return this.config.type; } get files() { return [...this.config.files]; } set files(files) { this.config.files = files; } absoluteFilePath(file) { return this.dir.join(file.path); } grouped() { const grouped = { ...this.config, groups: [], }; if (!this.config.groupBy?.length) { grouped.groups.push({ name: '', files: this.config.files }); return grouped; } const groupMap = new Map(); const groupPatterns = this.config.groupBy.map((p) => new RegExp(p)); for (const file of this.files) { let group = ''; for (const pattern of groupPatterns) { group = file.path.match(pattern)?.groups?.group ?? ''; if (group) { break; } } const groupedFiles = groupMap.get(group) ?? []; groupedFiles.push(file); groupMap.set(group, groupedFiles); } grouped.groups = [...groupMap.entries()] .map(([group, files]) => ({ name: group, files: files.sort(this.compareFiles), })) .sort((a, b) => { return this.compareFiles(a.files[0], b.files[0]); }); return grouped; } toJSON() { return { ...this.config }; } compareFiles(a, b) { // First by *deleted* if (a.deleted && !b.deleted) { return 1; } else if (!a.deleted && b.deleted) { return -1; } // Then by *importable* if (a.importable && !b.importable) { return 1; } if (b.importable && !a.importable) { return -1; } if ('updatedAt' in a && 'updatedAt' in b) { return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); } return 0; } } export class AssetSourcesConfig { path; constructor(path) { let configPath = Pathy.asInstance(path); configPath = configPath.basename === AssetSourcesConfig.basename ? configPath : configPath.join(AssetSourcesConfig.basename); this.path = Pathy.asInstance(configPath); } get dir() { return this.path.up(); } async listAudioSources() { return (await this.listSources({ filter: (s) => s.type === 'audio', })); } async listSources(options) { let sources = (await this.load()).sources.map((s) => new AssetSourceConfig(this.dir, s)); if (options?.filter) { sources = sources.filter(options.filter); } return sources; } async findAudioSource(id, config) { const source = await this.findSource(id, config); assert(source[0].type === 'audio', `Source ${id} is not an audio source`); return source; } async findSource(id, config) { config ||= await this.load(); const matchingSourceIdx = config.sources.findIndex((s) => s.id === id); const matchingSource = config.sources[matchingSourceIdx]; assert(matchingSource, `No audio source found with ID ${id}`); return [new AssetSourceConfig(this.dir, matchingSource), matchingSourceIdx]; } async addAudioSource(info) { const config = await this.load(); if (info?.id && config.sources.some((s) => s.id === info.id)) { throw new Error(`Source with id "${info.id}" already exists.`); } // For now, since we only have one set of audio source rules (i.e. "find everything") // we should just return any already-existing source. let audioSourceConfig = config.sources.find((s) => s.type === 'audio'); if (!audioSourceConfig) { audioSourceConfig = audioSourceConfigSchema.default({}).parse(info); config.sources.push(audioSourceConfig); await this.write(config); } return await this.refreshAudioSource(audioSourceConfig.id); } async removeSource(id) { const config = await this.load(); const [, sourceIdx] = await this.findSource(id, config); config.sources.splice(sourceIdx, 1); if (config.sources.length === 0) { await this.path.delete(); } else { await this.write(config); } } async updateAudioSource(id, info) { assert(!('id' in info) || info.id === id, 'Cannot change the id of an audio source'); assert(!('files' in info), 'Cannot directly change the files of an audio source'); const config = await this.load(); const [matchingSource, matchingSourceIdx] = await this.findSource(id, config); config.sources[matchingSourceIdx] = audioSourceConfigSchema.parse({ ...matchingSource, ...info, }); await this.write(config); await this.refreshAudioSource(id); return matchingSource; } /** * Update the config file to reflect the current state * of all described files. */ async refreshAudioSource(id) { const config = await this.load(); const [source, sourceIdx] = await this.findSource(id, config); assert(source.type === 'audio', `Source ${id} is not an audio source`); const existingPaths = new Set(); const [knownAudioFiles, audioFilePaths] = await Promise.all([ Promise.all(source.files.map((f) => { const file = AssetSourceFileAudio.from(pathy(f.path, this.dir), f); existingPaths.add(file.path.toString()); return file; })), this.dir.listChildrenRecursively({ includeExtension: ['mp3', 'wav', 'ogg', 'wma'], transform: (p) => { return new AssetSourceFileAudio(pathy(p, this.dir)); }, }), ]); // Identify new files for (const audioFilePath of audioFilePaths) { if (!existingPaths.has(audioFilePath.path.toString())) { knownAudioFiles.push(audioFilePath); logger.info(`Found new audio file: ${audioFilePath.path.relative}`); } } knownAudioFiles.sort((a, b) => Pathy.compare(a.path, b.path)); const files = await Promise.all(knownAudioFiles.map((f) => f.refresh())); source.files = toJson(files); config.sources[sourceIdx] = source.toJSON(); await this.write(config); return source; } async toggleImportables(sourceId, fileIds, importable) { const config = await this.load(); const [source] = await this.findSource(sourceId, config); const fileIdsSet = new Set(fileIds); for (const file of source.files) { if (fileIdsSet.size === 0) { break; } if (fileIdsSet.has(file.id)) { file.importable = importable; fileIdsSet.delete(file.id); } } await this.write(config); } async toggleImportable(sourceId, fileId, importable) { await this.toggleImportables(sourceId, [fileId], importable); } async load() { return await this.path.read({ schema: configFileSchema, fallback: {}, }); } async write(config) { await this.path.write(config, { schema: configFileSchema, }); } static basename = 'stitch.src.json'; static from(path) { return new AssetSourcesConfig(path); } } //# sourceMappingURL=AssetSourcesConfig.js.map