@bscotch/stitch-config
Version:
Schemas and utilities for the stitch.config.json file.
117 lines • 5.08 kB
JavaScript
import { ensureGroups, joinPaths, listYyFiles, parsePath, Yy, } from '@bscotch/yy';
import { ok } from 'node:assert';
import fsp from 'node:fs/promises';
import { stitchConfigFilename, stitchConfigSchema, } from "./schema.js";
/**
* Ensure that any texture/audio group assignments set in the config
* (if found) are being applied to the appropriate assets.
*/
export async function applyGroupAssignments(yypPath) {
const config = await loadProjectConfig(yypPath);
for (const kind of ['audio', 'texture']) {
// Make sure the group exists in the YYP file
const groupNames = Object.values(config[`${kind}GroupAssignments`] || {});
if (!groupNames.length)
continue;
await ensureGroups(yypPath, kind, groupNames);
// For each folder in the assignment config, find all matching
// resources and update their group. They apply by specificy, so
// can sort them by *longest* folder and apply the first match.
const assignments = Object.entries(config[`${kind}GroupAssignments`] || {});
// Sort by folder length
assignments.sort((a, b) => b[0].length - a[0].length);
const resourceParentFolder = joinPaths(parsePath(yypPath).parent, kind === 'audio' ? 'sounds' : 'sprites');
const yys = await listYyFiles(resourceParentFolder);
// Try to allow for some parallelization since we have to read a LOT
// of files.
const waits = [];
for (const yyFile of yys) {
waits.push(Yy.read(yyFile.path, kind === 'audio' ? 'sounds' : 'sprites').then(async (yy) => {
// Start from the *end* of the assignments and apply the first
// that matches (if any)
for (const [targetFolder, targetGroup] of assignments) {
const inFolder = yy.parent.path.replace(/^folders[/\\](.*)\.yy$/, '$1');
const isMatch = inFolder === targetFolder ||
inFolder.startsWith(`${targetFolder}/`);
if (!isMatch)
continue;
// Update the assigned group if it's different
if (kind === 'audio') {
const yySound = yy;
if (yySound.audioGroupId.name !== targetGroup) {
yySound.audioGroupId = {
name: targetGroup,
path: `audiogroups/${targetGroup}`,
};
await Yy.write(yyFile.path, yySound, 'sounds');
}
}
else {
const yySprite = yy;
if (yySprite.textureGroupId.name !== targetGroup) {
yySprite.textureGroupId = {
name: targetGroup,
path: `texturegroups/${targetGroup}`,
};
await Yy.write(yyFile.path, yySprite, 'sprites');
}
}
// No need to keep going if we got a match!
break;
}
}));
}
await Promise.allSettled(waits);
}
}
export async function ensureProjectConfig(yypPath) {
const configPath = await findProjectConfigPath(yypPath);
if (!(await fileExists(configPath.fullpath))) {
await saveProjectConfig(yypPath, stitchConfigSchema.parse({}));
}
}
/**
* Given a path to a GameMaker project's YYP file, load
* its Stitch Config file if it has one. Else return an
* empty config.
*/
export async function loadProjectConfig(yypPath) {
const configPath = await findProjectConfigPath(yypPath);
if (!(await fileExists(configPath.fullpath))) {
return stitchConfigSchema.parse({});
}
try {
const raw = await fsp.readFile(configPath.fullpath, 'utf8');
return stitchConfigSchema.parse(JSON.parse(raw));
}
catch (err) {
console.error('Failed to load existing Stitch config. Check for syntax and content errors.');
throw err;
}
}
export async function saveProjectConfig(yypPath, config) {
const configPath = await findProjectConfigPath(yypPath);
config = stitchConfigSchema.parse(config);
await fsp.writeFile(configPath.fullpath, JSON.stringify(config, null, 2), 'utf8');
}
/**
* Given a path to a GameMaker project's YYP file, get
* the path to where its config file should be. Asserts
* existence of the yyp file.
*/
export async function findProjectConfigPath(yypPath) {
ok(yypPath.endsWith('.yyp'), 'Target file must be a .yyp');
ok(await fileExists(yypPath), 'yyp file not found');
const configFile = joinPaths(parsePath(yypPath).parent, stitchConfigFilename);
return parsePath(configFile);
}
async function fileExists(path) {
try {
await fsp.access(path);
return true;
}
catch {
return false;
}
}
//# sourceMappingURL=project.js.map