Animator.MolangParser.context = {}; Animator.MolangParser.global_variables = { 'true': 1, 'false': 0, get 'query.delta_time'() { let time = (Date.now() - Timeline.last_frame_timecode) / 1000; if (time < 0) time += 1; return Math.clamp(time, 0, 0.1); }, get 'query.anim_time'() { return Animator.MolangParser.context.animation ? Animator.MolangParser.context.animation.time : Timeline.time; }, get 'query.life_time'() { return Timeline.time; }, get 'query.time_stamp'() { return Math.floor(Timeline.time * 20) / 20; }, get 'query.all_animations_finished'() { if (AnimationController.selected?.selected_state) { let state = AnimationController.selected?.selected_state; let state_time = state.getStateTime(); let all_finished = state.animations.allAre(a => { let animation = Animation.all.find(anim => anim.uuid == a.animation); return !animation || state_time > animation.length; }) return all_finished ? 1 : 0; } return 0; }, get 'query.any_animation_finished'() { if (AnimationController.selected?.selected_state) { let state = AnimationController.selected?.selected_state; let state_time = state.getStateTime(); let finished_anim = state.animations.find(a => { let animation = Animation.all.find(anim => anim.uuid == a.animation); return animation && state_time > animation.length; }) return finished_anim ? 1 : 0; } return 0; }, 'query.camera_rotation'(axis) { let val = cameraTargetToRotation(Preview.selected.camera.position.toArray(), Preview.selected.controls.target.toArray())[axis ? 0 : 1]; if (axis == 0) val *= -1; return val; }, 'query.rotation_to_camera'(axis) { let val = cameraTargetToRotation([0, 0, 0], Preview.selected.camera.position.toArray())[axis ? 0 : 1] ; if (axis == 0) val *= -1; return val; }, get 'query.distance_from_camera'() { return Preview.selected.camera.position.length() / 16; }, 'query.lod_index'(indices) { indices.sort((a, b) => a - b); let distance = Preview.selected.camera.position.length() / 16; let index = indices.length; indices.forEachReverse((val, i) => { if (distance < val) index = i; }) return index; }, 'query.camera_distance_range_lerp'(a, b) { let distance = Preview.selected.camera.position.length() / 16; return Math.clamp(Math.getLerp(a, b, distance), 0, 1); }, get 'query.is_first_person'() { return Project.bedrock_animation_mode == 'attachable_first' ? 1 : 0; }, get 'context.is_first_person'() { return Project.bedrock_animation_mode == 'attachable_first' ? 1 : 0; }, get 'time'() { return Timeline.time; } } Animator.MolangParser.variableHandler = function (variable, variables) { var inputs = Interface.Panels.variable_placeholders.inside_vue.text.split('\n'); var i = 0; while (i < inputs.length) { let key, val; [key, val] = inputs[i].split(/=\s*(.+)/); key = key.replace(/[\s;]/g, ''); key = key.replace(/^v\./, 'variable.').replace(/^q\./, 'query.').replace(/^t\./, 'temp.').replace(/^c\./, 'context.'); if (key === variable && val !== undefined) { val = val.trim(); if (val.match(/^(slider|toggle|impulse)\(/)) { let [type, content] = val.substring(0, val.length - 1).split(/\(/); let [id] = content.split(/\(|, */); id = id.replace(/['"]/g, ''); let button = Interface.Panels.variable_placeholders.inside_vue.buttons.find(b => b.id === id && b.type == type); return button ? parseFloat(button.value) : 0; } else { return val[0] == `'` ? val : Animator.MolangParser.parse(val, variables); } } i++; } }; (function() { let RootTokens = [ 'true', 'false', 'math.', 'query.', //'q.', 'variable.',//'v.', 'temp.', //'t.', 'context.', //'c.', 'this', 'loop()', 'return', 'break', 'continue', ] let MolangQueries = [ // common 'all_animations_finished', 'any_animation_finished', 'anim_time', 'life_time', 'yaw_speed', 'ground_speed', 'vertical_speed', 'property', 'has_property()', 'variant', 'mark_variant', 'skin_id', 'above_top_solid', 'actor_count', 'all()', 'all_tags', 'anger_level', 'any()', 'any_tag', 'approx_eq()', 'armor_color_slot', 'armor_material_slot', 'armor_texture_slot', 'average_frame_time', 'blocking', 'body_x_rotation', 'body_y_rotation', 'bone_aabb', 'bone_origin', 'bone_rotation', 'camera_distance_range_lerp', 'camera_rotation()', 'can_climb', 'can_damage_nearby_mobs', 'can_dash', 'can_fly', 'can_power_jump', 'can_swim', 'can_walk', 'cape_flap_amount', 'cardinal_facing', 'cardinal_facing_2d', 'cardinal_player_facing', 'combine_entities()', 'count', 'current_squish_value', 'dash_cooldown_progress', 'day', 'death_ticks', 'debug_output', 'delta_time', 'distance_from_camera', 'effect_emitter_count', 'effect_particle_count', 'equipment_count', 'equipped_item_all_tags', 'equipped_item_any_tag()', 'equipped_item_is_attachable', 'eye_target_x_rotation', 'eye_target_y_rotation', 'facing_target_to_range_attack', 'frame_alpha', 'get_actor_info_id', 'get_animation_frame', 'get_default_bone_pivot', 'get_locator_offset', 'get_root_locator_offset', 'had_component_group()', 'has_any_family()', 'has_armor_slot', 'has_biome_tag', 'has_block_property', 'has_cape', 'has_collision', 'has_dash_cooldown', 'has_gravity', 'has_owner', 'has_rider', 'has_target', 'head_roll_angle', 'head_x_rotation', 'head_y_rotation', 'health', 'heartbeat_interval', 'heartbeat_phase', 'heightmap', 'hurt_direction', 'hurt_time', 'in_range()', 'invulnerable_ticks', 'is_admiring', 'is_alive', 'is_angry', 'is_attached_to_entity', 'is_avoiding_block', 'is_avoiding_mobs', 'is_baby', 'is_breathing', 'is_bribed', 'is_carrying_block', 'is_casting', 'is_celebrating', 'is_celebrating_special', 'is_charged', 'is_charging', 'is_chested', 'is_critical', 'is_croaking', 'is_dancing', 'is_delayed_attacking', 'is_digging', 'is_eating', 'is_eating_mob', 'is_elder', 'is_emerging', 'is_emoting', 'is_enchanted', 'is_fire_immune', 'is_first_person', 'is_ghost', 'is_gliding', 'is_grazing', 'is_idling', 'is_ignited', 'is_illager_captain', 'is_in_contact_with_water', 'is_in_love', 'is_in_ui', 'is_in_water', 'is_in_water_or_rain', 'is_interested', 'is_invisible', 'is_item_equipped', 'is_item_name_any()', 'is_jump_goal_jumping', 'is_jumping', 'is_laying_down', 'is_laying_egg', 'is_leashed', 'is_levitating', 'is_lingering', 'is_moving', 'is_name_any()', 'is_on_fire', 'is_on_ground', 'is_on_screen', 'is_onfire', 'is_orphaned', 'is_owner_identifier_any()', 'is_persona_or_premium_skin', 'is_playing_dead', 'is_powered', 'is_pregnant', 'is_ram_attacking', 'is_resting', 'is_riding', 'is_roaring', 'is_rolling', 'is_saddled', 'is_scared', 'is_selected_item', 'is_shaking', 'is_shaking_wetness', 'is_sheared', 'is_shield_powered', 'is_silent', 'is_sitting', 'is_sleeping', 'is_sneaking', 'is_sneezing', 'is_sniffing', 'is_sonic_boom', 'is_spectator', 'is_sprinting', 'is_stackable', 'is_stalking', 'is_standing', 'is_stunned', 'is_swimming', 'is_tamed', 'is_transforming', 'is_using_item', 'is_wall_climbing', 'item_in_use_duration', 'item_is_charged', 'item_max_use_duration', 'item_remaining_use_duration', 'item_slot_to_bone_name()', 'key_frame_lerp_time', 'last_frame_time', 'last_hit_by_player', 'lie_amount', 'life_span', 'lod_index', 'log', 'main_hand_item_max_duration', 'main_hand_item_use_duration', 'max_durability', 'max_health', 'max_trade_tier', 'maximum_frame_time', 'minimum_frame_time', 'model_scale', 'modified_distance_moved', 'modified_move_speed', 'moon_brightness', 'moon_phase', 'movement_direction', 'noise', 'on_fire_time', 'out_of_control', 'player_level', 'position()', 'position_delta()', 'previous_squish_value', 'remaining_durability', 'roll_counter', 'rotation_to_camera()', 'shake_angle', 'shake_time', 'shield_blocking_bob', 'show_bottom', 'sit_amount', 'sleep_rotation', 'sneeze_counter', 'spellcolor', 'standing_scale', 'structural_integrity', 'surface_particle_color', 'surface_particle_texture_coordinate', 'surface_particle_texture_size', 'swell_amount', 'swelling_dir', 'swim_amount', 'tail_angle', 'target_x_rotation', 'target_y_rotation', 'texture_frame_index', 'time_of_day', 'time_since_last_vibration_detection', 'time_stamp', 'total_emitter_count', 'total_particle_count', 'trade_tier', 'unhappy_counter', 'walk_distance', 'wing_flap_position', 'wing_flap_speed', ]; let MolangQueryLabels = { 'in_range()': 'in_range( value, min, max )', 'all()': 'in_range( value, values... )', 'any()': 'in_range( value, values... )', 'approx_eq()': 'in_range( value, values... )', }; let DefaultContext = [ 'item_slot', 'block_face', 'cardinal_block_face_placed_on', 'is_first_person', 'owning_entity', 'player_offhand_arm_height', 'other', 'count', ]; let DefaultVariables = [ 'attack_time', 'is_first_person', ]; let MathFunctions = [ 'sin()', 'cos()', 'abs()', 'clamp()', 'pow()', 'sqrt()', 'random()', 'ceil()', 'round()', 'trunc()', 'floor()', 'mod()', 'min()', 'max()', 'exp()', 'ln()', 'lerp()', 'lerprotate()', 'pi', 'asin()', 'acos()', 'atan()', 'atan2()', 'die_roll()', 'die_roll_integer()', 'hermite_blend()', 'random_integer()', ]; let MathFunctionLabels = { 'clamp()': 'clamp( value, min, max )', 'pow()': 'pow( base, exponent )', 'random()': 'random( low, high )', 'mod()': 'mod( value, denominator )', 'min()': 'min( A, B )', 'max()': 'max( A, B )', 'lerp()': 'lerp( start, end, 0_to_1 )', 'lerprotate()': 'lerprotate( start, end, 0_to_1 )', 'atan2()': 'atan2( y, x )', 'die_roll()': 'die_roll( num, low, high )', 'die_roll_integer()': 'die_roll_integer( num, low, high )', 'random_integer()': 'random_integer( low, high )', 'hermite_blend()': 'hermite_blend( 0_to_1 )', }; function getProjectVariables(current) { let set = new Set(); let expressions = getAllMolangExpressions(); expressions.forEach(exp => { if (!exp.value) return; let matches = exp.value.match(/(v|variable)\.\w+/gi); if (!matches) return; matches.forEach(match => { let name = match.substring(match.indexOf('.')+1); if (name !== current) set.add(name); }) }) return set; } function filterAndSortList(list, match, blacklist, labels) { let result = list.filter(f => f.startsWith(match) && f.length != match.length); list.forEach(f => { if (!result.includes(f) && f.includes(match) && f.length != match.length) result.push(f); }) if (blacklist) blacklist.forEach(black => result.remove(black)); return result.map(text => {return {text, label: labels && labels[text], overlap: match.length}}) } Animator.autocompleteMolang = function(text, position, type) { let beginning = text.substring(0, position).split(/[^a-zA-Z_.]\.*/g).last(); if (!beginning) return []; beginning = beginning.toLowerCase(); if (beginning.includes('.')) { let [namespace, dir] = beginning.split('.'); if (namespace == 'math') { return filterAndSortList(MathFunctions, dir, null, MathFunctionLabels); } if (namespace == 'query' || namespace == 'q') { return filterAndSortList(MolangQueries, dir, type !== 'controller' && ['all_animations_finished', 'any_animation_finished'], MolangQueryLabels); } if (namespace == 'temp' || namespace == 't') { let temps = text.match(/([^a-z]|^)t(emp)?\.\w+/gi); if (temps) { temps = temps.map(t => t.split('.')[1]); temps = temps.filter((t, i) => t !== dir && temps.indexOf(t) === i); return filterAndSortList(temps, dir); } } if (namespace == 'context' || namespace == 'c') { return filterAndSortList(DefaultContext, dir); } if (namespace == 'variable' || namespace == 'v') { let options = [...getProjectVariables(dir)]; options.safePush(...DefaultVariables); return filterAndSortList(options, dir); } } else { let root_tokens = RootTokens.slice(); let labels = {}; if (type === 'placeholders') { labels = { 'toggle()': 'toggle( name )', 'slider()': 'slider( name, step?, min?, max? )', 'impulse()': 'impulse( name, duration )', }; root_tokens.push(...Object.keys(labels)); } return filterAndSortList(root_tokens, beginning, null, labels); } return []; } })() function getAllMolangExpressions() { let expressions = []; Animation.all.forEach(animation => { for (let key in Animation.properties) { let property = Animation.properties[key]; if (Condition(property.condition, animation) && property.type == 'molang' && animation[key] && isNaN(animation[key])) { let value = animation[key]; expressions.push({ value, type: 'animation', key, 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' && data_point[key] && isNaN(data_point[key])) { expressions.push({ value: data_point[key], type: 'keyframe', key, animation, animator, channel, kf }) } } }) }) } } }) AnimationController.all.forEach(controller => { controller.states.forEach(state => { if (state.on_entry && isNaN(state.on_entry)) { expressions.push({ value: state.on_entry, type: 'controller', controller, state }) } if (state.on_entry && isNaN(state.on_exit)) { expressions.push({ value: state.on_exit, type: 'controller', controller, state }) } state.animations.forEach(a => { if (a.blend_value && isNaN(a.blend_value)) { expressions.push({ value: a.blend_value, type: 'controller_animation', controller, state }) } }) state.transitions.forEach(t => { if (t.condition && isNaN(t.condition)) { expressions.push({ value: t.condition, type: 'controller_transition', controller, state }) } }) }) }) return expressions; } 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(/[\w.]\s+[\w.]/)) { issues.push('Two expressions with no operator in between'); } if (clear_string.match(/(^|[^a-z0-9_])[\d.]+[a-z_]+/i)) { issues.push('Invalid token ' + clear_string.match(/(^|[^a-z0-9_])[\d.]+[a-z_]+/i)[0].replace(/[^a-z0-9._]/g, '')); } if (clear_string.match(/[^\w\s+\-*/().,;:[\]!?=<>&|]/)) { issues.push('Invalid character: ' + clear_string.match(/[^\s\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] }) } } getAllMolangExpressions().forEach(ex => { if (ex.type == 'animation') { validateMolang(ex.value, `Property "${ex.key}" on animation "${ex.animation.name}" contains invalid molang:`, ex.animation); } else if (ex.type == 'keyframe') { let channel_name = ex.animator.channels[ex.channel].name; validateMolang(ex.value, `${channel_name} keyframe at ${ex.kf.time.toFixed(2)} on "${ex.animator.name}" in "${ex.animation.name}" contains invalid molang:`, ex.kf); } }) } })