diff --git a/src/formats/blueprint/codec.ts b/src/formats/blueprint/codec.ts index fc07e69e..ad6c9145 100644 --- a/src/formats/blueprint/codec.ts +++ b/src/formats/blueprint/codec.ts @@ -11,6 +11,7 @@ import { localize as translate } from '../../util/lang' import { sanitizeStorageKey } from '../../util/minecraftUtil' import { Variant } from '../../variants' import { upgradeAnimatedJavaBlueprint } from './dfu' +import { groupsReferencedByOutliner } from './outlinerGroups' import * as blueprintSettings from './settings' // region Codec @@ -150,7 +151,10 @@ export const BLUEPRINT_CODEC = registerDeletableHandlerPatch({ } if (model.groups) { - for (const template of model.groups) { + for (const template of groupsReferencedByOutliner( + model.groups, + model.outliner + )) { // @ts-expect-error - missing UUID arg new Group(template, template.uuid).init() } @@ -291,13 +295,15 @@ export const BLUEPRINT_CODEC = registerDeletableHandlerPatch({ model.elements.push(element.getSaveCopy?.(!!model.meta)) } + const outliner = Outliner.toJSON() + model.groups = [] - for (const group of Group.all) { + for (const group of groupsReferencedByOutliner(Group.all, outliner)) { // @ts-expect-error - missing arg model.groups.push(group.getSaveCopy(false)) } - model.outliner = Outliner.toJSON() + model.outliner = outliner model.textures = [] for (const texture of Texture.all) { diff --git a/src/formats/blueprint/outlinerGroups.ts b/src/formats/blueprint/outlinerGroups.ts new file mode 100644 index 00000000..685ae381 --- /dev/null +++ b/src/formats/blueprint/outlinerGroups.ts @@ -0,0 +1,31 @@ +interface GroupWithUUID { + uuid?: unknown +} + +interface OutlinerGroupEntry { + uuid?: unknown + children?: unknown + content?: unknown +} + +export function groupsReferencedByOutliner( + groups: readonly T[], + outliner: readonly unknown[] | undefined +): T[] { + if (outliner === undefined) return [...groups] + + const referencedUUIDs = new Set() + function visit(entries: readonly unknown[]) { + for (const entry of entries) { + if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) continue + + const group = entry as OutlinerGroupEntry + if (typeof group.uuid === 'string') referencedUUIDs.add(group.uuid) + if (Array.isArray(group.children)) visit(group.children) + if (Array.isArray(group.content)) visit(group.content) + } + } + + visit(outliner) + return groups.filter(group => typeof group.uuid === 'string' && referencedUUIDs.has(group.uuid)) +} diff --git a/src/tests/blueprintOutlinerGroups.test.ts b/src/tests/blueprintOutlinerGroups.test.ts new file mode 100644 index 00000000..0d830eca --- /dev/null +++ b/src/tests/blueprintOutlinerGroups.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest' +import { groupsReferencedByOutliner } from '../formats/blueprint/outlinerGroups' + +describe('groupsReferencedByOutliner', () => { + it('collects nested children and legacy content entries while ignoring UUID strings', () => { + const groups = [ + { uuid: 'root' }, + { uuid: 'child' }, + { uuid: 'legacy' }, + { uuid: 'string-entry' }, + { uuid: 'orphan' }, + ] + + const referenced = groupsReferencedByOutliner(groups, [ + { + uuid: 'root', + children: [{ uuid: 'child', content: [{ uuid: 'legacy' }, 'string-entry'] }], + }, + 'string-entry', + ]) + + expect(referenced.map(group => group.uuid)).toEqual(['root', 'child', 'legacy']) + }) + + it('keeps all groups when outliner is undefined', () => { + const groups = [{ uuid: 'first' }, { uuid: 'second' }] + + expect(groupsReferencedByOutliner(groups, undefined)).toEqual(groups) + }) + + it('excludes all groups for a defined empty outliner', () => { + const groups = [{ uuid: 'orphan' }] + + expect(groupsReferencedByOutliner(groups, [])).toEqual([]) + }) +})