Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Particle Compatibility Improvements #6716

Merged
merged 9 commits into from
May 30, 2024
Merged
28 changes: 22 additions & 6 deletions src/main/java/ch/njol/skript/util/visual/VisualEffect.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@
import ch.njol.skript.lang.Expression;
import ch.njol.skript.lang.SkriptParser.ParseResult;
import ch.njol.skript.lang.SyntaxElement;
import ch.njol.skript.lang.util.ContextlessEvent;
import ch.njol.util.Kleenean;
import ch.njol.yggdrasil.YggdrasilSerializable;
import org.bukkit.Bukkit;
import org.bukkit.Effect;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
Expand All @@ -36,8 +39,6 @@

public class VisualEffect implements SyntaxElement, YggdrasilSerializable {

private static final boolean HAS_REDSTONE_DATA = Skript.classExists("org.bukkit.Particle$DustOptions");

private VisualEffectType type;

@Nullable
Expand All @@ -46,14 +47,29 @@ public class VisualEffect implements SyntaxElement, YggdrasilSerializable {
private float dX, dY, dZ = 0f;

public VisualEffect() {}

@SuppressWarnings({"null", "ConstantConditions"})
@Override
public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
type = VisualEffects.get(matchedPattern);

if (exprs.length > 4 && exprs[0] != null) {
data = exprs[0].getSingle(null);
if (exprs.length > 4) {
int exprCount = exprs.length - 4; // some effects might have multiple expressions
ContextlessEvent event = ContextlessEvent.get();
if (exprCount == 1) {
data = exprs[0] != null ? exprs[0].getSingle(event) : null;
} else { // provide an array of expression values
Object[] dataArray = new Object[exprCount];
for (int i = 0; i < exprCount; i++)
dataArray[i] = exprs[i] != null ? exprs[i].getSingle(event) : null;
data = dataArray;
}
}

if (parseResult.hasTag("barrierbm")) { // barrier compatibility
data = Bukkit.createBlockData(Material.BARRIER);
} else if (parseResult.hasTag("lightbm")) { // light compatibility
data = Bukkit.createBlockData(Material.LIGHT);
}

if ((parseResult.mark & 1) != 0) {
Expand Down Expand Up @@ -100,7 +116,7 @@ public void play(@Nullable Player[] ps, Location l, @Nullable Entity e, int coun
}

// Some particles use offset as RGB color codes
if (type.isColorable() && (!HAS_REDSTONE_DATA || particle != (VisualEffects.OLD_REDSTONE_PARTICLE != null ? VisualEffects.OLD_REDSTONE_PARTICLE : Particle.DUST)) && data instanceof ParticleOption) {
if (type.isColorable() && data instanceof ParticleOption) {
ParticleOption option = ((ParticleOption) data);
dX = option.getRed();
dY = option.getGreen();
Expand Down
200 changes: 119 additions & 81 deletions src/main/java/ch/njol/skript/util/visual/VisualEffects.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
package ch.njol.skript.util.visual;

import ch.njol.skript.Skript;
import ch.njol.skript.aliases.Aliases;
import ch.njol.skript.aliases.ItemType;
import ch.njol.skript.bukkitutil.ItemUtils;
import ch.njol.skript.lang.SkriptParser;
import ch.njol.skript.lang.SyntaxElementInfo;
import ch.njol.skript.localization.Language;
Expand All @@ -29,13 +29,14 @@
import ch.njol.skript.util.ColorRGB;
import ch.njol.skript.util.Direction;
import ch.njol.skript.util.SkriptColor;
import ch.njol.skript.util.Timespan;
import ch.njol.skript.variables.Variables;
import ch.njol.util.StringUtils;
import ch.njol.util.coll.iterator.SingleItemIterator;
import org.bukkit.*;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Entity;
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.Nullable;
Expand All @@ -53,7 +54,6 @@
public class VisualEffects {

private static final boolean NEW_EFFECT_DATA = Skript.classExists("org.bukkit.block.data.BlockData");
private static final boolean HAS_REDSTONE_DATA = Skript.classExists("org.bukkit.Particle$DustOptions");

private static final Map<String, Consumer<VisualEffectType>> effectTypeModifiers = new HashMap<>();
private static SyntaxElementInfo<VisualEffect> elementInfo;
Expand Down Expand Up @@ -120,37 +120,15 @@ private static void registerDataSupplier(String id, BiFunction<Object, Location,
effectTypeModifiers.put(id, consumer);
}

@Nullable
static final Particle OLD_REDSTONE_PARTICLE;
@Nullable
static final Particle OLD_ITEM_CRACK_PARTICLE;

static {
Particle oldRedstoneParticle = null;
Particle oldItemCrackParticle = null;
if (Skript.fieldExists(Particle.class, "REDSTONE")) { // initialize legacy particle support
try {
oldRedstoneParticle = Particle.valueOf("REDSTONE");
oldItemCrackParticle = Particle.valueOf("ITEM_CRACK");
} catch (IllegalArgumentException e) {
oldRedstoneParticle = null;
oldItemCrackParticle = null;
Skript.exception(e, "Failed to initialize legacy particle support. Some particles may not work as expected.");
}
}
OLD_REDSTONE_PARTICLE = oldRedstoneParticle;
OLD_ITEM_CRACK_PARTICLE = oldItemCrackParticle;
}
// only applies to some older versions where ITEM_CRACK exists
private static final boolean IS_ITEM_CRACK_MATERIAL =
Skript.fieldExists(Particle.class, "ITEM_CRACK")
&& Particle.valueOf("ITEM_CRACK").getDataType() == Material.class;

static {
Language.addListener(() -> {
if (visualEffectTypes != null) // Already registered
return;
// Colorables
registerColorable("Particle.SPELL_MOB");
registerColorable("Particle.SPELL_MOB_AMBIENT");
registerColorable("Particle.REDSTONE");
registerColorable("Particle.NOTE");

// Data suppliers
registerDataSupplier("Effect.POTION_BREAK", (raw, location) ->
Expand All @@ -161,72 +139,132 @@ private static void registerDataSupplier(String id, BiFunction<Object, Location,
return Direction.getFacing(((Direction) raw).getDirection(location), false);
});

Color defaultColor = SkriptColor.LIGHT_RED;
registerDataSupplier("Particle.SPELL_MOB", (raw, location) -> {
Color color = raw == null ? defaultColor : (Color) raw;
return new ParticleOption(color, 1);
// Useful: https://minecraft.wiki/w/Particle_format

/*
* Particles with BlockData DataType
*/
final BiFunction<Object, Location, Object> blockDataSupplier = (raw, location) -> {
if (raw instanceof Object[]) { // workaround for modern pattern since it contains a choice
Object[] data = (Object[]) raw;
raw = data[0] != null ? data[0] : data[1];
}
if (raw == null)
return Bukkit.createBlockData(Material.AIR);
if (raw instanceof ItemType)
return Bukkit.createBlockData(((ItemType) raw).getRandom().getType());
return raw;
};
registerDataSupplier("Particle.BLOCK", blockDataSupplier);
registerDataSupplier("Particle.BLOCK_CRACK", blockDataSupplier);
registerDataSupplier("Particle.BLOCK_DUST", blockDataSupplier);

registerDataSupplier("Particle.BLOCK_MARKER", blockDataSupplier);

registerDataSupplier("Particle.DUST_PILLAR", blockDataSupplier);

registerDataSupplier("Particle.FALLING_DUST", blockDataSupplier);

/*
* Particles with DustOptions DataType
*/
final Color defaultColor = SkriptColor.LIGHT_RED;
final BiFunction<Object, Location, Object> dustOptionsSupplier = (raw, location) -> {
Object[] data = (Object[]) raw;
Color color = data[0] != null ? (Color) data[0] : defaultColor;
float size = data[1] != null ? (Float) data[1] : 1;
return new Particle.DustOptions(color.asBukkitColor(), size);
};
registerDataSupplier("Particle.DUST", dustOptionsSupplier);
registerDataSupplier("Particle.REDSTONE", dustOptionsSupplier);

/*
* Particles with Color DataType
*/
registerDataSupplier("Particle.ENTITY_EFFECT", (raw, location) -> {
if (raw == null)
return defaultColor.asBukkitColor();
return ((Color) raw).asBukkitColor();
});
registerDataSupplier("Particle.SPELL_MOB_AMBIENT", (raw, location) -> {
Color color = raw == null ? defaultColor : (Color) raw;
final BiFunction<Object, Location, Object> oldColorSupplier = (raw, location) -> {
Color color = raw != null ? (Color) raw : defaultColor;
return new ParticleOption(color, 1);
};
registerColorable("Particle.SPELL_MOB");
registerDataSupplier("Particle.SPELL_MOB", oldColorSupplier);
registerColorable("Particle.SPELL_MOB_AMBIENT");
registerDataSupplier("Particle.SPELL_MOB_AMBIENT", oldColorSupplier);

final BiFunction<Object, Location, Object> itemStackSupplier = (raw, location) -> {
ItemStack itemStack = null;
if (raw instanceof ItemType)
itemStack = ((ItemType) raw).getRandom();
if (itemStack == null || ItemUtils.isAir(itemStack.getType())) // item crack air is not allowed
itemStack = new ItemStack(Material.IRON_SWORD);
if (IS_ITEM_CRACK_MATERIAL)
return itemStack.getType();
return itemStack;
};
registerDataSupplier("Particle.ITEM", itemStackSupplier);
registerDataSupplier("Particle.ITEM_CRACK", itemStackSupplier);

/*
* Particles with other DataTypes
*/
registerDataSupplier("Particle.DUST_COLOR_TRANSITION", (raw, location) -> {
Object[] data = (Object[]) raw;
Color fromColor = data[0] != null ? (Color) data[0] : defaultColor;
Color toColor = data[1] != null ? (Color) data[1] : defaultColor;
float size = data[2] != null ? (Float) data[2] : 1;
return new Particle.DustTransition(fromColor.asBukkitColor(), toColor.asBukkitColor(), size);
});
registerDataSupplier("Particle.REDSTONE", (raw, location) -> {
Color color = raw == null ? defaultColor : (Color) raw;
ParticleOption particleOption = new ParticleOption(color, 1);

if (HAS_REDSTONE_DATA && (OLD_REDSTONE_PARTICLE == null || OLD_REDSTONE_PARTICLE.getDataType() == Particle.DustOptions.class)) {
return new Particle.DustOptions(particleOption.getBukkitColor(), particleOption.size);
} else {
return particleOption;
}
});

// uses color differently
registerColorable("Particle.NOTE");
// TODO test how this works
registerDataSupplier("Particle.NOTE", (raw, location) -> {
int colorValue = (int) (((Number) raw).floatValue() * 255);
ColorRGB color = new ColorRGB(colorValue, 0, 0);
return new ParticleOption(color, 1);
});
registerDataSupplier("Particle.ITEM_CRACK", (raw, location) -> {
ItemStack itemStack = Aliases.javaItemType("iron sword").getRandom();
if (raw instanceof ItemType) {
ItemStack rand = ((ItemType) raw).getRandom();
if (rand != null)
itemStack = rand;
} else if (raw != null) {
return raw;
}

assert itemStack != null;
if (OLD_ITEM_CRACK_PARTICLE == null || OLD_ITEM_CRACK_PARTICLE.getDataType() == Material.class)
return itemStack.getType();
return itemStack;
// Float DataType, represents "the angle the particle displays at in radians"
registerDataSupplier("Particle.SCULK_CHARGE", (raw, location) -> raw != null ? raw : 0);

// Integer DataType, represents "the delay in ticks"
registerDataSupplier("Particle.SHRIEK", (raw, location) -> {
int delay = 0;
if (raw instanceof Timespan)
delay = (int) Math.min(Math.max(((Timespan) raw).getTicks(), 0), Integer.MAX_VALUE);
return delay;
});

BiFunction<Object, Location, Object> crackDustBiFunction = (raw, location) -> {
if (raw == null) {
return Material.STONE.getData();
} else if (raw instanceof ItemType) {
ItemStack rand = ((ItemType) raw).getRandom();
if (NEW_EFFECT_DATA) {
return Bukkit.createBlockData(rand != null ? rand.getType() : Material.STONE);
} else {
if (rand == null)
return Material.STONE.getData();

@SuppressWarnings("deprecation")
MaterialData type = rand.getData();
assert type != null;
return type;
}
} else {
return raw;
}
};
registerDataSupplier("Particle.BLOCK_CRACK", crackDustBiFunction);
registerDataSupplier("Particle.BLOCK_DUST", crackDustBiFunction);
registerDataSupplier("Particle.FALLING_DUST", crackDustBiFunction);
registerDataSupplier("Particle.VIBRATION", (raw, location) -> VibrationUtils.buildVibration((Object[]) raw, location));

generateTypes();
});
}

// exists to avoid NoClassDefFoundError from Vibration
private static final class VibrationUtils {
private static Vibration buildVibration(Object[] data, Location location) {
int arrivalTime = -1;
if (data[1] != null)
arrivalTime = (int) Math.min(Math.max(((Timespan) data[1]).getTicks(), 0), Integer.MAX_VALUE);
if (data[0] instanceof Entity) {
Entity entity = (Entity) data[0];
if (arrivalTime == -1)
arrivalTime = (int) (location.distance(entity.getLocation()) / 20);
//noinspection removal - new constructor only exists on newer versions
return new Vibration(location, new Vibration.Destination.EntityDestination(entity), arrivalTime);
}
// assume it's a location
Location destination = data[0] != null ? (Location) data[0] : location;
if (arrivalTime == -1)
arrivalTime = (int) (location.distance(destination) / 20);
//noinspection removal - new constructor only exists on newer versions
return new Vibration(location, new Vibration.Destination.BlockDestination(destination), arrivalTime);
}
}

}