@bscotch/yy
Version:
Stringify, parse, read, and write GameMaker yy and yyp files.
154 lines • 5.2 kB
JavaScript
import { z } from 'zod';
export const nameField = '%Name';
export function randomString(length = 32) {
let a = '';
for (let i = 0; i < length; i++) {
a += '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz'[(Math.random() * 60) | 0];
}
return a;
}
export class FixedNumber extends Number {
digits;
constructor(value, digits = 1) {
super(value.valueOf());
this.digits = digits;
}
[Symbol.toPrimitive](hint) {
return hint === 'string' ? this.toString() : this.valueOf();
}
toString() {
return this.toFixed(this.digits);
}
toJSON() {
return this.valueOf();
}
}
/**
* A wrapper that transforms the wrapped value to `undefined` and marks it as optional.
* Useful for fields you want to provide for extra
* information when transforming parents, since the
* `z.input<>` inferred type will show this field,
* but that don't normally exist in the source data.
*/
export function hint(schema) {
return schema.optional().transform(() => undefined);
}
export const fixed0 = new FixedNumber(0);
export const fixed1 = new FixedNumber(1);
export function fixedNumber(schema = z.number(), digits = 1) {
const coercedToNumber = z.preprocess((arg) => arg instanceof FixedNumber || typeof arg === 'number' ? +arg : arg, schema);
return coercedToNumber.transform((value) => new FixedNumber(value, digits));
}
/**
* Schema for a number or bigint cast to a bigint
*/
export function bigNumber() {
return z
.union([z.number(), z.bigint()])
.transform((value) => (typeof value === 'bigint' ? value : BigInt(value)));
}
/**
* Ensure that an array is initialized to an array with
* at least one element, allowing for defaults to be
* populated in each element.
*/
export function ensureObjects(obj, minItems = 1) {
return z.preprocess((arg) => {
arg = typeof arg === 'undefined' ? [] : arg;
if (Array.isArray(arg) && arg.length < minItems) {
const newItems = [...Array(Math.max(minItems - arg.length, 0))].map(() => ({}));
arg.push(...newItems);
}
return arg;
}, z.array(obj));
}
/**
* Shorthand for a `ZodObject` instance that doesn't strip
* out any unknown keys, and that logs unexpected keys to
* the console.
*/
export function unstable(shape) {
return z.looseObject(shape);
// return z.object(shape).catchall(
// z.unknown().superRefine((_arg, ctx) => {
// // The new format for name/resourcetype keys should be ignore, since those are handled in other ways.
// ctx.issues.every((iss) => {
// const isNewKey = `${String(iss.path?.at(-1))}`.match(/^[$%]/);
// if (!isNewKey) {
// console.log(`WARNING: Unexpected Key "${iss.path?.join('/')}"`);
// }
// });
// }),
// );
}
export function getYyResourceId(yyType, name) {
return {
name,
path: `${yyType}/${name}/${name}.yy`,
};
}
export function yyResourceIdSchemaGenerator(yyType) {
const pathFromName = (name) => `${yyType}/${name}/${name}.yy`;
return z.preprocess((arg) => {
if (arg === null || !['undefined', 'object'].includes(typeof arg)) {
return arg;
}
const objectId = arg === undefined ? {} : arg;
if (objectId.name && !objectId.path) {
objectId.path = pathFromName(objectId.name);
}
return objectId;
}, z
.object({
/** Object name */
name: z.string(),
/** Object resource path, e.g. "objects/{name}/{name}.yy" */
path: z.string(),
})
.refine((arg) => arg.path === pathFromName(arg.name)));
}
export function yyIsNewFormat(yyData) {
if (!yyData || typeof yyData !== 'object')
return false;
if (nameField in yyData && yyData[nameField] !== undefined)
return true;
if ('resourceType' in yyData && yyData.resourceType === '2.0')
return true;
return false;
}
export function isObjectWithField(obj, field) {
return (obj !== null &&
typeof obj === 'object' &&
field in obj &&
// @ts-expect-error
obj[field] !== undefined);
}
export function assert(claim, message) {
if (!claim)
throw new Error(message);
}
export function toPosixPath(path) {
return path.replace(/\\/g, '/');
}
export function parsePath(path) {
path = toPosixPath(path);
const parts = path.match(/^(?<parent>.*\/)?(?<filename>[^/]+?(?<ext>\.[^.]*)?)$/);
assert(parts, `Could not identify path parts of "${path}"`);
return { ...parts.groups, fullpath: path };
}
/**
* Join path parts, ensuring exactly one POSIX separator is between
* each part. Maintains initial and final seps.
*/
export function joinPaths(...parts) {
return parts
.map((part, i) => {
if (i === 0)
return part.replace(/\/+$/, ''); // First part: trim end only
if (i === parts.length - 1)
return part.replace(/^\/+/, ''); // Last part: trim start only
return part.replace(/^\/+|\/+$/g, ''); // Middle parts: trim both ends
})
.join('/');
}
//# sourceMappingURL=utility.js.map