diff --git a/src/components/blueprintSettingsDialog.svelte b/src/components/blueprintSettingsDialog.svelte index 63146e3a..19ece9f6 100644 --- a/src/components/blueprintSettingsDialog.svelte +++ b/src/components/blueprintSettingsDialog.svelte @@ -271,7 +271,7 @@ console.error(e) return { type: 'error', - message: translate('dialog.blueprint_settings.json_file.error.file_does_not_exist'), + message: translate('dialog.blueprint_settings.json_file.error.invalid_path'), } } switch (true) { @@ -333,8 +333,6 @@ // Export Settings export let exportNamespace: Valuable export let enablePluginMode: Valuable - // FIXME - Force-disable plugin mode for now - $enablePluginMode = false export let resourcePackExportMode: Valuable export let dataPackExportMode: Valuable export let targetMinecraftVersion: Valuable @@ -433,12 +431,12 @@ valueChecker={exportNamespaceChecker} /> - + {#if $enablePluginMode} [1] + ) { + super(message) + this.name = 'IntentionalExportError' + } +} + +export class IntentionalExportErrorFromInvalidFile extends IntentionalExportError { + constructor(filePath: string, public originalError: Error) { + const parsed = PathModule.parse(filePath) + super( + `Failed to read file ${parsed.base}:\n\n` + + '```\n' + + originalError + + '\n```', + { + commands: { + open_file: { + text: 'Open File Location', + icon: 'folder_open', + }, + }, + }, + button => { + if (button === 'open_file') { + shell.showItemInFolder(filePath) + } + } + ) + this.name = 'IntentionalExportErrorFromInvalidFile' + } +} + diff --git a/src/systems/exporter.ts b/src/systems/exporter.ts index 67bed5c0..b1a143f2 100644 --- a/src/systems/exporter.ts +++ b/src/systems/exporter.ts @@ -9,48 +9,13 @@ import { isResourcePackPath } from '../util/minecraftUtil' import { translate } from '../util/translation' import { Variant } from '../variants' import { hashAnimations, renderProjectAnimations } from './animationRenderer' +import { exportJSON } from './jsonCompiler' import compileDataPack from './datapackCompiler' +import { IntentionalExportError } from './errors' import resourcepackCompiler from './resourcepackCompiler' import { hashRig, renderRig } from './rigRenderer' import { isCubeValid } from './util' -export class IntentionalExportError extends Error { - constructor( - message: string, - public messageBoxOptions?: MessageBoxOptions, - public messageBoxCallback?: Parameters[1] - ) { - super(message) - this.name = 'IntentionalExportError' - } -} - -export class IntentionalExportErrorFromInvalidFile extends IntentionalExportError { - constructor(filePath: string, public originalError: Error) { - const parsed = PathModule.parse(filePath) - super( - `Failed to read file ${parsed.base}:\n\n` + - '```\n' + - originalError + - '\n```', - { - commands: { - open_file: { - text: 'Open File Location', - icon: 'folder_open', - }, - }, - }, - button => { - if (button === 'open_file') { - shell.showItemInFolder(filePath) - } - } - ) - this.name = 'IntentionalExportErrorFromInvalidFile' - } -} - export function getExportPaths() { const aj = Project!.animated_java @@ -164,7 +129,7 @@ async function actuallyExportProject({ debugMode, }) - if (aj.data_pack_export_mode !== 'none') { + if (!aj.enable_plugin_mode && aj.data_pack_export_mode !== 'none') { await compileDataPack([aj.target_minecraft_version], { rig, animations, @@ -175,6 +140,17 @@ async function actuallyExportProject({ }) } + if (aj.enable_plugin_mode) { + PROGRESS_DESCRIPTION.set('Exporting Plugin JSON...') + exportJSON({ + rig, + animations, + displayItemPath, + textureExportFolder, + modelExportFolder, + }) + } + Project!.last_used_export_namespace = aj.export_namespace if (forceSave) saveBlueprint() diff --git a/src/systems/global.ts b/src/systems/global.ts index 8e322dbf..0f6fe29c 100644 --- a/src/systems/global.ts +++ b/src/systems/global.ts @@ -1,5 +1,5 @@ import { normalizePath } from '../util/fileUtil' -import { IntentionalExportError, IntentionalExportErrorFromInvalidFile } from './exporter' +import { IntentionalExportError, IntentionalExportErrorFromInvalidFile } from './errors' import { sortObjectKeys } from './util' export enum SUPPORTED_MINECRAFT_VERSIONS { diff --git a/src/systems/jsonCompiler.ts b/src/systems/jsonCompiler.ts index 2dba4d0c..cad1388b 100644 --- a/src/systems/jsonCompiler.ts +++ b/src/systems/jsonCompiler.ts @@ -2,6 +2,7 @@ /// /// +import { PACKAGE } from '../constants' import type { IBlueprintDisplayEntityConfigJSON } from '../formats/blueprint' import { type defaultValues } from '../formats/blueprint/settings' import type { EasingKey } from '../util/easing' @@ -9,6 +10,7 @@ import { resolvePath } from '../util/fileUtil' import { detectCircularReferences, mapObjEntries, scrubUndefined } from '../util/misc' import { Variant } from '../variants' import type { INodeTransform, IRenderedAnimation, IRenderedFrame } from './animationRenderer' +import { IntentionalExportError } from './errors' import { JsonText } from './jsonText' import type { AnyRenderedNode, @@ -18,42 +20,34 @@ import type { IRenderedVariantModel, } from './rigRenderer' -type ExportedNodetransform = Omit< - INodeTransform, - 'type' | 'name' | 'uuid' | 'node' | 'matrix' | 'decomposed' | 'executeCondition' -> & { +type ExportedNodetransform = Omit & { matrix: number[] decomposed: { translation: ArrayVector3 left_rotation: ArrayVector4 scale: ArrayVector3 } - pos: ArrayVector3 - rot: ArrayVector3 - scale: ArrayVector3 - execute_condition?: string } type ExportedRenderedNode = Omit< AnyRenderedNode, - | 'node' - | 'parentNode' - | 'model' - | 'boundingBox' - | 'configs' - | 'baseScale' - | 'path_name' + 'default_transform' + | 'bounding_box' + | 'configs' | 'storage_name' > & { default_transform: ExportedNodetransform bounding_box?: { min: ArrayVector3; max: ArrayVector3 } configs?: Record } -type ExportedAnimationFrame = Omit & { +type ExportedAnimationFrame = Omit & { node_transforms: Record } type ExportedBakedAnimation = Omit< IRenderedAnimation, - 'uuid' | 'frames' | 'modified_nodes' | 'path_name' | 'storage_name' + 'uuid' + | 'frames' + | 'modified_nodes' + | 'storage_name' > & { frames: ExportedAnimationFrame[] modified_nodes: string[] @@ -98,16 +92,16 @@ interface ExportedDynamicAnimation { animators: Record } interface ExportedTexture { + uuid: string name: string src: string } -type ExportedVariantModel = Omit< +type ExportedVariantModel = Pick< IRenderedVariantModel, - 'model_path' | 'resource_location' | 'item_model' -> & { - model: IRenderedModel | null - custom_model_data: number -} + 'custom_model_data' + | 'resource_location' + | 'item_model' +> & { model: IRenderedModel | null } type ExportedVariant = Omit & { /** * A map of bone UUID -> IRenderedVariantModel @@ -116,16 +110,25 @@ type ExportedVariant = Omit & { } export interface IExportedJSON { + format_version: '2.0.0' + exported_with: { + name: string + version: string + } /** * The Blueprint's Settings */ settings: { export_namespace: (typeof defaultValues)['export_namespace'] + target_minecraft_version: (typeof defaultValues)['target_minecraft_version'] + display_item: (typeof defaultValues)['display_item'] bounding_box: (typeof defaultValues)['render_box'] // Resource Pack Settings custom_model_data_offset: (typeof defaultValues)['custom_model_data_offset'] // Plugin Settings baked_animations: (typeof defaultValues)['baked_animations'] + interpolation_duration: (typeof defaultValues)['interpolation_duration'] + teleportation_duration: (typeof defaultValues)['teleportation_duration'] } textures: Record nodes: Record @@ -137,7 +140,12 @@ export interface IExportedJSON { } function transferKey(obj: any, oldKey: string, newKey: string) { - obj[newKey] = obj[oldKey] + if (!Object.prototype.hasOwnProperty.call(obj, oldKey)) return + const value = obj[oldKey] + if (value === undefined) return + if (obj[newKey] === undefined) { + obj[newKey] = value + } delete obj[oldKey] } @@ -209,6 +217,8 @@ function serializeVariant(rig: IRenderedRig, variant: IRenderedVariant): Exporte const json: ExportedVariantModel = { model: model.model, custom_model_data: model.custom_model_data, + resource_location: model.resource_location, + item_model: model.item_model, } return [uuid, json] }), @@ -230,22 +240,29 @@ export function exportJSON(options: { function serializeTexture(texture: Texture): ExportedTexture { return { + uuid: texture.uuid, name: texture.name, src: texture.getDataURL(), } } const json: IExportedJSON = { + format_version: '2.0.0', + exported_with: { + name: PACKAGE.name, + version: PACKAGE.version, + }, settings: { export_namespace: aj.export_namespace, + target_minecraft_version: aj.target_minecraft_version, + display_item: aj.display_item, bounding_box: aj.render_box, custom_model_data_offset: aj.custom_model_data_offset, baked_animations: aj.baked_animations, + interpolation_duration: aj.interpolation_duration, + teleportation_duration: aj.teleportation_duration, }, - textures: mapObjEntries(rig.textures, (_, texture) => [ - texture.uuid, - serializeTexture(texture), - ]), + textures: mapObjEntries(rig.textures, (id, texture) => [id, serializeTexture(texture)]), nodes: mapObjEntries(rig.nodes, (uuid, node) => [uuid, serailizeRenderedNode(node)]), variants: mapObjEntries(rig.variants, (uuid, variant) => [ uuid, @@ -286,12 +303,22 @@ export function exportJSON(options: { try { exportPath = resolvePath(aj.json_file) } catch (e) { - console.log(`Failed to resolve export path '${aj.json_file}'`) - console.error(e) - return + throw new IntentionalExportError( + `Failed to resolve export path ${aj.json_file}: ${String(e)}` + ) } - fs.writeFileSync(exportPath, compileJSON(json).toString()) + try { + const dir = PathModule.dirname(exportPath) + if (dir && dir !== '.' && !fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }) + } + fs.writeFileSync(exportPath, compileJSON(json).toString()) + } catch (e: any) { + throw new IntentionalExportError( + `Failed to write JSON file ${exportPath}: ${String(e)}` + ) + } } function serailizeNodeTransform(node: INodeTransform): ExportedNodetransform { @@ -325,19 +352,24 @@ function serailizeRenderedNode(node: AnyRenderedNode): ExportedRenderedNode { transferKey(json, 'backgroundAlpha', 'background_alpha') json.default_transform = serailizeNodeTransform(json.default_transform as INodeTransform) + + if (node.type !== 'struct' && (node as any).bounding_box) { + json.bounding_box = { + min: (node as any).bounding_box.min.toArray(), + max: (node as any).bounding_box.max.toArray(), + } + } + + if ((node as any).configs) { + json.configs = { ...(node as any).configs?.variants } + const defaultVariant = Variant.getDefault() + if ((node as any).configs?.default && defaultVariant) { + json.configs[defaultVariant.uuid] = (node as any).configs.default + } + } + switch (node.type) { case 'bone': { - delete json.boundingBox - json.bounding_box = { - min: node.bounding_box.min.toArray(), - max: node.bounding_box.max.toArray(), - } - delete json.configs - json.configs = { ...node.configs?.variants } - const defaultVariant = Variant.getDefault() - if (node.configs?.default && defaultVariant) { - json.configs[defaultVariant.uuid] = node.configs.default - } break } case 'text_display': { diff --git a/src/systems/resourcepackCompiler/index.ts b/src/systems/resourcepackCompiler/index.ts index d80d0f10..8071fa42 100644 --- a/src/systems/resourcepackCompiler/index.ts +++ b/src/systems/resourcepackCompiler/index.ts @@ -1,6 +1,6 @@ import { MAX_PROGRESS, PROGRESS, PROGRESS_DESCRIPTION } from '../../interface/dialog/exportProgress' import { getNextSupportedVersion, getResourcePackFormat } from '../../util/minecraftUtil' -import { IntentionalExportError } from '../exporter' +import { IntentionalExportError } from '../errors' import { type IRenderedRig } from '../rigRenderer' import type { ExportedFile } from '../util' diff --git a/src/systems/rigRenderer.ts b/src/systems/rigRenderer.ts index 389907fd..95b8924b 100644 --- a/src/systems/rigRenderer.ts +++ b/src/systems/rigRenderer.ts @@ -20,7 +20,7 @@ import { restoreSceneAngle, updatePreview, } from './animationRenderer' -import { IntentionalExportError } from './exporter' +import { IntentionalExportError } from './errors' import { JsonText } from './jsonText' export interface IRenderedFace {