import armorTemplate from '../resources/armorTemplate.json';
import { isEmpty, isValidPath } from './utils';
import {
GECKOLIB_MODEL_ID,
GeckoModelType, PROPERTY_FILEPATH_CACHE,
PROPERTY_MODEL_TYPE,
PROPERTY_MODID, SETTING_ALWAYS_SHOW_DISPLAY, SETTING_AUTO_PARTICLE_TEXTURE,
SETTING_REMEMBER_EXPORT_LOCATIONS
} from "./constants";
import { FormResultValue } from 'blockbench-types/generated/interface/form';
const codec = Codecs.bedrock;
// This gets automatically applied by Blockbench, we don't need to do anything with it
export const format = new ModelFormat(GECKOLIB_MODEL_ID, {
id: GECKOLIB_MODEL_ID,
icon: "view_in_ar",
name: "GeckoLib Animated Model",
description: "Animated Model for Java mods using GeckoLib",
category: "minecraft",
box_uv: true,
optional_box_uv: true,
single_texture: true,
animated_textures: true,
bone_rig: true,
centered_grid: true,
rotate_cubes: true,
locators: true,
uv_rotation: true,
select_texture_for_particles: true,
texture_mcmeta: true,
animation_files: true,
display_mode: false,
animation_mode: true,
codec: Codecs.project,
animation_codec: Codecs.bedrock.format.animation_codec,
});
// Override the new project panel to allow customisation
format.new = function () {
if (newProject(this))
return openProjectSettingsDialog();
}
/**
* Open a GeckoLib-customised project settings dialog (usually found when creating a new project, or via the File -> Project... menu item
*/
export function openProjectSettingsDialog() {
if (Project instanceof ModelProject)
return createProjectSettingsDialog(Project, createProjectSettingsForm(Project));
}
/**
* Internal function for determining the placeholder value for the model_identifier form element in dialog windows
*/
function getObjectIdPlaceholder(formResult?: { [key: string]: FormResultValue }) {
const name = formResult?.['name'] as string;
const modelType = formResult?.[PROPERTY_MODEL_TYPE] as string;
if (!name && !modelType)
return 'my_entity';
const invalidPathChar = new RegExp('[^_\\-/.a-z0-9]+', 'g')
const pseudoWhitepaceChar = new RegExp('[\\s&-]+', 'g')
if (name)
return name.toLowerCase().replace(pseudoWhitepaceChar, "_").replace(invalidPathChar, "");
switch (GeckoModelType[modelType]) {
case GeckoModelType.ENTITY: return 'my_entity'
case GeckoModelType.BLOCK: return 'my_block';
case GeckoModelType.ITEM: return 'my_item';
case GeckoModelType.ARMOR: return 'my_armor';
case GeckoModelType.OBJECT: return 'my_object';
default: return 'my_entity';
}
}
/**
* Create the Project Settings dialog form for use in both new projects and editing existing ones
*/
function createProjectSettingsForm(Project: ModelProject) {
const form = { format: { type: 'info', label: 'data.format', text: Format.name || 'unknown', description: Format.description } as FormElementOptions }
const properties = ModelProject['properties'];
const modelType = properties[PROPERTY_MODEL_TYPE];
if (modelType) {
const currentType = Project[PROPERTY_MODEL_TYPE];
form[PROPERTY_MODEL_TYPE] = {
label: modelType.label,
description: modelType["description"],
default: GeckoModelType.ENTITY.toUpperCase(),
value: typeof (currentType) === 'string' ?
GeckoModelType[currentType.toUpperCase()].toUpperCase() :
GeckoModelType.ENTITY.toUpperCase(),
placeholder: modelType["placeholder"],
type: 'select',
options: modelType["options"],
}
}
for (const key in properties) {
const property = properties[key];
if (property.exposed === false || !Condition(property.condition))
continue;
const entry = form[property.name] = {
label: property.label,
description: property["description"],
value: Project[property.name],
placeholder: property["placeholder"],
type: property.type
}
if (property.name === 'name') {
entry.label = 'Project Name'
entry.placeholder = 'My Project'
entry.description = 'The name of the Blockbench project'
}
else if (property.name === 'model_identifier') {
entry.label = 'Object ID'
entry.description = 'The registered id of the object this model represents, for exporting purposes'
entry.placeholder = getObjectIdPlaceholder()
}
switch (property.type) {
case 'boolean':
entry.type = 'checkbox'
break;
case 'string':
entry.type = 'text';
break;
default:
if (property["options"]) {
entry['options'] = property["options"];
entry.type = 'select';
}
break;
}
}
if (form['name'] && (Project.save_path || Project.export_path || Format.image_editor) && !Format['legacy_editable_file_name'])
delete form['name'];
form['uv_mode'] = {
label: 'dialog.project.default_uv_mode',
description: 'dialog.project.default_uv_mode.description',
type: 'select',
condition: Format.optional_box_uv,
options: {
face_uv: 'dialog.project.uv_mode.face_uv',
box_uv: 'dialog.project.uv_mode.box_uv',
},
value: Project.box_uv ? 'box_uv' : 'face_uv',
};
form['texture_size'] = {
label: 'dialog.project.texture_size',
type: 'vector',
dimensions: 2,
value: [Project.texture_width, Project.texture_height],
min: 1
};
return form;
}
/**
* Create the 'new project' popup dialogue for GeckoLib projects.
*
* The contents of this is mostly a copy of project.js "project_window" action declaration (Copyright Blockbench)
* Periodically check this is up-to-date with Blockbench to ensure ongoing compatibility
* @return false if the user clicks cancel, otherwise true
*/
function createProjectSettingsDialog(Project: ModelProject, form: { [formElement: string]: '_' | FormElementOptions }) {
const dialog = new Dialog({
id: 'project',
title: 'dialog.project.title',
width: 500,
form,
onConfirm: function (formResult) {
let save;
const box_uv = formResult['uv_mode'] == 'box_uv';
const texture_width = Math.clamp(formResult['texture_size'][0], 1, Infinity);
const texture_height = Math.clamp(formResult['texture_size'][1], 1, Infinity);
if (Project.box_uv != box_uv || Project.texture_width != texture_width || Project.texture_height != texture_height) {
// Adjust UV Mapping if resolution changed
if (!Project.box_uv && !box_uv && !Format['per_texture_uv_size'] && (Project.texture_width != texture_width || Project.texture_height != texture_height)) {
save = Undo.initEdit({ elements: [...Cube.all, ...Mesh.all], uv_only: true, uv_mode: true } as UndoAspects)
Cube.all.forEach(cube => {
for (const key in cube.faces) {
const uv = cube.faces[key].uv;
uv[0] *= texture_width / Project.texture_width;
uv[2] *= texture_width / Project.texture_width;
uv[1] *= texture_height / Project.texture_height;
uv[3] *= texture_height / Project.texture_height;
}
})
Mesh.all.forEach(mesh => {
for (const key in mesh.faces) {
const uv = mesh.faces[key].uv;
for (const vkey in uv) {
uv[vkey][0] *= texture_width / Project.texture_width;
uv[vkey][1] *= texture_height / Project.texture_height;
}
}
})
}
// Convert UV mode per element
if (Project.box_uv != box_uv && ((box_uv && !Cube.all.find(cube => cube['box_uv'])) || (!box_uv && !Cube.all.find(cube => !cube['box_uv'])))) {
if (!save)
save = Undo.initEdit({ elements: Cube.all, uv_only: true, uv_mode: true } as UndoAspects);
Cube.all.forEach(cube => cube.setUVMode(box_uv));
}
if (!save)
save = Undo.initEdit({ uv_mode: true });
Project.texture_width = texture_width;
Project.texture_height = texture_height;
if (Format.optional_box_uv)
Project.box_uv = box_uv;
Canvas.updateAllUVs();
updateSelection();
}
const properties = ModelProject['properties'];
for (const key in properties) {
properties[key].merge(Project, formResult);
}
Project.name = Project.name.trim();
Project.model_identifier = Project.model_identifier.trim();
if (save)
Undo.finishEdit('Change project UV settings');
Blockbench.dispatchEvent('update_project_settings', formResult);
BARS.updateConditions();
if (Project.EditSession) {
const metadata = {
texture_width: Project.texture_width,
texture_height: Project.texture_height,
box_uv: Project.box_uv
};
for (const key in properties) {
properties[key].copy(Project, metadata);
}
Project.EditSession.sendAll('change_project_meta', JSON.stringify(metadata));
}
const modelType = GeckoModelType[formResult[PROPERTY_MODEL_TYPE]]
Project[PROPERTY_MODEL_TYPE] = modelType;
if (modelType == GeckoModelType.ITEM)
Project.parent = 'builtin/entity';
if (Project.name === Format.name || Project.name === '')
Project.name = "GeckoLib " + Project[PROPERTY_MODEL_TYPE];
switch (modelType) {
case GeckoModelType.ARMOR:
if (Outliner.root.length === 0) {
Codecs.project.parse(armorTemplate, null);
}
else {
alert('Unable to generate Armor Template over an existing model. Please select Armor on a new or empty project to use this model type.')
return false;
}
break;
default:
break;
}
Format.display_mode = modelType === GeckoModelType.ITEM || settings[SETTING_ALWAYS_SHOW_DISPLAY].value as boolean;
dialog.hide();
},
onFormChange(formResult) {
try {
document.getElementById('model_identifier')['placeholder'] = getObjectIdPlaceholder(formResult)
} // eslint-disable-next-line @typescript-eslint/no-unused-vars
catch (ex) { /* empty */ }
},
})
dialog.show()
return true;
}
/**
* Export the item display json
*
* Only called for GeckoLib projects */ export function buildDisplaySettingsJson(options = {}) { if (!Project) return; const modelProperties: any = {} if (options['comment'] || settings.credit.value) modelProperties.credit = settings.credit.value modelProperties.parent = !Project.parent ? 'builtin/entity' : Project.parent; if (options['ambientocclusion'] || Project.ambientocclusion === false) modelProperties.ambientocclusion = false if (Project.texture_width !== 16 || Project.texture_height !== 16) modelProperties.texture_size = [Project.texture_width, Project.texture_height] if (options['front_gui_light'] || Project.front_gui_light) modelProperties.gui_light = 'front'; if (options['overrides'] || Project.overrides) modelProperties.overrides = Project.overrides; if (options['display'] || !isEmpty(Project.display_settings)) { const nonDefaultDisplays = {} for (const slot in DisplayMode.slots) { const perspective = DisplayMode.slots[slot] // eslint-disable-next-line no-prototype-builtins if (DisplayMode.slots.hasOwnProperty(slot) && Project.display_settings[perspective]) { const display: any = Project.display_settings[perspective].export(); if (display) nonDefaultDisplays[perspective] = display } } if (!isEmpty(nonDefaultDisplays)) modelProperties.display = nonDefaultDisplays } if ((options['textures'] || !isEmpty(Project.textures)) && Project[PROPERTY_MODID]) { for (const texture of Project.textures) { if (texture.particle || (settings[SETTING_AUTO_PARTICLE_TEXTURE].value && Object.keys(Project.textures).length === 1)) { let name = texture.name; if (name.indexOf(".png") > 0) name = name.substring(0, name.indexOf(".png")) if (!isValidPath(name)) { name = name.toLowerCase().replace(" ", "_") if (!isValidPath(name)) continue; } name = (Project[PROPERTY_MODEL_TYPE] == GeckoModelType.BLOCK ? "block/" : "item/") + name; name = Project[PROPERTY_MODID] + ":" + name modelProperties.textures = { 'particle': name }; break } } } Blockbench.export({ resource_id: 'model', type: Codecs.java_block.name, extensions: ['json'], name: Project.model_identifier ? (Project.model_identifier + ".json") : codec.fileName().replace(".geo", ""), startpath: Project[PROPERTY_FILEPATH_CACHE].display, content: JSON.stringify(modelProperties, null, 2), }, file_path => { const oldPath = Project[PROPERTY_FILEPATH_CACHE].display; Project[PROPERTY_FILEPATH_CACHE].display = settings[SETTING_REMEMBER_EXPORT_LOCATIONS].value ? file_path : undefined; if (oldPath !== Project[PROPERTY_FILEPATH_CACHE].display) Project.saved = false; }); return this; } export default codec;