blockbench/js/validator.js

299 lines
8.4 KiB
JavaScript
Raw Normal View History

2022-08-04 03:14:59 +08:00
const Validator = {
checks: [],
warnings: [],
errors: [],
_timeout: null,
2022-08-04 03:14:59 +08:00
validate(trigger) {
if (this._timeout) {
clearTimeout(this._timeout);
this._timeout = null;
}
2022-08-04 03:14:59 +08:00
if (!Project) return;
this._timeout = setTimeout(() => {
this._timeout = null;
Validator.warnings.empty();
Validator.errors.empty();
2022-08-04 03:14:59 +08:00
Validator.checks.forEach(check => {
try {
if (!Condition(check.condition)) return;
if (!trigger || check.update_triggers.includes(trigger)) {
check.update();
}
Validator.warnings.push(...check.warnings);
Validator.errors.push(...check.errors);
} catch (error) {
console.error(error);
}
})
}, 400)
2022-08-04 03:14:59 +08:00
},
openDialog() {
if (!Validator.dialog) {
Validator.dialog = new Dialog({
id: 'validator',
title: 'action.validator_window',
singleButton: true,
width: 800,
2022-08-04 03:14:59 +08:00
component: {
data() {return {
warnings: Validator.warnings,
errors: Validator.errors,
}},
computed: {
problems() {
this.errors.forEach(error => error.error = true);
return [...this.errors, ...this.warnings]
}
},
methods: {
getIconNode: Blockbench.getIconNode,
marked
2022-08-04 03:14:59 +08:00
},
template: `
<template>
<ul>
<li v-for="problem in problems" class="validator_dialog_problem" :class="problem.error ? 'validator_error' : 'validator_warning'" :key="problem.message">
<i class="material-icons">{{ problem.error ? 'error' : 'warning' }}</i>
<span class="markdown" v-html="marked(problem.message.replace(/\\n/g, '\\n\\n'))"></span>
2022-08-04 03:14:59 +08:00
<template v-if="problem.buttons">
<div v-for="button in problem.buttons" class="tool" :title="button.name" @click="button.click($event)">
<div class="icon_wrapper plugin_icon normal" v-html="getIconNode(button.icon, button.color).outerHTML"></div>
</div>
</div>
</li>
</ul>
</template>
`
}
});
}
Validator.dialog.show();
},
triggers: [],
updateCashedTriggers() {
Validator.triggers.empty();
Validator.checks.forEach(check => {
Validator.triggers.safePush(...check.update_triggers);
})
}
};
class ValidatorCheck {
constructor(id, options) {
this.id = id;
this.type = options.type;
this.update_triggers = options.update_triggers
? (options.update_triggers instanceof Array ? options.update_triggers : [options.update_triggers])
: [];
this.condition = options.condition;
this.run = options.run;
this.errors = [];
this.warnings = [];
Validator.checks.push(this);
Validator.updateCashedTriggers();
}
delete() {
Validator.checks.remove(this);
Validator.updateCashedTriggers();
}
update() {
this.errors.empty();
this.warnings.empty();
try {
this.run();
if (this.errors.length > 100) this.errors.splice(100);
if (this.warnings.length > 100) this.warnings.splice(100);
} catch (error) {
console.error(error);
}
}
warn(...warnings) {
this.warnings.push(...warnings);
}
fail(...errors) {
this.errors.push(...errors);
}
}
BARS.defineActions(function() {
new Action('validator_window', {
icon: 'checklist',
category: 'file',
condition: () => Project,
click() {
Validator.openDialog();
}
})
})
new ValidatorCheck('texture_names', {
condition: {formats: ['java_block']},
update_triggers: ['add_texture', 'change_texture_path'],
run() {
Texture.all.forEach(texture => {
let characters = (texture.folder + texture.name).match(/[^a-z0-9._/\\-]/)
if (characters) {
this.warn({
message: `Texture "${texture.name}" contains the following invalid characters: "${characters.join('')}"`,
buttons: [
{
name: 'Select Texture',
icon: 'mouse',
click() {
Validator.dialog.hide();
texture.select();
}
}
]
})
}
})
}
})
2022-08-10 05:14:03 +08:00
new ValidatorCheck('catmullrom_keyframes', {
condition: {features: ['animation_files']},
update_triggers: ['update_keyframe_selection'],
run() {
Animation.all.forEach(animation => {
for (let key in animation.animators) {
let animator = animation.animators[key];
if (animator instanceof BoneAnimator) {
for (let channel in animator.channels) {
if (!animator[channel] || !animator[channel].find(kf => kf.interpolation == 'catmullrom')) continue;
let keyframes = animator[channel].slice().sort((a, b) => a.time - b.time);
keyframes.forEach((kf, i) => {
if (kf.interpolation == 'catmullrom') {
if (kf.data_points.find(dp => isNaN(dp.x) || isNaN(dp.y) || isNaN(dp.z))) {
this.fail({
message: `${animator.channels[channel].name} keyframe at ${kf.time.toFixed(2)} on "${animator.name}" in "${animation.name}" contains non-numeric value. Smooth keyframes cannot contain math expressions.`,
buttons: [{
name: 'Reveal Keyframe',
icon: 'icon-keyframe',
click() {
Dialog.open.close();
kf.showInTimeline();
}
}]
2022-08-10 05:14:03 +08:00
})
}
if ((!keyframes[i-1] || keyframes[i-1].interpolation != 'catmullrom') && (!keyframes[i+1] || keyframes[i+1].interpolation != 'catmullrom')) {
this.warn({
message: `${animator.channels[channel].name} keyframe at ${kf.time.toFixed(2)} on "${animator.name}" in "${animation.name}" is not surrounded by smooth keyframes. Multiple smooth keyframes are required to create a smooth spline.`,
buttons: [{
name: 'Reveal Keyframe',
icon: 'icon-keyframe',
click() {
Dialog.open.close();
kf.showInTimeline();
}
}]
2022-08-10 05:14:03 +08:00
})
}
}
})
}
}
}
})
}
})
new ValidatorCheck('molang_syntax', {
condition: {features: ['animation_mode']},
update_triggers: ['update_keyframe_selection', 'edit_animation_properties'],
run() {
let check = this;
function validateMolang(string, message, instance) {
if (!string || typeof string !== 'string') return;
let clear_string = string.replace(/'.*'/g, '0');
let issues = [];
if (clear_string.match(/([-+*/]\s*[+*/])|(\+\s*-)/)) {
issues.push('Two directly adjacent operators');
}
if (clear_string.match(/^[+*/.,?=&<>|]/)) {
issues.push('Expression starts with an invalid character');
}
if (clear_string.match(/(?!')[a-df-z_]+\s*[-?]+\s*[a-z_]+/i)) {
issues.push('Invalid expression "' + clear_string.match(/(?!')[a-df-z_]+\s*[-?]+\s*[a-z_]+/i)[0] + '"');
}
if (clear_string.match(/[\w.]\s+[\w.]/)) {
issues.push('Two expressions with no operator in between');
}
if (clear_string.match(/[^\w\s+\-*/().,;[\]!?=<>&|]/)) {
issues.push('Invalid character: ' + clear_string.match(/[^\w+\-*/().,;[\]!?=<>&|]+/g).join(', '));
}
let left = string.match(/\(/g) || 0;
let right = string.match(/\)/g) || 0;
if (left.length !== right.length) {
issues.push('Brackets do not match');
}
if (issues.length) {
let button;
if (instance instanceof Animation) {
button = {
name: 'Edit Animation',
icon: 'movie',
click() {
Dialog.open.close();
instance.propertiesDialog();
}
}
} else {
button = {
name: 'Reveal Keyframe',
icon: 'icon-keyframe',
click() {
Dialog.open.close();
instance.showInTimeline();
}
}
}
check.fail({
message: `${message} ${issues.join('; ')}. Script: \`${string}\``,
buttons: [button]
})
}
}
Animation.all.forEach(animation => {
for (let key in Animation.properties) {
let property = Animation.properties[key];
if (Condition(property.condition, animation) && property.type == 'molang') {
let value = animation[key];
validateMolang(value, `Property "${key}" on animation "${animation.name}" contains invalid molang:`, animation);
}
}
for (let key in animation.animators) {
let animator = animation.animators[key];
for (let channel in animator.channels) {
animator[channel].forEach((kf, i) => {
kf.data_points.forEach(data_point => {
for (let key in KeyframeDataPoint.properties) {
let property = KeyframeDataPoint.properties[key];
if (Condition(property.condition, data_point) && property.type == 'molang') {
let value = data_point[key];
validateMolang(value, `${animator.channels[channel].name} keyframe at ${kf.time.toFixed(2)} on "${animator.name}" in "${animation.name}" contains invalid molang:`, kf);
}
}
})
})
}
}
})
}
})