UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

1,011 lines (1,010 loc) 75 kB
"use strict"; // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const ste_events_1 = require("ste-events"); const StorageUtilities_1 = __importDefault(require("../storage/StorageUtilities")); const ModelGeometryDefinition_1 = __importDefault(require("../minecraft/ModelGeometryDefinition")); const IProjectItemData_1 = require("../app/IProjectItemData"); const EntityTypeResourceDefinition_1 = __importDefault(require("../minecraft/EntityTypeResourceDefinition")); const Utilities_1 = __importDefault(require("../core/Utilities")); const exifrModule = __importStar(require("exifr")); // Handle CJS/ESM interop: esbuild wraps CJS default export, while ts-node uses named exports directly const Exifr = exifrModule.Exifr || exifrModule.default?.Exifr; const Project_1 = require("../app/Project"); const Log_1 = __importDefault(require("../core/Log")); const AttachableResourceDefinition_1 = __importDefault(require("../minecraft/AttachableResourceDefinition")); const BlockTypeDefinition_1 = __importDefault(require("../minecraft/BlockTypeDefinition")); const AnimationResourceDefinition_1 = __importDefault(require("../minecraft/AnimationResourceDefinition")); const MinecraftUtilities_1 = __importDefault(require("../minecraft/MinecraftUtilities")); class BlockbenchModel { _file; _id; _isLoaded = false; _data; _onLoaded = new ste_events_1.EventDispatcher(); get data() { return this._data; } set data(content) { this._data = content; } get isLoaded() { return this._isLoaded; } get file() { return this._file; } set file(newFile) { this._file = newFile; } get onLoaded() { return this._onLoaded.asEvent(); } get name() { if (this._data) { return this._data.name; } return undefined; } get id() { if (this._id) { return this._id; } if (this._data && this._data.model_identifier) { return this._data.model_identifier; } return undefined; } set id(newId) { this._id = newId; if (this._data && newId !== undefined) { this._data.model_identifier = newId; } } static ensureFromContent(content) { const bd = new BlockbenchModel(); let obj; try { obj = JSON.parse(content); } catch (e) { throw new Error("Failed to parse Blockbench model JSON: " + (e instanceof Error ? e.message : String(e))); } bd.data = obj; return bd; } getMinecraftUVFace(face) { if (face.uv.length >= 4) { return { uv: [face.uv[0], face.uv[1]], uv_size: [face.uv[2] - face.uv[0], face.uv[3] - face.uv[1]], }; } else if (face.uv.length >= 2) { return { uv: [face.uv[0], face.uv[1]], uv_size: [1, 1], }; } return { uv: [0, 0], uv_size: [1, 1] }; } async updateMinecraftAnimationsFromBlockbench(animationDef, etrd) { if (!this.data?.animations) { return; } for (const animation of this.data.animations) { if (animation.name) { const targetMcAnimation = animationDef.ensureAnimation(animation.name); if (animation.loop === "loop") { targetMcAnimation.loop = true; } else { targetMcAnimation.loop = false; } if (etrd) { etrd.ensureAnimationAndScript(animation.name); etrd.persist(); } const rotationKeyframes = {}; const positionKeyframes = {}; const scaleKeyframes = {}; for (const animatorName in animation.animators) { const animator = animation.animators[animatorName]; if (animator && Utilities_1.default.isUsableAsObjectKey(animatorName)) { if (animator.type === "bone" && animator.name && animator.keyframes) { for (const keyframe of animator.keyframes) { if (keyframe.data_points && keyframe.data_points.length > 0) { const keyframeData = []; if (keyframe.data_points[0].x) { keyframeData.push(keyframe.data_points[0].x); } if (keyframe.data_points[0].y) { keyframeData.push(keyframe.data_points[0].y); } if (keyframe.data_points[0].z) { keyframeData.push(keyframe.data_points[0].z); } if (keyframe.channel === "rotation") { if (!rotationKeyframes[animator.name]) { rotationKeyframes[animator.name] = {}; } rotationKeyframes[animator.name][keyframe.time.toString()] = keyframeData; } else if (keyframe.channel === "position") { if (!positionKeyframes[animator.name]) { positionKeyframes[animator.name] = {}; } positionKeyframes[animator.name][keyframe.time.toString()] = keyframeData; } else if (keyframe.channel === "scale") { if (!scaleKeyframes[animator.name]) { scaleKeyframes[animator.name] = {}; } scaleKeyframes[animator.name][keyframe.time.toString()] = keyframeData; } } } } } } for (const boneName in rotationKeyframes) { if (Utilities_1.default.isUsableAsObjectKey(boneName)) { const boneKeyframes = rotationKeyframes[boneName]; if (boneKeyframes) { if (!targetMcAnimation.bones[boneName]) { targetMcAnimation.bones[boneName] = {}; } const boneKeys = Object.keys(boneKeyframes); if (boneKeys.length === 1 && boneKeys[0] === "0") { targetMcAnimation.bones[boneName].rotation = boneKeyframes["0"]; } else { targetMcAnimation.bones[boneName].rotation = boneKeyframes; } } } } for (const boneName in positionKeyframes) { if (Utilities_1.default.isUsableAsObjectKey(boneName)) { const boneKeyframes = positionKeyframes[boneName]; if (boneKeyframes) { if (!targetMcAnimation.bones[boneName]) { targetMcAnimation.bones[boneName] = { rotation: {}, position: {}, scale: {}, }; } const boneKeys = Object.keys(boneKeyframes); if (boneKeys.length === 1 && boneKeys[0] === "0") { targetMcAnimation.bones[boneName].position = boneKeyframes["0"]; } else { targetMcAnimation.bones[boneName].position = boneKeyframes; } } } } for (const boneName in scaleKeyframes) { if (Utilities_1.default.isUsableAsObjectKey(boneName)) { const boneKeyframes = scaleKeyframes[boneName]; if (boneKeyframes) { if (!targetMcAnimation.bones[boneName]) { targetMcAnimation.bones[boneName] = { rotation: {}, position: {}, scale: {}, }; } const boneKeys = Object.keys(boneKeyframes); if (boneKeys.length === 1 && boneKeys[0] === "0") { targetMcAnimation.bones[boneName].scale = boneKeyframes["0"]; } else { targetMcAnimation.bones[boneName].scale = boneKeyframes; } } } } } } } async updateGeometryFromModel(geo, formatVersion) { if (this.data?.resolution) { geo.textureheight = this.data.resolution.height; geo.texturewidth = this.data.resolution.width; // For 1.10+ format, also update the description which the renderer reads first if (geo.description) { geo.description.texture_width = this.data.resolution.width; geo.description.texture_height = this.data.resolution.height; } } if (this.data?.visible_box && this.data.visible_box.length === 3) { geo.visible_bounds_width = this.data.visible_box[0]; geo.visible_bounds_height = this.data.visible_box[1]; geo.visible_bounds_offset = [0, this.data.visible_box[2], 0]; if (geo.description) { geo.description.visible_bounds_width = this.data.visible_box[0]; geo.description.visible_bounds_height = this.data.visible_box[1]; geo.description.visible_bounds_offset = [0, this.data.visible_box[2], 0]; } } const bonesByName = {}; const groupsByUuid = {}; const cubesById = {}; const locatorsById = {}; if (this.data?.elements) { geo.bones = []; for (const elt of this.data.elements) { if (elt.type === "cube") { if (elt.from && elt.from.length === 3 && elt.to && elt.to.length === 3) { let uvTarg = undefined; if (elt.box_uv) { if (!elt.uv_offset) { if (elt.faces && elt.faces.east && elt.faces.east.uv && elt.faces.east.uv.length >= 1 && elt.faces.down && elt.faces.down.uv && elt.faces.down.uv.length >= 1) { uvTarg = [elt.faces.east.uv[0], elt.faces.down.uv[1]]; } } else { uvTarg = elt.uv_offset; } } else if (elt.faces) { uvTarg = { north: this.getMinecraftUVFace(elt.faces.north), east: this.getMinecraftUVFace(elt.faces.east), south: this.getMinecraftUVFace(elt.faces.south), west: this.getMinecraftUVFace(elt.faces.west), up: this.getMinecraftUVFace(elt.faces.up), down: this.getMinecraftUVFace(elt.faces.down), }; } let cube = { origin: elt.from, size: [ Math.abs(elt.to[0] - elt.from[0]), Math.abs(elt.to[1] - elt.from[1]), Math.abs(elt.to[2] - elt.from[2]), ], uv: uvTarg, }; cube.name = elt.name; if (elt.rotation && elt.rotation.length === 3) { cube.rotation = [-elt.rotation[0], -elt.rotation[1], -elt.rotation[2]]; } if (elt.origin) { cube.pivot = elt.origin; } if (Utilities_1.default.isUsableAsObjectKey(elt.uuid)) { cubesById[elt.uuid] = cube; } } } else if (elt.type === "locator") { if (Utilities_1.default.isUsableAsObjectKey(elt.uuid)) { locatorsById[elt.uuid] = elt; } } } } if (this.data?.groups) { for (const groupItem of this.data.groups) { if (groupItem && typeof groupItem !== "string" && groupItem.name && Utilities_1.default.isUsableAsObjectKey(groupItem.uuid)) { groupsByUuid[groupItem.uuid] = groupItem; } } } if (this.data?.outliner) { this.processOutlineItemsIntoBonesByName(this.data.outliner, groupsByUuid, bonesByName, cubesById, locatorsById, formatVersion); } for (const boneName in bonesByName) { if (Utilities_1.default.isUsableAsObjectKey(boneName)) { const bone = bonesByName[boneName]; geo.bones.push(bone); } } } static isLessThan110(formatVersion) { if (formatVersion[0] > 1) { return false; } if (formatVersion.length >= 2) { return formatVersion[1] < 10; } // if the format version doesn't match any known format, presume it's a pre-format version file? return true; } processOutlineItemsIntoBonesByName(outlineItems, groupsByUuid, bonesByName, cubesById, locatorsById, formatVersion, parent, context) { if (!context) { context = { addIndex: 1, }; } for (const outlineItem of outlineItems) { if (outlineItem && typeof outlineItem === "string") { const elt = cubesById[outlineItem]; if (elt) { if (parent) { if (!parent.cubes) { parent.cubes = []; } if (elt.pivot && parent.pivot && elt.pivot[0] === parent.pivot[0] && elt.pivot[1] === parent.pivot[1] && elt.pivot[2] === parent.pivot[2]) { elt.pivot = undefined; } parent.cubes.push(elt); } else { // Top-level cube not inside any group — create a bone for it, // since Minecraft geometry requires every cube to belong to a bone. const cubeName = elt.name || "bone" + context.addIndex.toString(); let boneName = cubeName; // Ensure unique bone name if (bonesByName[boneName]) { let suffix = 1; while (bonesByName[boneName + suffix]) { suffix++; } boneName = boneName + suffix; } context.addIndex++; const orphanBone = { name: boneName, pivot: elt.pivot || [0, 0, 0], cubes: [elt], }; elt.pivot = undefined; bonesByName[boneName] = orphanBone; } } else { const lead = locatorsById[outlineItem]; if (lead && lead.name && lead.position && Utilities_1.default.isUsableAsObjectKey(lead.name)) { if (parent) { if (!parent.locators) { parent.locators = {}; } parent.locators[lead.name] = lead.position; } } } } else if (outlineItem && typeof outlineItem !== "string") { let groupData = groupsByUuid[outlineItem.uuid]; if (!groupData) { groupData = outlineItem; } let name = groupData.name ? groupData.name : String("bone" + context.addIndex.toString()); let bone = bonesByName[name]; if (!bone) { bone = { name: name, pivot: [], binding: !BlockbenchModel.isLessThan110(formatVersion) && groupData.bedrock_binding ? groupData.bedrock_binding : undefined, cubes: undefined, locators: undefined, }; bonesByName[name] = bone; } let rot = groupData.rotation; if (rot) { if (rot.length === 3) { rot[0] = -rot[0]; rot[1] = -rot[1]; rot[2] = -rot[2]; bone.rotation = rot; } } if (groupData.origin) { bone.pivot = groupData.origin; } if (parent) { bone.parent = parent.name; } if (outlineItem.children) { this.processOutlineItemsIntoBonesByName(outlineItem.children, groupsByUuid, bonesByName, cubesById, locatorsById, formatVersion, bone, context); } if (bone.cubes) { // geo fv of 1.8 uses bind_pose_rotation at the bone level rather than per-cube rotation // also it manages pivot at the bone level // so "pull up" rotation -> bind_pose_rotation or create new bones if (BlockbenchModel.isLessThan110(formatVersion) && bone.cubes) { const newCubesPivot = []; // promote cubes to their own bones if they have a separate pivot than the governing bone for (const cube of bone.cubes) { let addCube = true; if (cube.pivot && bone.pivot && cube.pivot.length === 3 && bone.pivot.length === 3) { if (cube.pivot[0] === bone.pivot[0] && cube.pivot[1] === bone.pivot[1] && cube.pivot[2] === bone.pivot[2]) { cube.pivot = undefined; } else { addCube = false; context.addIndex++; const newBone = { pivot: cube.pivot, bind_pose_rotation: cube.rotation, cubes: [cube], name: cube.name ? cube.name + context.addIndex.toString() : bone.name + context.addIndex.toString(), parent: bone.name, }; cube.name = undefined; cube.pivot = undefined; cube.rotation = undefined; if (Utilities_1.default.isUsableAsObjectKey(newBone.name)) { bonesByName[newBone.name] = newBone; } } } if (addCube) { newCubesPivot.push(cube); } } const newCubesRotation = []; for (const cube of newCubesPivot) { let addCube = true; if (cube.rotation) { if (bone.bind_pose_rotation) { if (cube.rotation.length !== 3 || bone.bind_pose_rotation.length !== 3 || cube.rotation[0] !== bone.bind_pose_rotation[0] || cube.rotation[1] !== bone.bind_pose_rotation[1] || cube.rotation[2] !== bone.bind_pose_rotation[2]) { addCube = false; context.addIndex++; const newBone = { pivot: bone.pivot, bind_pose_rotation: cube.rotation, cubes: [cube], name: cube.name ? cube.name + context.addIndex.toString() : bone.name + context.addIndex.toString(), parent: bone.name, }; cube.rotation = undefined; cube.name = undefined; bonesByName[newBone.name] = newBone; } } else { bone.bind_pose_rotation = cube.rotation; cube.rotation = undefined; } } else { if ( // if the parent has a nontrivial bind pose rotation and this cube has no rotation, put it under its own bone bone.bind_pose_rotation && bone.bind_pose_rotation.length === 3 && (bone.bind_pose_rotation[0] !== 0 || bone.bind_pose_rotation[1] !== 0 || bone.bind_pose_rotation[2] !== 0)) { addCube = false; context.addIndex++; const newBone = { pivot: bone.pivot, cubes: [cube], name: cube.name ? cube.name + context.addIndex.toString() : bone.name + context.addIndex.toString(), parent: bone.name, }; cube.name = undefined; bonesByName[newBone.name] = newBone; } } if (addCube) { newCubesRotation.push(cube); } } bone.cubes = newCubesRotation; } for (const cube of bone.cubes) { cube.name = undefined; } } } } } async integrateIntoProject(project) { const modelId = this.id; if (modelId) { let geoToUpdate = undefined; let animationToUpdate = undefined; let animationFile = undefined; let modelGeometryDefinitionToUpdate = undefined; let entityTypeResourceDefinitionToUpdate = undefined; let entityTypeResourceProjectItem = undefined; // first pass: find the model name and the ETRD for (const item of project.items) { if (item.itemType === IProjectItemData_1.ProjectItemType.modelGeometryJson && geoToUpdate === undefined) { if (!item.isContentLoaded) { await item.loadContent(); } if (item.primaryFile) { const modelDefOuter = await ModelGeometryDefinition_1.default.ensureOnFile(item.primaryFile); if (modelDefOuter && modelDefOuter.definitions) { geoToUpdate = modelDefOuter.getById(modelId); modelGeometryDefinitionToUpdate = modelDefOuter; } } } else if (item.itemType === IProjectItemData_1.ProjectItemType.entityTypeResource) { // ensure references to textures if an entiy exists if (!item.isContentLoaded) { await item.loadContent(); } if (item.primaryFile) { const etrd = await EntityTypeResourceDefinition_1.default.ensureOnFile(item.primaryFile); if (etrd && etrd.geometryList) { for (const geometry of etrd.geometryList) { if (geometry === modelId) { entityTypeResourceProjectItem = item; entityTypeResourceDefinitionToUpdate = etrd; if (this.data && this.data.textures && etrd.textures) { for (const texture of this.data.textures) { let path = texture.path ? texture.path : texture.name; path = StorageUtilities_1.default.canonicalizePath(path); let hasPath = false; let hasDefault = false; const texturesIndex = path.indexOf("textures/"); if (texturesIndex >= 0) { path = path.substring(texturesIndex); path = StorageUtilities_1.default.stripExtension(path); for (const textureKey in etrd.textures) { const targetPath = etrd.textures[textureKey]; if (textureKey === "default") { hasDefault = true; } if (targetPath && StorageUtilities_1.default.isPathEqual(targetPath, path)) { hasPath = true; } } if (!hasPath) { if (!hasDefault) { etrd.textures["default"] = path; } else { etrd.textures[texture.name] = path; } } } } } } } } } } } // second pass: find the animation if we have an ETRD if (entityTypeResourceDefinitionToUpdate && entityTypeResourceProjectItem && entityTypeResourceProjectItem.childItems) { for (const item of entityTypeResourceProjectItem.childItems) { if (item.childItem.itemType === IProjectItemData_1.ProjectItemType.animationResourceJson && animationToUpdate === undefined) { if (!item.childItem.isContentLoaded) { await item.childItem.loadContent(); } if (item.childItem.primaryFile) { const animationOuter = await AnimationResourceDefinition_1.default.ensureOnFile(item.childItem.primaryFile); if (animationOuter && animationOuter.data) { animationToUpdate = animationOuter; animationFile = item.childItem.primaryFile; } } } } } // a model file doesn't exist, so let's create one. if (!geoToUpdate && project.projectFolder) { let modelName = this.data?.name; if (modelName === undefined) { modelName = modelId; const colonNamespaceSep = modelName.lastIndexOf(":"); if (colonNamespaceSep >= 0) { modelName = modelName.substring(colonNamespaceSep + 1); } } const defaultRp = await project.getDefaultResourcePackFolder(); if (defaultRp) { const modelsFolder = defaultRp.ensureFolder("models"); await modelsFolder.ensureExists(); const newFileName = await StorageUtilities_1.default.getUniqueFileName(modelName, "json", modelsFolder); const newFile = modelsFolder.ensureFile(newFileName); const modelGen = await ModelGeometryDefinition_1.default.ensureOnFile(newFile); if (modelGen) { modelGen.ensureDefault(modelId); if (modelGen.definitions.length >= 0) { geoToUpdate = modelGen.definitions[0]; modelGeometryDefinitionToUpdate = modelGen; } } } } // an animation file doesn't exist, so let's create one if we need to. if (!animationToUpdate && project.projectFolder && this.data?.animations) { let animationSetName = MinecraftUtilities_1.default.removeSubTypeExtensionFromName(this.data?.name); if (animationSetName === undefined) { animationSetName = modelId; const colonNamespaceSep = animationSetName.lastIndexOf(":"); if (colonNamespaceSep >= 0) { animationSetName = animationSetName.substring(colonNamespaceSep + 1); } } const defaultRp = await project.getDefaultResourcePackFolder(); if (defaultRp) { const animationsFolder = defaultRp.ensureFolder("animations"); await animationsFolder.ensureExists(); const newFileName = await StorageUtilities_1.default.getUniqueFileName(animationSetName, "animation.json", animationsFolder); animationFile = animationsFolder.ensureFile(newFileName); animationToUpdate = await AnimationResourceDefinition_1.default.ensureOnFile(animationFile); if (animationToUpdate) { animationToUpdate.ensureDefault(); } } } if (animationToUpdate && this.data?.animations && animationFile) { await this.updateMinecraftAnimationsFromBlockbench(animationToUpdate, entityTypeResourceDefinitionToUpdate); animationToUpdate.persist(); project.ensureItemFromFile(animationFile, IProjectItemData_1.ProjectItemType.animationResourceJson, Project_1.FolderContext.resourcePack); } if (geoToUpdate && modelGeometryDefinitionToUpdate) { const fv = modelGeometryDefinitionToUpdate?.getFormatVersion(); await this.updateGeometryFromModel(geoToUpdate, fv); modelGeometryDefinitionToUpdate?.persist(); } } if (this.data && this.data.textures) { for (const texture of this.data.textures) { let setItem = false; if (texture.name) { let path = texture.path ? texture.path : texture.name; const bytes = Utilities_1.default.base64ToUint8Array(texture.source); if (bytes && project.projectFolder) { path = StorageUtilities_1.default.canonicalizePath(path); const texturesIndex = path.indexOf("textures/"); if (texturesIndex >= 0) { path = path.substring(texturesIndex); } // first, try to match an item by its path leaf for (const item of project.items) { if (item.itemType === IProjectItemData_1.ProjectItemType.texture && !setItem) { if (!item.isContentLoaded) { await item.loadContent(); } if (item.primaryFile) { const projectPath = item.primaryFile.getFolderRelativePath(project.projectFolder); if (projectPath && projectPath.endsWith(path)) { item.primaryFile.setContent(bytes); setItem = true; } } } } // we didn't match by path, but try to match by file name? if (!setItem) { for (const item of project.items) { if (item.itemType === IProjectItemData_1.ProjectItemType.texture && !setItem) { if (item.primaryFile && item.primaryFile.name === texture.name) { item.primaryFile.setContent(bytes); setItem = true; } } } } // we didn't find a match, so create a new texture if (!setItem) { const defaultRp = await project.getDefaultResourcePackFolder(); if (defaultRp && project.projectFolder) { // the path is not standard Minecraft, let's just create a new texture path in RP if (!path.startsWith("textures/")) { path = "textures/" + texture.name; } const file = defaultRp.ensureFile(path); file.setContent(bytes); const projectPath = file.getFolderRelativePath(project.projectFolder); if (projectPath) { project.ensureItemByProjectPath(projectPath, IProjectItemData_1.ProjectItemStorageType.singleFile, file.name, IProjectItemData_1.ProjectItemType.texture, Project_1.FolderContext.resourcePack, undefined, IProjectItemData_1.ProjectItemCreationType.normal, file); } } } } } } } } static async ensureOnFile(file, loadHandler) { let bd; if (file.manager === undefined) { bd = new BlockbenchModel(); bd.file = file; file.manager = bd; } if (file.manager !== undefined && file.manager instanceof BlockbenchModel) { bd = file.manager; if (!bd.isLoaded && loadHandler) { bd.onLoaded.subscribe(loadHandler); } await bd.load(); return bd; } return bd; } async persist() { if (this._file === undefined) { return false; } Log_1.default.assert(this._data !== null, "ITDP"); if (this._data) { return this._file.setObjectContentIfSemanticallyDifferent(this._data); } return false; } async save() { if (this._file === undefined) { return; } if (await this.persist()) { await this._file.saveContent(false); } } async load() { if (this._file === undefined || this._isLoaded) { return; } if (!this._file.isContentLoaded) { await this._file.loadContent(); } if (this._file.content === null || this._file.content instanceof Uint8Array) { return; } this.id = this._file.name; this._data = StorageUtilities_1.default.getJsonObject(this._file); this._isLoaded = true; } static createEmptyModel(name, identifier) { return { meta: { format_version: "5.0", model_format: "bedrock", box_uv: true, }, name: name, model_identifier: identifier, variable_placeholder_buttons: [], variable_placeholders: "", visible_box: [1, 1, 1], bedrock_animation_mode: "entity", timeline_setups: [], unhandled_root_fields: {}, resolution: { width: 64, height: 32 }, elements: [], groups: [], outliner: [], }; } /** * Exports Minecraft animations to Blockbench format * @param animationDef The Minecraft animation resource definition to export from * @param bbmodel The Blockbench model to export animations to */ static async exportAnimationsToBlockbench(animationDef, bbmodel) { if (!animationDef.animations || !bbmodel.animations) { return; } for (const animationName in animationDef.animations) { const mcAnimation = animationDef.animations[animationName]; if (mcAnimation && mcAnimation.bones) { const bbAnimation = { uuid: Utilities_1.default.createUuid(), name: animationName, loop: mcAnimation.loop ? "loop" : "once", override: false, length: mcAnimation.animation_length || 1.0, snapping: 20, selected: false, saved: true, path: "", anim_time_update: "", blend_weight: "", loop_delay: "", animators: {}, }; // Convert Minecraft animation bones to Blockbench animators for (const boneName in mcAnimation.bones) { const mcBone = mcAnimation.bones[boneName]; if (mcBone) { const animator = { name: boneName, type: "bone", keyframes: [], }; // Process rotation keyframes if (mcBone.rotation) { this.convertKeyframesToBlockbench(mcBone.rotation, "rotation", animator); } // Process position keyframes if (mcBone.position) { this.convertKeyframesToBlockbench(mcBone.position, "position", animator); } // Process scale keyframes if (mcBone.scale) { this.convertKeyframesToBlockbench(mcBone.scale, "scale", animator); } if (animator.keyframes.length > 0) { bbAnimation.animators[boneName] = animator; } } } bbmodel.animations.push(bbAnimation); } } } /** * Converts Minecraft keyframe data to Blockbench keyframe format * @param keyframeData The Minecraft keyframe data (can be static values or time-based keyframes) * @param channel The animation channel (rotation, position, scale) * @param animator The Blockbench animator to add keyframes to */ static convertKeyframesToBlockbench(keyframeData, // Using any to handle the complex union types from Minecraft channel, animator) { if (Array.isArray(keyframeData)) { // Single static value - create a keyframe at time 0 const keyframe = { channel: channel, data_points: [ { x: keyframeData[0]?.toString() || "0", y: keyframeData[1]?.toString() || "0", z: keyframeData[2]?.toString() || "0", }, ], uuid: Utilities_1.default.createUuid(), time: 0, color: 0, interpolation: "linear", }; animator.keyframes.push(keyframe); } else if (keyframeData && typeof keyframeData === "object") { // Multiple keyframes with time codes for (const timeCode in keyframeData) { const values = keyframeData[timeCode]; if (Array.isArray(values)) { const keyframe = { channel: channel, data_points: [ { x: values[0]?.toString() || "0", y: values[1]?.toString() || "0", z: values[2]?.toString() || "0", }, ], uuid: Utilities_1.default.createUuid(), time: parseFloat(timeCode), color: 0, interpolation: "linear", }; animator.keyframes.push(keyframe); } } } } static async exportModel(modelProjectItem, modelIndex) { if (modelIndex === undefined) { modelIndex = 0; } if (!modelProjectItem.isContentLoaded) { await modelProjectItem.loadContent(); } let clientItemProjectItem = undefined; let clientItem = undefined; let serverBlockProjectItem = undefined; let serverBlock = undefined; let clientEntityProjectItem = undefined; let clientEntity = undefined; let model = undefined; if (modelProjectItem.primaryFile) { model = await ModelGeometryDefinition_1.default.ensureOnFile(modelProjectItem.primaryFile); } if (modelProjectItem.parentItems) { for (const parentItemOuter of modelProjectItem.parentItems) { if (parentItemOuter.parentItem.itemType === IProjectItemData_1.ProjectItemType.entityTypeResource) { clientEntityProjectItem = parentItemOuter.parentItem; if (clientEntityProjectItem && clientEntityProjectItem.primaryFile) { clientEntity = await EntityTypeResourceDefinition_1.default.ensureOnFile(clientEntityProjectItem.primaryFile); } } else if (parentItemOuter.parentItem.itemType === IProjectItemData_1.ProjectItemType.blockTypeBehavior) { serverBlockProjectItem = parentItemOuter.parentItem; if (serverBlockProjectItem && serverBlockProjectItem.primaryFile) { serverBlock = await BlockTypeDefinition_1.default.ensureOnFile(serverBlockProjectItem.primaryFile); } } else if (parentItemOuter.parentItem.itemType === IProjectItemData_1.ProjectItemType.attachableResourceJson) { clientItemProjectItem = parentItemOuter.parentItem; if (clientItemProjectItem && clientItemProjectItem.primaryFile) { clientItem = await AttachableResourceDefinition_1.default.ensureOnFile(clientItemProjectItem.primaryFile); } } } } if (!model || model.identifiers.length === 0 || !model.file || !model.data || model.definitions.length === 0) { return undefined; } const bbmodel = this.createEmptyModel(StorageUtilities_1.default.getBaseFromName(model.file.name), model.identifiers[modelIndex]); const textureWidth = model.getTextureWidth(modelIndex); const textureHeight = model.getTextureHeight(modelIndex); if (textureWidth !== undefined && textureHeight !== undefined) { bbmodel.resolution = { width: textureWidth, height: textureHeight, }; } const visibleBoundsWidth = model.getVisibleBoundsWidth(modelIndex); const visibleBoundsHeight = model.getVisibleBoundsHeight(modelIndex); const visibleBoundsOffset = model.getVisibleBoundsOffset(modelIndex); if (visibleBoundsWidth && visibleBoundsHeight && visibleBoundsOffset && visibleBoundsOffset.length > 1) { bbmodel.visible_box = [visibleBoundsWidth, visibleBoundsHeight, visibleBoundsOffset[1]]; } const def = model.definitions[modelIndex]; const outlinerEltsByName = {}; const groupEltsByName = {}; let colorIndex = 0; let rootBone = undefined; let hasMultipleRoots = false;