src/model/modelFunctions.js
import { loadBlockState, loadJsonFromPath, loadTextureAsBase64 } from "../functions";
import merge from "deepmerge";
export function parseModel(model, modelOptions, parsedModelList, assetRoot) {
return new Promise(resolve => {
let type = "block";
let offset;
let rotation;
let scale;
if (typeof model === "string") {
let parsed = parseModelType(model);
model = parsed.model;
type = parsed.type;
parsedModelList.push({
name: model,
type: type,
options: modelOptions
});
resolve(parsedModelList);
} else if (typeof model === "object") {
if (model.hasOwnProperty("offset")) {
offset = model["offset"];
}
if (model.hasOwnProperty("rotation")) {
rotation = model["rotation"];
}
if (model.hasOwnProperty("scale")) {
scale = model["scale"];
}
if (model.hasOwnProperty("model")) {
if (model.hasOwnProperty("type")) {
type = model["type"];
model = model["model"];
} else {
let parsed = parseModelType(model["model"]);
model = parsed.model;
type = parsed.type;
}
parsedModelList.push({
name: model,
type: type,
offset: offset,
rotation: rotation,
scale: scale,
options: modelOptions
});
resolve(parsedModelList);
} else if (model.hasOwnProperty("blockstate")) {
type = "block";
loadBlockState(model.blockstate, assetRoot).then((blockstate) => {
if (blockstate.hasOwnProperty("variants")) {
if (model.hasOwnProperty("variant")) {
let variantKey = findMatchingVariant(blockstate.variants, model.variant);
if (variantKey === null) {
console.warn("Missing variant key for " + model.blockstate + ": " + model.variant);
console.warn(blockstate.variants);
resolve(null);
return;
}
let variant = blockstate.variants[variantKey];
if (!variant) {
console.warn("Missing variant for " + model.blockstate + ": " + model.variant);
resolve(null);
return;
}
let variants = [];
if (!Array.isArray(variant)) {
variants = [variant];
} else {
variants = variant;
}
rotation = [0, 0, 0];
let v = variants[Math.floor(Math.random() * variants.length)];
if (variant.hasOwnProperty("x")) {
rotation[0] = v.x;
}
if (variant.hasOwnProperty("y")) {
rotation[1] = v.y;
}
if (variant.hasOwnProperty("z")) {// Not actually used by MC, but why not?
rotation[2] = v.z;
}
let parsed = parseModelType(v.model);
parsedModelList.push({
name: parsed.model,
type: "block",
variant: model.variant,
offset: offset,
rotation: rotation,
scale: scale,
options: modelOptions
});
resolve(parsedModelList);
} else {
let variant;
if (blockstate.variants.hasOwnProperty("normal")) {
variant = blockstate.variants.normal;
} else if (blockstate.variants.hasOwnProperty("")) {
variant = blockstate.variants[""];
} else {
variant = blockstate.variants[Object.keys(blockstate.variants)[0]]
}
let variants = [];
if (!Array.isArray(variant)) {
variants = [variant];
} else {
variants = variant;
}
rotation = [0, 0, 0];
let v = variants[Math.floor(Math.random() * variants.length)];
if (variant.hasOwnProperty("x")) {
rotation[0] = v.x;
}
if (variant.hasOwnProperty("y")) {
rotation[1] = v.y;
}
if (variant.hasOwnProperty("z")) {// Not actually used by MC, but why not?
rotation[2] = v.z;
}
let parsed = parseModelType(v.model);
parsedModelList.push({
name: parsed.model,
type: "block",
variant: model.variant,
offset: offset,
rotation: rotation,
scale: scale,
options: modelOptions
})
resolve(parsedModelList);
}
} else if (blockstate.hasOwnProperty("multipart")) {
for (let j = 0; j < blockstate.multipart.length; j++) {
let cond = blockstate.multipart[j];
let apply = cond.apply;
let when = cond.when;
rotation = [0, 0, 0];
if (!when) {
if (apply.hasOwnProperty("x")) {
rotation[0] = apply.x;
}
if (apply.hasOwnProperty("y")) {
rotation[1] = apply.y;
}
if (apply.hasOwnProperty("z")) {// Not actually used by MC, but why not?
rotation[2] = apply.z;
}
let parsed = parseModelType(apply.model);
parsedModelList.push({
name: parsed.model,
type: "block",
offset: offset,
rotation: rotation,
scale: scale,
options: modelOptions
});
} else if (model.hasOwnProperty("multipart")) {
let multipartConditions = model.multipart;
let applies = false;
if (when.hasOwnProperty("OR")) {
for (let k = 0; k < when.OR.length; k++) {
if (applies) break;
for (let c in when.OR[k]) {
if (applies) break;
if (when.OR[k].hasOwnProperty(c)) {
let expected = when.OR[k][c];
let expectedArray = expected.split("|");
let given = multipartConditions[c];
for (let k = 0; k < expectedArray.length; k++) {
if (expectedArray[k] === given) {
applies = true;
break;
}
}
}
}
}
} else {
for (let c in when) {// this SHOULD be a single case, but iterating makes it a bit easier
if (applies) break;
if (when.hasOwnProperty(c)) {
let expected = String(when[c]);
let expectedArray = expected.split("|");
let given = multipartConditions[c];
for (let k = 0; k < expectedArray.length; k++) {
if (expectedArray[k] === given) {
applies = true;
break;
}
}
}
}
}
if (applies) {
if (apply.hasOwnProperty("x")) {
rotation[0] = apply.x;
}
if (apply.hasOwnProperty("y")) {
rotation[1] = apply.y;
}
if (apply.hasOwnProperty("z")) {// Not actually used by MC, but why not?
rotation[2] = apply.z;
}
let parsed = parseModelType(apply.model);
parsedModelList.push({
name: parsed.model,
type: "block",
offset: offset,
rotation: rotation,
scale: scale,
options: modelOptions
})
}
}
}
resolve(parsedModelList);
}
}).catch(()=>{
resolve(parsedModelList);
})
}
}
})
}
export function loadAndMergeModel(model, assetRoot) {
return loadModel(model.name, model.type, assetRoot)
.then(modelData => mergeParents(modelData, model.name, assetRoot));
}
// Utils
export function modelCacheKey(model) {
return model.type + "__" + model.name /*+ "[" + (model.variant || "default") + "]"*/;
}
export function findMatchingVariant(variants, selector) {
if (!Array.isArray(variants)) variants = Object.keys(variants);
if (!selector || selector === "" || selector.length === 0) return "";
let selectorObj = variantStringToObject(selector);
for (let i = 0; i < variants.length; i++) {
let variantObj = variantStringToObject(variants[i]);
let matches = true;
for (let k in selectorObj) {
if (selectorObj.hasOwnProperty(k)) {
if (variantObj.hasOwnProperty(k)) {
if (selectorObj[k] !== variantObj[k]) {
matches = false;
break;
}
}
}
}
if (matches) return variants[i];
}
return null;
}
export function variantStringToObject(str) {
let split = str.split(",");
let obj = {};
for (let i = 0; i < split.length; i++) {
let spl = split[i];
let split1 = spl.split("=");
obj[split1[0]] = split1[1];
}
return obj;
}
export function parseModelType(string) {
if (string.startsWith("block/")) {
// if (type === "item") {
// throw new Error("Tried to mix block/item models");
// }
return {
type: "block",
model: string.substr("block/".length)
}
} else if (string.startsWith("item/")) {
// if (type === "block") {
// throw new Error("Tried to mix item/block models");
// }
return {
type: "item",
model: string.substr("item/".length)
}
}
return {
type: "block",
model: "string"
}
}
export function loadModel(model, type/* block OR item */, assetRoot) {
return new Promise((resolve, reject) => {
if (typeof model === "string") {
if (model.startsWith("{") && model.endsWith("}")) {// JSON string
resolve(JSON.parse(model));
} else if (model.startsWith("http")) {// URL
fetch(model, {
mode: "cors",
redirect: "follow"
})
.then(response => response.json())
.then(data => {
console.log("model data:", data);
resolve(data);
})
} else {// model name -> use local data
loadJsonFromPath(assetRoot, "/assets/minecraft/models/" + (type || "block") + "/" + model + ".json").then((data) => {
resolve(data);
})
}
} else if (typeof model === "object") {// JSON object
resolve(model);
} else {
console.warn("Invalid model");
reject();
}
});
};
export function loadTextures(textureNames, assetRoot) {
return new Promise((resolve) => {
let promises = [];
let filteredNames = [];
let names = Object.keys(textureNames);
for (let i = 0; i < names.length; i++) {
let name = names[i];
let texture = textureNames[name];
if (texture.startsWith("#")) {// reference to another texture, no need to load
continue;
}
filteredNames.push(name);
promises.push(loadTextureAsBase64(assetRoot, "minecraft", "/", texture));
}
Promise.all(promises).then((textures) => {
let mappedTextures = {};
for (let i = 0; i < textures.length; i++) {
mappedTextures[filteredNames[i]] = textures[i];
}
// Fill in the referenced textures
for (let i = 0; i < names.length; i++) {
let name = names[i];
if (!mappedTextures.hasOwnProperty(name) && textureNames.hasOwnProperty(name)) {
let ref = textureNames[name].substr(1);
mappedTextures[name] = mappedTextures[ref];
}
}
resolve(mappedTextures);
});
})
};
export function mergeParents(model, modelName, assetRoot) {
return new Promise((resolve, reject) => {
mergeParents_(model, modelName, [], [], assetRoot, resolve, reject);
});
};
let mergeParents_ = function (model, name, stack, hierarchy, assetRoot, resolve, reject) {
stack.push(model);
if (!model.hasOwnProperty("parent") || model["parent"] === "builtin/generated" || model["parent"] === "builtin/entity") {// already at the highest parent OR we reach the builtin parent which seems to be the hardcoded stuff that's not in the json files
let merged = {};
for (let i = stack.length - 1; i >= 0; i--) {
merged = merge(merged, stack[i]);
}
hierarchy.unshift(name);
merged.hierarchy = hierarchy;
resolve(merged);
return;
}
let parent = model["parent"];
delete model["parent"];// remove the child's parent so it will be replaced by the parent's parent
hierarchy.push(parent);
loadJsonFromPath(assetRoot, "/assets/minecraft/models/" + parent + ".json").then((parentData) => {
let mergedModel = Object.assign({}, model, parentData);
mergeParents_(mergedModel, name, stack, hierarchy, assetRoot, resolve, reject);
}).catch(reject);
};
export function toRadians(angle) {
return angle * (Math.PI / 180);
}
export function deleteObjectProperties(obj) {
Object.keys(obj).forEach(function (key) {
delete obj[key];
});
}