/** * This file is part of Skript. * * Skript is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Skript is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Skript. If not, see <http://www.gnu.org/licenses/>. * * * Copyright 2011-2017 Peter Güttinger and contributors */ package ch.njol.skript.util; import java.util.ArrayList; import java.util.List; import org.bukkit.Bukkit; import org.bukkit.Effect; import org.bukkit.EntityEffect; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.Particle; import org.bukkit.block.BlockFace; import org.bukkit.block.data.BlockData; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.material.MaterialData; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import ch.njol.skript.Skript; import ch.njol.skript.aliases.ItemType; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.SkriptParser; import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.lang.SyntaxElement; import ch.njol.skript.lang.SyntaxElementInfo; import ch.njol.skript.localization.Language; import ch.njol.skript.localization.LanguageChangeListener; import ch.njol.skript.localization.Noun; import ch.njol.skript.variables.Variables; import ch.njol.util.Kleenean; import ch.njol.util.StringUtils; import ch.njol.util.coll.iterator.SingleItemIterator; import ch.njol.yggdrasil.YggdrasilSerializable; /** * @author Peter Güttinger */ public final class VisualEffect implements SyntaxElement, YggdrasilSerializable { private final static String LANGUAGE_NODE = "visual effects"; static final boolean newEffectData = Skript.classExists("org.bukkit.block.data.BlockData"); public static enum Type implements YggdrasilSerializable { ENDER_SIGNAL(Effect.ENDER_SIGNAL), MOBSPAWNER_FLAMES(Effect.MOBSPAWNER_FLAMES), POTION_BREAK(Effect.POTION_BREAK) { @Override public Object getData(final @Nullable Object raw, final Location l) { return new PotionEffect(raw == null ? PotionEffectType.SPEED : (PotionEffectType) raw, 1, 0); } }, SMOKE(Effect.SMOKE) { @Override public Object getData(final @Nullable Object raw, final Location l) { if (raw == null) return BlockFace.SELF; return Direction.getFacing(((Direction) raw).getDirection(l), false); // TODO allow this to not be a literal } }, HURT(EntityEffect.HURT), // Omit DEATH, because it causes client glitches WOLF_SMOKE(EntityEffect.WOLF_SMOKE), WOLF_HEARTS(EntityEffect.WOLF_HEARTS), WOLF_SHAKE(EntityEffect.WOLF_SHAKE), SHEEP_EAT("SHEEP_EAT", true), // Was once mistakenly removed from Spigot IRON_GOLEM_ROSE(EntityEffect.IRON_GOLEM_ROSE), VILLAGER_HEARTS(EntityEffect.VILLAGER_HEART), VILLAGER_ENTITY_ANGRY(EntityEffect.VILLAGER_ANGRY), VILLAGER_ENTITY_HAPPY(EntityEffect.VILLAGER_HAPPY), WITCH_MAGIC(EntityEffect.WITCH_MAGIC), ZOMBIE_TRANSFORM(EntityEffect.ZOMBIE_TRANSFORM), FIREWORK_EXPLODE(EntityEffect.FIREWORK_EXPLODE), // Spigot 2017 entity effects update (1.10+) ARROW_PARTICLES("ARROW_PARTICLES", true), RABBIT_JUMP("RABBIT_JUMP", true), LOVE_HEARTS("LOVE_HEARTS", true), SQUID_ROTATE("SQUID_ROTATE", true), ENTITY_POOF("ENTITY_POOF", true), GUARDIAN_TARGET("GUARDIAN_TARGET", true), SHIELD_BLOCK("SHIELD_BLOCK", true), SHIELD_BREAK("SHIELD_BREAK", true), ARMOR_STAND_HIT("ARMOR_STAND_HIT", true), THORNS_HURT("THORNS_HURT", true), IRON_GOLEM_SHEATH("IRON_GOLEM_SHEATH", true), TOTEM_RESURRECT("TOTEM_RESURRECT", true), HURT_DROWN("HURT_DROWN", true), HURT_EXPLOSION("HURT_EXPLOSION", true), // Particles FIREWORKS_SPARK(Particle.FIREWORKS_SPARK), CRIT(Particle.CRIT), MAGIC_CRIT(Particle.CRIT_MAGIC), POTION_SWIRL(Particle.SPELL_MOB) { @Override public boolean isColorable() { return true; } }, POTION_SWIRL_TRANSPARENT(Particle.SPELL_MOB_AMBIENT) { @Override public boolean isColorable() { return true; } }, SPELL(Particle.SPELL), INSTANT_SPELL(Particle.SPELL_INSTANT), WITCH_SPELL(Particle.SPELL_WITCH), NOTE(Particle.NOTE), PORTAL(Particle.PORTAL), FLYING_GLYPH(Particle.ENCHANTMENT_TABLE), FLAME(Particle.FLAME), LAVA_POP(Particle.LAVA), FOOTSTEP("FOOTSTEP"), // 1.13 removed SPLASH(Particle.WATER_SPLASH), PARTICLE_SMOKE(Particle.SMOKE_NORMAL), // Why separate particle... ? EXPLOSION_HUGE(Particle.EXPLOSION_HUGE), EXPLOSION_LARGE(Particle.EXPLOSION_LARGE), EXPLOSION(Particle.EXPLOSION_NORMAL), VOID_FOG(Particle.SUSPENDED_DEPTH), SMALL_SMOKE(Particle.TOWN_AURA), CLOUD(Particle.CLOUD), COLOURED_DUST(Particle.REDSTONE) { @Override public boolean isColorable() { return true; } }, SNOWBALL_BREAK(Particle.SNOWBALL), WATER_DRIP(Particle.DRIP_WATER), LAVA_DRIP(Particle.DRIP_LAVA), SNOW_SHOVEL(Particle.SNOW_SHOVEL), SLIME(Particle.SLIME), HEART(Particle.HEART), ANGRY_VILLAGER(Particle.VILLAGER_ANGRY), HAPPY_VILLAGER(Particle.VILLAGER_HAPPY), LARGE_SMOKE(Particle.SMOKE_LARGE), ITEM_CRACK(Particle.ITEM_CRACK) { @Override public Object getData(final @Nullable Object raw, final Location l) { if (raw == null) return Material.IRON_SWORD; else if (raw instanceof ItemType) { ItemStack rand = ((ItemType) raw).getRandom(); if (rand == null) return Material.IRON_SWORD; Material type = rand.getType(); assert type != null; return type; } else { return raw; } } }, BLOCK_BREAK(Particle.BLOCK_CRACK) { @Override public Object getData(final @Nullable Object raw, final Location l) { if (raw == null) return Material.STONE.getData(); else if (raw instanceof ItemType) { if (newEffectData) { ItemStack rand = ((ItemType) raw).getRandom(); if (rand == null) return Bukkit.createBlockData(Material.STONE); return Bukkit.createBlockData(rand.getType()); } else { ItemStack rand = ((ItemType) raw).getRandom(); if (rand == null) return Material.STONE.getData(); @SuppressWarnings("deprecation") MaterialData type = rand.getData(); assert type != null; return type; } } else { return raw; } } }, BLOCK_DUST(Particle.BLOCK_DUST) { @Override public Object getData(final @Nullable Object raw, final Location l) { if (raw == null) return Material.STONE.getData(); else if (raw instanceof ItemType) { if (newEffectData) { ItemStack rand = ((ItemType) raw).getRandom(); if (rand == null) return Bukkit.createBlockData(Material.STONE); return Bukkit.createBlockData(rand.getType()); } else { ItemStack rand = ((ItemType) raw).getRandom(); if (rand == null) return Material.STONE.getData(); @SuppressWarnings("deprecation") MaterialData type = rand.getData(); assert type != null; return type; } } else { return raw; } } }, // 1.11 particles TOTEM("TOTEM"), SPIT("SPIT"), // 1.13 particles SQUID_INK("SQUID_INK"), BUBBLE_POP("BUBBLE_POP"), CURRENT_DOWN("CURRENT_DOWN"), BUBBLE_COLUMN_UP("BUBBLE_COLUMN_UP"), NAUTILUS("NAUTILUS"), DOLPHIN("DOLPHIN"), // 1.14 particles SNEEZE("SNEEZE"), CAMPFIRE_COSY_SMOKE("CAMPFIRE_COSY_SMOKE"), CAMPFIRE_SIGNAL_SMOKE("CAMPFIRE_SIGNAL_SMOKE"), COMPOSTER("COMPOSTER"), FLASH("FLASH"), FALLING_LAVA("FALLING_LAVA"), LANDING_LAVA("LANDING_LAVA"), FALLING_WATER("FALLING_WATER"), // 1.15 particles DRIPPING_HONEY("DRIPPING_HONEY"), FALLING_HONEY("FALLING_HONEY"), LANDING_HONEY("LANDING_HONEY"), FALLING_NECTAR("FALLING_NECTAR"); @Nullable final Object effect; @Nullable final String name; private Type(final Effect effect) { this.effect = effect; this.name = effect.name(); } private Type(final EntityEffect effect) { this.effect = effect; this.name = null; } private Type(final Particle effect) { this.effect = effect; this.name = null; } private Type(final String name) { this(name, false); } private Type(final String name, boolean entityEffect) { this.name = null; if (entityEffect) { EntityEffect real = null; try { real = EntityEffect.valueOf(name); } catch (IllegalArgumentException e) { // This Spigot version is idiotic } this.effect = real; } else { Particle real = null; try { real = Particle.valueOf(name); } catch (IllegalArgumentException e) { // This MC version does not support this particle... } this.effect = real; } } /** * Converts the data from the pattern to the data required by Bukkit */ @Nullable public Object getData(final @Nullable Object raw, final Location l) { assert raw == null; return null; } /** * Checks if this effect has color support. */ public boolean isColorable() { return false; } /** * Gets Minecraft name of the effect, if it exists. * @return Name or null if effect uses numeric id instead. */ @Nullable public String getMinecraftName() { return this.name; } } private final static String TYPE_ID = "VisualEffect.Type"; static { Variables.yggdrasil.registerSingleClass(Type.class, TYPE_ID); Variables.yggdrasil.registerSingleClass(Effect.class, "Bukkit_Effect"); Variables.yggdrasil.registerSingleClass(EntityEffect.class, "Bukkit_EntityEffect"); } @Nullable static SyntaxElementInfo<VisualEffect> info; final static List<Type> types = new ArrayList<>(Type.values().length); final static Noun[] names = new Noun[Type.values().length]; static { Language.addListener(new LanguageChangeListener() { @Override public void onLanguageChange() { final Type[] ts = Type.values(); types.clear(); final List<String> patterns = new ArrayList<>(ts.length); for (int i = 0; i < ts.length; i++) { final String node = LANGUAGE_NODE + "." + ts[i].name(); final String pattern = Language.get_(node + ".pattern"); if (pattern == null) { if (Skript.testing()) Skript.warning("Missing pattern at '" + (node + ".pattern") + "' in the " + Language.getName() + " language file"); } else { types.add(ts[i]); if (ts[i].isColorable()) patterns.add(pattern); else { String dVarExpr = Language.get_(LANGUAGE_NODE + ".area_expression"); if (dVarExpr == null) dVarExpr = ""; patterns.add(pattern + " " + dVarExpr); } } if (names[i] == null) names[i] = new Noun(node + ".name"); } final String[] ps = patterns.toArray(new String[patterns.size()]); assert ps != null; info = new SyntaxElementInfo<>(ps, VisualEffect.class, getOriginPath(VisualEffect.class)); } }); } static String getOriginPath(Class<VisualEffect> c){ String path = c.getName(); if (path != null){ return path; } return "<null>"; } private Type type; @Nullable private Object data; private float speed = 0; private float dX, dY, dZ = 0; /** * For parsing & deserialisation */ @SuppressWarnings("null") public VisualEffect() {} @SuppressWarnings("null") @Override public boolean init(final Expression<?>[] exprs, final int matchedPattern, final Kleenean isDelayed, final ParseResult parseResult) { type = types.get(matchedPattern); if (type.effect == null) { Skript.error("Minecraft " + Skript.getMinecraftVersion() + " version does not support particle " + type); return false; } if (type.isColorable()) { for (Expression<?> expr : exprs) { if (expr == null) continue; else if (expr.getReturnType().isAssignableFrom(Color.class)) { org.bukkit.Color color = ((Color) expr.getSingle(null)).asBukkitColor(); /* * Colored particles use dX, dY and dZ as RGB values which * have range from 0 to 1. * * For now, only speed exactly 1 is allowed. */ dX = color.getRed() / 255.0f + 0.00001f; dY = color.getGreen() / 255.0f; dZ = color.getBlue() / 255.0f; speed = 1; } else { Skript.exception("Color not specified for colored particle"); } } } else { int numberParams = 0; for (Expression<?> expr : exprs) { if (expr.getReturnType() == Long.class || expr.getReturnType() == Integer.class || expr.getReturnType() == Number.class) numberParams++; } int dPos = 0; // Data index Expression<?> expr = exprs[0]; if (expr.getReturnType() != Long.class && expr.getReturnType() != Integer.class && expr.getReturnType() != Number.class) { dPos = 1; data = exprs[0].getSingle(null); } if (numberParams == 1) // Only speed speed = ((Number) exprs[dPos].getSingle(null)).floatValue(); else if (numberParams == 3) { // Only dX, dY, dZ dX = ((Number) exprs[dPos].getSingle(null)).floatValue(); dY = ((Number) exprs[dPos + 1].getSingle(null)).floatValue(); dZ = ((Number) exprs[dPos + 2].getSingle(null)).floatValue(); } else if (numberParams == 4){ // Both present dX = ((Number) exprs[dPos].getSingle(null)).floatValue(); dY = ((Number) exprs[dPos + 1].getSingle(null)).floatValue(); dZ = ((Number) exprs[dPos + 2].getSingle(null)).floatValue(); speed = ((Number) exprs[dPos + 3].getSingle(null)).floatValue(); } } return true; } /** * Entity effects are always played on entities. * @return If this is an entity effect. */ public boolean isEntityEffect() { return type.effect instanceof EntityEffect; } /** * Particles are implemented differently from traditional effects in * Minecraft. Most new visual effects are particles. * @return If this is a particle effect. */ public boolean isParticle() { return type.effect instanceof Particle; } @Nullable public static VisualEffect parse(final String s) { final SyntaxElementInfo<VisualEffect> info = VisualEffect.info; if (info == null) return null; return SkriptParser.parseStatic(Noun.stripIndefiniteArticle(s), new SingleItemIterator<>(info), null); } public void play(final @Nullable Player[] ps, final Location l, final @Nullable Entity e) { play(ps, l, e, 0, 32); } public void play(final @Nullable Player[] ps, final Location l, final @Nullable Entity e, final int count, final int radius) { assert e == null || l.equals(e.getLocation()); if (isEntityEffect()) { if (e != null) { assert type.effect != null; e.playEffect((EntityEffect) type.effect); } } else { if (type.effect instanceof Particle) { // Particle effect... Particle particle = (Particle) type.effect; assert particle != null; Object pData = type.getData(data, l); assert type.effect != null; // Check that data has correct type (otherwise bad things will happen) if (pData != null && !((Particle) type.effect).getDataType().isAssignableFrom(pData.getClass())) { pData = null; if (Skript.debug()) Skript.warning("Incompatible particle data, resetting it!"); } if (ps == null) { // Colored particles must be played one at time; otherwise, colors are broken if (type.isColorable()) { for (int i = 0; i < count; i++) { l.getWorld().spawnParticle(particle, l, 0, dX, dY, dZ, speed, pData); } } else { l.getWorld().spawnParticle(particle, l, count, dX, dY, dZ, speed, pData); } } else { for (final Player p : ps) { if (type.isColorable()) { for (int i = 0; i < count; i++) { p.spawnParticle(particle, l, 0, dX, dY, dZ, speed, pData); } } else { p.spawnParticle(particle, l, count, dX, dY, dZ, speed, pData); } } } } else { // Non-particle effect (whatever Spigot API says, there are a few) Effect effect = (Effect) type.effect; assert effect != null; if (ps == null) { //l.getWorld().spigot().playEffect(l, (Effect) type.effect, 0, 0, dX, dY, dZ, speed, count, radius); l.getWorld().playEffect(l, effect, 0, radius); } else { for (final Player p : ps) p.playEffect(l, effect, type.getData(data, l)); } } } } @Override public String toString() { return toString(0); } public String toString(final int flags) { return names[type.ordinal()].toString(flags); } public static String getAllNames() { return StringUtils.join(names, ", "); } /** * Gets Bukkit effect backing this visual effect. It may be either * {@link Effect}, {@link EntityEffect} or {@link Particle}. * @return Backing effect. * @throws IllegalStateException When this is called before the effect * is initialized. Note that * {@link #init(Expression[], int, Kleenean, ParseResult)} may fail when * the used Minecraft version lacks support for effect used. */ public Object getEffect() { Object effect = type.effect; if (effect == null) { // init() not called or returned false throw new IllegalStateException("effect not initialized"); } return effect; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + type.hashCode(); final Object d = data; result = prime * result + ((d == null) ? 0 : d.hashCode()); return result; } @Override public boolean equals(final @Nullable Object obj) { if (this == obj) return true; if (obj == null) return false; if (!(obj instanceof VisualEffect)) return false; final VisualEffect other = (VisualEffect) obj; if (type != other.type) return false; final Object d = data; if (d == null) { if (other.data != null) return false; } else if (!d.equals(other.data)) { return false; } return true; } }