From e2fb7c7189715325eca9d87817331b87ae0db343 Mon Sep 17 00:00:00 2001 From: Adam Coster Date: Wed, 25 Sep 2024 11:11:07 -0500 Subject: [PATCH] feat: GameChanger data can now optionally have all refs and overrides resolved on load, for cases where that's useful. --- packages/gcdata/src/GameChanger.ts | 68 ++++++++++++++++++++++++++++-- packages/gcdata/src/types.ts | 12 +++++- packages/gcdata/src/util.ts | 5 +++ 3 files changed, 81 insertions(+), 4 deletions(-) diff --git a/packages/gcdata/src/GameChanger.ts b/packages/gcdata/src/GameChanger.ts index d8f9fa77..603e854d 100644 --- a/packages/gcdata/src/GameChanger.ts +++ b/packages/gcdata/src/GameChanger.ts @@ -9,6 +9,7 @@ import { } from './types.cl2.rumpus.js'; import { Bschema, + BschemaObject, ChangeType, Changes, changeSchema, @@ -17,7 +18,9 @@ import { isBschemaConst, isBschemaEnum, isBschemaNumeric, + isBschemaRef, isBschemaString, + isObject, type Mote, type MoteId, type PackedData, @@ -51,7 +54,15 @@ export interface MoteVisitorCtx { } export class Gcdata { - constructor(public data: PackedData) {} + constructor( + public data: PackedData, + options?: { resolveRefsAndOverrides?: boolean }, + ) { + // Resolve all refs and overrides in the schemas + if (options?.resolveRefsAndOverrides) { + Gcdata.resolveRefsAndOverrides(data); + } + } get motes(): PackedData['motes'] { return { ...this.data.motes, @@ -201,9 +212,60 @@ export class Gcdata { ) as Mote[]; } - static async from(gcdataFile: Pathy) { + static async from( + gcdataFile: Pathy, + options?: { resolveRefsAndOverrides?: boolean }, + ) { const data = JSON.parse(await gcdataFile.read({ parse: false })); - return new Gcdata(data); + return new Gcdata(data, options); + } + + /** + * Given a raw packed data object, recurse through to resolve + * all references and overrides. + */ + protected static resolveRefsAndOverrides(data: PackedData): void { + const recursivelyResolve = (subschema: Bschema) => { + while (isBschemaRef(subschema)) { + const ref = data.schemas[subschema.$ref]; + assert(ref, `Could not resolve reference ${subschema.$ref}`); + // Mutate in place + Object.assign(subschema, ref); + //@ts-expect-error We've changed the type from a ref + delete subschema.$ref; + } + if (isObject(subschema)) { + if ('overrides' in subschema && isObject(subschema.overrides)) { + Object.assign(subschema, subschema.overrides); + delete subschema.overrides; + } + // If this was an object, need to recurse through properties + // and additionalProperties + if ( + 'additionalProperties' in subschema && + isObject(subschema.additionalProperties) + ) { + recursivelyResolve(subschema.additionalProperties as BschemaObject); + } + if ('properties' in subschema && isObject(subschema.properties)) { + for (const prop of Object.values( + subschema.properties as Record, + )) { + recursivelyResolve(prop); + } + } + // // This gives us a max callstack error, + // // so there must be circularity somewhere + // if ('oneOf' in subschema && Array.isArray(subschema.oneOf)) { + // for (const oneOf of subschema.oneOf) { + // recursivelyResolve(oneOf); + // } + // } + } + }; + for (const schema of Object.values(data.schemas)) { + recursivelyResolve(schema); + } } } diff --git a/packages/gcdata/src/types.ts b/packages/gcdata/src/types.ts index 7c764f67..400eeee9 100644 --- a/packages/gcdata/src/types.ts +++ b/packages/gcdata/src/types.ts @@ -87,6 +87,10 @@ export function getAdditionalProperties( return; } +export function isBschemaRef(schema: any): schema is BschemaRef { + return isObject(schema) && '$ref' in schema && !!schema['$ref']; +} + export function isBschemaObject(schema: any): schema is BschemaObject { return ( typeof schema === 'object' && @@ -131,7 +135,7 @@ interface BschemaBase { /** * A partial schema that will overwrite fields of the current one */ - overrides?: string; + overrides?: any; /** * Internal pointer to the field that holds the mote's name @@ -324,3 +328,9 @@ export const changesSchema = z }), }) .passthrough(); + +export function isObject( + value: unknown, +): value is Exclude { + return typeof value === 'object' && value !== null; +} diff --git a/packages/gcdata/src/util.ts b/packages/gcdata/src/util.ts index 68192b9e..5d78e4fb 100644 --- a/packages/gcdata/src/util.ts +++ b/packages/gcdata/src/util.ts @@ -154,6 +154,7 @@ export function normalizeSchema( /** For resolving oneOfs */ data: any, ): Bschema { + let overrides: Bschema | undefined; if ('$ref' in schema) { const refParts = schema.$ref.split('/'); schema = gcData.getSchema(refParts[0])!; @@ -162,6 +163,9 @@ export function normalizeSchema( schema = resolvePointer(refParts.slice(1), schema)!; assert(schema, `Could not resolve subpointer $ref ${refParts.join('/')}`); } + if ('overrides' in schema) { + overrides = schema.overrides; + } } const oneOf = 'oneOf' in schema ? resolveOneOf(schema, data) : undefined; let properties: BschemaObject['properties'] | undefined; @@ -184,6 +188,7 @@ export function normalizeSchema( // @ts-ignore additionalProperties, oneOf: undefined, + ...overrides, }; }