diff --git a/plugin/src/main/java/org/battleplugins/arena/config/ArenaConfigParser.java b/plugin/src/main/java/org/battleplugins/arena/config/ArenaConfigParser.java index ba282dda..7196ed9f 100644 --- a/plugin/src/main/java/org/battleplugins/arena/config/ArenaConfigParser.java +++ b/plugin/src/main/java/org/battleplugins/arena/config/ArenaConfigParser.java @@ -414,7 +414,14 @@ private static List parseList(@Nullable Path sourceFile, Object instance List objectList = new ArrayList<>(list.size()); // Get the primitive type of the list - Class listType = (Class) ((ParameterizedType) genericType).getActualTypeArguments()[0]; + Class listType; + Type[] types = ((ParameterizedType) genericType).getActualTypeArguments(); + if (types[0] instanceof ParameterizedType) { + listType = (Class) ((ParameterizedType) types[0]).getRawType(); + } else { + listType = (Class) types[0]; + } + if (OBJECT_PROVIDERS.containsKey(listType)) { Parser objectProvider = OBJECT_PROVIDERS.get(listType); for (Object object : list) { diff --git a/plugin/src/main/java/org/battleplugins/arena/config/ColorParser.java b/plugin/src/main/java/org/battleplugins/arena/config/ColorParser.java new file mode 100644 index 00000000..c9075c64 --- /dev/null +++ b/plugin/src/main/java/org/battleplugins/arena/config/ColorParser.java @@ -0,0 +1,43 @@ +package org.battleplugins.arena.config; + +import java.awt.Color; + +public class ColorParser implements ArenaConfigParser.Parser { + + @Override + public Color parse(Object object) throws ParseException { + if (!(object instanceof String value)) { + throw new ParseException("Color must be a string!") + .context("Provided color", object == null ? "null" : object.toString()) + .cause(ParseException.Cause.INVALID_TYPE) + .type(this.getClass()) + .userError(); + } + + return deserializeSingular(value); + } + + public static org.bukkit.Color deserializeSingularBukkit(String contents) throws ParseException { + Color color = deserializeSingular(contents); + return org.bukkit.Color.fromRGB(color.getRed(), color.getGreen(), color.getBlue()); + } + + public static Color deserializeSingular(String contents) throws ParseException { + if (contents.startsWith("#")) { + return Color.decode(contents); + } else if (contents.contains(",")) { + String[] split = contents.split(","); + if (split.length != 3) { + throw new ParseException("Color must have 3 values!") + .context("Provided color", contents) + .context("Expected format", "r,g,b") + .cause(ParseException.Cause.INVALID_VALUE) + .userError(); + } + + return new Color(Integer.parseInt(split[0]), Integer.parseInt(split[1]), Integer.parseInt(split[2])); + } else { + return Color.getColor(contents); + } + } +} diff --git a/plugin/src/main/java/org/battleplugins/arena/config/CustomEffectParser.java b/plugin/src/main/java/org/battleplugins/arena/config/CustomEffectParser.java new file mode 100644 index 00000000..b07dedd4 --- /dev/null +++ b/plugin/src/main/java/org/battleplugins/arena/config/CustomEffectParser.java @@ -0,0 +1,95 @@ +package org.battleplugins.arena.config; + +import org.battleplugins.arena.util.CustomEffect; +import org.bukkit.configuration.ConfigurationSection; + +import java.util.HashMap; +import java.util.Map; + +public class CustomEffectParser> implements ArenaConfigParser.Parser { + + @SuppressWarnings("unchecked") + @Override + public T parse(Object object) throws ParseException { + if (object instanceof String string) { + return (T) deserializeSingular(string); + } + + if (object instanceof ConfigurationSection section) { + return (T) deserializeNode(section); + } + + throw new ParseException("Invalid CustomEffect for object: " + object) + .cause(ParseException.Cause.INVALID_TYPE) + .type(this.getClass()) + .userError(); + } + + public static CustomEffect deserializeSingular(String contents) throws ParseException { + CustomEffect.EffectType type; + + SingularValueParser.ArgumentBuffer buffer = SingularValueParser.parseNamed(contents, SingularValueParser.BraceStyle.CURLY, ';'); + if (!buffer.hasNext()) { + throw new ParseException("No data found for CustomEffect") + .cause(ParseException.Cause.INVALID_TYPE) + .type(CustomEffectParser.class) + .userError(); + } + + SingularValueParser.Argument root = buffer.pop(); + if (root.key().equals("root")) { + type = CustomEffect.EffectType.get(root.value()); + if (type == null) { + throw new ParseException("Invalid CustomEffect type: " + root.value()) + .cause(ParseException.Cause.INVALID_TYPE) + .type(CustomEffectParser.class) + .userError(); + } + } else { + throw new ParseException("Invalid CustomEffect root tag " + root.key()) + .cause(ParseException.Cause.INTERNAL_ERROR) + .type(CustomEffectParser.class); + } + + Map data = new HashMap<>(); + while (buffer.hasNext()) { + SingularValueParser.Argument argument = buffer.pop(); + data.put(argument.key(), argument.value()); + } + + CustomEffect customEffect = type.create(builder -> { }); + customEffect.deserialize(data); + return customEffect; + } + + private static CustomEffect deserializeNode(ConfigurationSection section) throws ParseException { + String type = section.getString("type"); + if (type == null) { + throw new ParseException("No type found for CustomEffect") + .cause(ParseException.Cause.INVALID_TYPE) + .type(CustomEffectParser.class) + .userError(); + } + + CustomEffect.EffectType effectType = CustomEffect.EffectType.get(type); + if (effectType == null) { + throw new ParseException("Invalid CustomEffect type: " + type) + .cause(ParseException.Cause.INVALID_TYPE) + .type(CustomEffectParser.class) + .userError(); + } + + Map data = new HashMap<>(); + for (String key : section.getKeys(false)) { + if (key.equals("type")) { + continue; + } + + data.put(key, section.getString(key)); + } + + CustomEffect customEffect = effectType.create(builder -> { }); + customEffect.deserialize(data); + return customEffect; + } +} diff --git a/plugin/src/main/java/org/battleplugins/arena/config/DefaultParsers.java b/plugin/src/main/java/org/battleplugins/arena/config/DefaultParsers.java index a3b3f798..842e4322 100644 --- a/plugin/src/main/java/org/battleplugins/arena/config/DefaultParsers.java +++ b/plugin/src/main/java/org/battleplugins/arena/config/DefaultParsers.java @@ -10,6 +10,7 @@ import org.battleplugins.arena.config.context.OptionContextProvider; import org.battleplugins.arena.config.context.PhaseContextProvider; import org.battleplugins.arena.config.context.VictoryConditionContextProvider; +import org.battleplugins.arena.util.CustomEffect; import org.battleplugins.arena.util.IntRange; import org.battleplugins.arena.util.PositionWithRotation; import org.bukkit.Bukkit; @@ -70,29 +71,7 @@ static void register() { } }); - ArenaConfigParser.registerProvider(Color.class, configValue -> { - if (!(configValue instanceof String value)) { - return null; - } - - if (value.startsWith("#")) { - return Color.decode(value); - } else if (value.contains(",")) { - String[] split = value.split(","); - if (split.length != 3) { - throw new ParseException("Color must have 3 values!") - .context("Provided color", value) - .context("Expected format", "r,g,b") - .cause(ParseException.Cause.INVALID_VALUE) - .userError(); - } - - return new Color(Integer.parseInt(split[0]), Integer.parseInt(split[1]), Integer.parseInt(split[2])); - } else { - return Color.getColor(value); - } - }); - + ArenaConfigParser.registerProvider(Color.class, new ColorParser()); ArenaConfigParser.registerProvider(Component.class, configValue -> { if (!(configValue instanceof String value)) { return null; @@ -102,6 +81,7 @@ static void register() { }); ArenaConfigParser.registerProvider(BlockData.class, parseString(Bukkit::createBlockData)); + ArenaConfigParser.registerProvider(CustomEffect.class, new CustomEffectParser<>()); } private static ArenaConfigParser.Parser parseString(Function parser) { diff --git a/plugin/src/main/java/org/battleplugins/arena/config/ItemStackParser.java b/plugin/src/main/java/org/battleplugins/arena/config/ItemStackParser.java index 4364d2c0..a50ce3b5 100644 --- a/plugin/src/main/java/org/battleplugins/arena/config/ItemStackParser.java +++ b/plugin/src/main/java/org/battleplugins/arena/config/ItemStackParser.java @@ -15,7 +15,6 @@ import org.bukkit.inventory.meta.PotionMeta; import org.bukkit.potion.PotionEffect; -import java.awt.Color; import java.util.Arrays; import java.util.HashSet; import java.util.List; @@ -78,9 +77,9 @@ public static ItemStack deserializeSingular(String contents) throws ParseExcepti switch (key) { case "color" -> { if (itemMeta instanceof ColorableArmorMeta colorMeta) { - colorMeta.setColor(org.bukkit.Color.fromRGB(Color.getColor(value).getRGB())); + colorMeta.setColor(ColorParser.deserializeSingularBukkit(value)); } else if (itemMeta instanceof PotionMeta potionMeta) { - potionMeta.setColor(org.bukkit.Color.fromRGB(Color.getColor(value).getRGB())); + potionMeta.setColor(ColorParser.deserializeSingularBukkit(value)); } } case "custom-model-data" -> { @@ -169,9 +168,9 @@ private static ItemStack deserializeNode(ConfigurationSection section) throws Pa switch (meta) { case "color": { if (itemMeta instanceof ColorableArmorMeta colorMeta) { - colorMeta.setColor(org.bukkit.Color.fromRGB(Color.getColor(section.getString(meta)).getRGB())); + colorMeta.setColor(ColorParser.deserializeSingularBukkit(section.getString(meta))); } else if (itemMeta instanceof PotionMeta potionMeta) { - potionMeta.setColor(org.bukkit.Color.fromRGB(Color.getColor(section.getString(meta)).getRGB())); + potionMeta.setColor(ColorParser.deserializeSingularBukkit(section.getString(meta))); } } case "custom-model-data": { diff --git a/plugin/src/main/java/org/battleplugins/arena/util/CustomEffect.java b/plugin/src/main/java/org/battleplugins/arena/util/CustomEffect.java new file mode 100644 index 00000000..0e3c1233 --- /dev/null +++ b/plugin/src/main/java/org/battleplugins/arena/util/CustomEffect.java @@ -0,0 +1,386 @@ +package org.battleplugins.arena.util; + +import org.battleplugins.arena.config.ColorParser; +import org.battleplugins.arena.config.DurationParser; +import org.battleplugins.arena.config.ItemStackParser; +import org.battleplugins.arena.config.ParseException; +import org.bukkit.Bukkit; +import org.bukkit.Color; +import org.bukkit.FireworkEffect; +import org.bukkit.Location; +import org.bukkit.Particle; +import org.bukkit.Vibration; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Firework; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.FireworkMeta; +import org.bukkit.util.Vector; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * A utility for playing and creating custom effects. + * + * @param the builder type + */ +public interface CustomEffect { + + /** + * Gets the type of the effect. + * + * @return the type of the effect + */ + EffectType getType(); + + /** + * Plays the effect at the given location. + * + * @param location the location to play the effect + */ + void play(Location location); + + /** + * Plays the effect at the given entity. + * + * @param entity the entity to play the effect + */ + void play(Entity entity); + + /** + * Deserializes the data into the effect. + * + * @param data the data to deserialize + */ + void deserialize(Map data) throws ParseException; + + static CustomEffect firework(Consumer builder) { + FireworkEffect.Builder fireworkBuilder = FireworkEffect.builder(); + builder.accept(fireworkBuilder); + + return new CustomEffect<>() { + + @Override + public EffectType getType() { + return EffectType.FIREWORK; + } + + @Override + public void play(Location location) { + location.getWorld().spawn(location, Firework.class, firework -> { + FireworkMeta meta = firework.getFireworkMeta(); + meta.addEffect(fireworkBuilder.build()); + firework.setFireworkMeta(meta); + }).detonate(); + } + + @Override + public void play(Entity entity) { + this.play(entity.getLocation()); + } + + @Override + public void deserialize(Map data) throws ParseException { + if (!data.containsKey("firework-type")) { + throw new IllegalArgumentException("FireworkEffect type must be set"); + } + + FireworkEffect.Type type = FireworkEffect.Type.valueOf(data.get("firework-type").toUpperCase(Locale.ROOT)); + fireworkBuilder.with(type); + for (Map.Entry entry : data.entrySet()) { + if (entry.getKey().equals("firework-type")) { + continue; + } + + String key = entry.getKey(); + String value = entry.getValue(); + + switch (key) { + case "flicker" -> fireworkBuilder.flicker(Boolean.parseBoolean(value)); + case "trail" -> fireworkBuilder.trail(Boolean.parseBoolean(value)); + case "colors" -> { + List colorStrings = CustomEffect.getList(value); + List colors = new ArrayList<>(); + for (String colorString : colorStrings) { + colors.add(ColorParser.deserializeSingularBukkit(colorString)); + } + + fireworkBuilder.withColor(colors); + } + case "fade-colors" -> { + List colorStrings = CustomEffect.getList(value); + List colors = new ArrayList<>(); + for (String colorString : colorStrings) { + colors.add(ColorParser.deserializeSingularBukkit(colorString)); + } + + fireworkBuilder.withFade(colors); + } + } + } + } + }; + } + + static CustomEffect particle(Consumer builder) { + ParticleBuilder particleBuilder = new ParticleBuilder(); + builder.accept(particleBuilder); + + return new CustomEffect<>() { + + @Override + public EffectType getType() { + return EffectType.PARTICLE; + } + + @Override + public void play(Location location) { + if (particleBuilder.particle == null) { + throw new IllegalStateException("Particle must be set before playing the effect"); + } + + location.getWorld().spawnParticle(particleBuilder.particle, location, particleBuilder.count, particleBuilder.offset.getX(), particleBuilder.offset.getY(), particleBuilder.offset.getZ(), particleBuilder.speed, particleBuilder.data); + } + + @Override + public void play(Entity entity) { + this.play(entity.getLocation()); + } + + @Override + public void deserialize(Map data) throws ParseException { + if (!data.containsKey("particle")) { + throw new IllegalArgumentException("Particle must be set"); + } + + Particle particle = Particle.valueOf(data.get("particle").toUpperCase(Locale.ROOT)); + particleBuilder.particle(particle); + + for (Map.Entry entry : data.entrySet()) { + if (entry.getKey().equals("particle")) { + continue; + } + + String key = entry.getKey(); + String value = entry.getValue(); + + switch (key) { + case "speed" -> particleBuilder.speed(Double.parseDouble(value)); + case "count" -> particleBuilder.count(Integer.parseInt(value)); + case "offset" -> { + String[] split = value.split(","); + if (split.length != 3) { + throw new IllegalArgumentException("Offset must have 3 values!"); + } + + particleBuilder.offset(new Vector(Double.parseDouble(split[0]), Double.parseDouble(split[1]), Double.parseDouble(split[2]))); + } + case "data" -> { + switch (particle) { + case REDSTONE -> { + String[] split = value.split(","); + if (split.length != 3) { + throw new IllegalArgumentException("Data must have 3 values for REDSTONE particles"); + } + + particleBuilder.data(new Particle.DustOptions(Color.fromRGB(Integer.parseInt(split[0]), Integer.parseInt(split[1]), Integer.parseInt(split[2])), 1)); + } + case ITEM_CRACK -> { + ItemStack item = ItemStackParser.deserializeSingular(value); + particleBuilder.data(item); + } + case BLOCK_CRACK, FALLING_DUST, BLOCK_MARKER -> { + BlockData blockData; + try { + blockData = Bukkit.createBlockData(value); + } catch (IllegalArgumentException e) { + throw new ParseException("Invalid BlockData: " + value) + .cause(ParseException.Cause.INVALID_TYPE) + .userError(); + } + particleBuilder.data(blockData); + } + case DUST_COLOR_TRANSITION -> { + List colorStrings = CustomEffect.getList(value); + if (colorStrings.size() != 3) { + throw new IllegalArgumentException("Data must have 3 values"); + } + + Color fromColor = ColorParser.deserializeSingularBukkit(colorStrings.get(0)); + Color toColor = ColorParser.deserializeSingularBukkit(colorStrings.get(1)); + float size = Float.parseFloat(colorStrings.get(2)); + + particleBuilder.data(new Particle.DustTransition(fromColor, toColor, size)); + } + case VIBRATION -> throw new UnsupportedOperationException("Vibration particles are not supported"); + case SCULK_CHARGE -> particleBuilder.data(Float.parseFloat(value)); + case SHRIEK -> particleBuilder.data(Integer.parseInt(value)); + } + } + } + } + } + }; + } + + static CustomEffect freeze(Consumer builder) { + FreezeBuilder freezeBuilder = new FreezeBuilder(); + builder.accept(freezeBuilder); + + return new CustomEffect<>() { + + @Override + public EffectType getType() { + return EffectType.FREEZE; + } + + @Override + public void play(Location location) { + location.getWorld().getNearbyEntities(location, freezeBuilder.radius, freezeBuilder.radius, freezeBuilder.radius).forEach(entity -> { + entity.setFreezeTicks(entity.getFreezeTicks() + (int) (freezeBuilder.duration.toMillis() / 50)); + }); + } + + @Override + public void play(Entity entity) { + entity.setFreezeTicks(entity.getFreezeTicks() + (int) (freezeBuilder.duration.toMillis() / 50)); + } + + @Override + public void deserialize(Map data) throws ParseException { + if (!data.containsKey("duration")) { + throw new IllegalArgumentException("Freeze duration must be set"); + } + + freezeBuilder.duration(DurationParser.deserializeSingular(data.get("duration"))); + freezeBuilder.radius(Double.parseDouble(data.getOrDefault("radius", "0"))); + } + }; + } + + class EffectType { + private static final Map> EFFECT_TYPES = new HashMap<>(); + + public static final EffectType FIREWORK = new EffectType<>("firework", CustomEffect::firework); + public static final EffectType PARTICLE = new EffectType<>("particle", CustomEffect::particle); + public static final EffectType FREEZE = new EffectType<>("freeze", CustomEffect::freeze); + + private final Function, CustomEffect> function; + + EffectType(String type, Function, CustomEffect> function) { + this.function = function; + + EFFECT_TYPES.put(type, this); + } + + public CustomEffect create(Consumer builder) { + return this.function.apply(builder); + } + + public static EffectType get(String type) { + return EFFECT_TYPES.get(type); + } + } + + class ParticleBuilder { + private Particle particle; + private double speed; + private int count; + private Vector offset = new Vector(0, 0, 0); + private Object data; + + public ParticleBuilder particle(Particle particle) { + this.particle = particle; + return this; + } + + public ParticleBuilder speed(double speed) { + this.speed = speed; + return this; + } + + public ParticleBuilder count(int count) { + this.count = count; + return this; + } + + public ParticleBuilder offset(Vector offset) { + this.offset = offset; + return this; + } + + public ParticleBuilder data(T data) { + if (this.particle == null) { + throw new IllegalStateException("Particle must be set before setting data"); + } + switch (this.particle) { + case REDSTONE -> { + if (!(data instanceof Particle.DustOptions)) { + throw new IllegalArgumentException("Data must be of type Particle.DustOptions for REDSTONE particles"); + } + } + case ITEM_CRACK -> { + if (!(data instanceof ItemStack)) { + throw new IllegalArgumentException("Data must be of type ItemStack for ITEM_CRACK particles"); + } + } + case BLOCK_CRACK, FALLING_DUST, BLOCK_MARKER -> { + if (!(data instanceof BlockData)) { + throw new IllegalArgumentException("Data must be of type BlockData for " + this.particle + " particles"); + } + } + case DUST_COLOR_TRANSITION -> { + if (!(data instanceof Particle.DustTransition)) { + throw new IllegalArgumentException("Data must be of type Particle.DustTransition"); + } + } + case VIBRATION -> { + if (!(data instanceof Vibration)) { + throw new IllegalArgumentException("Data must be of type Particle.Vibration"); + } + } + case SCULK_CHARGE -> { + if (!(data instanceof Float)) { + throw new IllegalArgumentException("Data must be of type Float for SCULK_CHARGE particles"); + } + } + case SHRIEK -> { + if (!(data instanceof Integer)) { + throw new IllegalArgumentException("Data must be of type Integer for SHRIEK particles"); + } + } + } + + this.data = data; + return this; + } + } + + class FreezeBuilder { + private Duration duration; + private double radius; + + public FreezeBuilder duration(Duration duration) { + this.duration = duration; + return this; + } + + public FreezeBuilder radius(double radius) { + this.radius = radius; + return this; + } + } + + private static List getList(String value) { + return Arrays.asList(value.replace("[", "") + .replace("]", "").split(",")); + } +}