@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
1,011 lines (1,010 loc) • 75 kB
JavaScript
"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;