diff --git a/plugin/src/main/java/org/battleplugins/arena/command/BaseCommandExecutor.java b/plugin/src/main/java/org/battleplugins/arena/command/BaseCommandExecutor.java index bb288eb8..958ba976 100644 --- a/plugin/src/main/java/org/battleplugins/arena/command/BaseCommandExecutor.java +++ b/plugin/src/main/java/org/battleplugins/arena/command/BaseCommandExecutor.java @@ -438,7 +438,7 @@ private Object verifyArgument(CommandSender sender, String arg, Class paramet } case "duration" -> { try { - return DurationParser.parseDuration(arg); + return DurationParser.deserializeSingular(arg); } catch (ParseException e) { ParseException.handle(e); return null; 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 03f9311f..a3b3f798 100644 --- a/plugin/src/main/java/org/battleplugins/arena/config/DefaultParsers.java +++ b/plugin/src/main/java/org/battleplugins/arena/config/DefaultParsers.java @@ -16,6 +16,7 @@ import org.bukkit.block.data.BlockData; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.inventory.ItemStack; +import org.bukkit.potion.PotionEffect; import java.awt.Color; import java.time.Duration; @@ -31,6 +32,7 @@ static void register() { ArenaConfigParser.registerProvider(Duration.class, new DurationParser()); ArenaConfigParser.registerProvider(ItemStack.class, new ItemStackParser()); + ArenaConfigParser.registerProvider(PotionEffect.class, new PotionEffectParser()); ArenaConfigParser.registerProvider(PositionWithRotation.class, configValue -> { if (!(configValue instanceof ConfigurationSection positionSection)) { return null; diff --git a/plugin/src/main/java/org/battleplugins/arena/config/DurationParser.java b/plugin/src/main/java/org/battleplugins/arena/config/DurationParser.java index da4eebdd..ca55e4f8 100644 --- a/plugin/src/main/java/org/battleplugins/arena/config/DurationParser.java +++ b/plugin/src/main/java/org/battleplugins/arena/config/DurationParser.java @@ -1,7 +1,6 @@ package org.battleplugins.arena.config; import java.time.Duration; -import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -11,16 +10,22 @@ public final class DurationParser implements ArenaConfigParser.Parser @Override public Duration parse(Object object) throws ParseException { - return parseDuration(object); - } - - public static Duration parseDuration(Object object) throws ParseException { if (object instanceof Number number) { return Duration.ofSeconds(number.longValue()); } - String value = object.toString(); - if (value.isBlank()) { + if (object instanceof String contents) { + return deserializeSingular(contents); + } + + throw new ParseException("Invalid Duration for object: " + object) + .cause(ParseException.Cause.INVALID_TYPE) + .type(this.getClass()) + .userError(); + } + + public static Duration deserializeSingular(String contents) throws ParseException { + if (contents.isBlank()) { throw new ParseException("Duration value was not provided!") .cause(ParseException.Cause.MISSING_VALUE) .type(DurationParser.class) @@ -28,7 +33,7 @@ public static Duration parseDuration(Object object) throws ParseException { } // Define a regular expression to match the duration format - Matcher matcher = DURATION_PATTERN.matcher(value); + Matcher matcher = DURATION_PATTERN.matcher(contents); long totalSeconds = 0; while (matcher.find()) { @@ -66,7 +71,7 @@ public static Duration parseDuration(Object object) throws ParseException { } if (totalSeconds == 0) { - throw new ParseException("Failed to parse Duration from value " + object) + throw new ParseException("Failed to parse Duration from value " + contents) .cause(ParseException.Cause.INVALID_VALUE) .type(DurationParser.class) .userError(); 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 97cdb5e8..4364d2c0 100644 --- a/plugin/src/main/java/org/battleplugins/arena/config/ItemStackParser.java +++ b/plugin/src/main/java/org/battleplugins/arena/config/ItemStackParser.java @@ -1,6 +1,7 @@ package org.battleplugins.arena.config; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextDecoration; import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.Material; import org.bukkit.NamespacedKey; @@ -13,7 +14,6 @@ import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.PotionMeta; import org.bukkit.potion.PotionEffect; -import org.bukkit.potion.PotionEffectType; import java.awt.Color; import java.util.Arrays; @@ -21,11 +21,10 @@ import java.util.List; import java.util.Locale; import java.util.Set; -import java.util.function.BiFunction; -import java.util.function.Function; @DocumentationSource("https://docs.battleplugins.org/books/user-guide/page/item-syntax") public final class ItemStackParser implements ArenaConfigParser.Parser { + private static final Component NO_ITALIC = Component.empty().decoration(TextDecoration.ITALIC, false); @Override public ItemStack parse(Object object) throws ParseException { @@ -93,7 +92,7 @@ public static ItemStack deserializeSingular(String contents) throws ParseExcepti } } case "display-name", "name" -> { - itemMeta.displayName(MiniMessage.miniMessage().deserialize(value)); + itemMeta.displayName(NO_ITALIC.append(MiniMessage.miniMessage().deserialize(value))); } case "enchants", "enchantments" -> { for (String enchant : getList(value)) { @@ -119,6 +118,7 @@ public static ItemStack deserializeSingular(String contents) throws ParseExcepti List lore = getList(value) .stream() .map(MiniMessage.miniMessage()::deserialize) + .map(NO_ITALIC::append) .toList(); itemMeta.lore(lore); @@ -135,16 +135,8 @@ public static ItemStack deserializeSingular(String contents) throws ParseExcepti } for (String effect : getList(value)) { - String[] effectSplit = effect.split(" "); - PotionEffectType effectType = PotionEffectType.getByName(effectSplit[0]); - if (effectType == null) { - throw new ParseException("Invalid potion effect " + effectSplit[0]); - } - - int duration = Integer.parseInt(effectSplit[1]) * 20; - int amplifier = Integer.parseInt(effectSplit[2]) - 1; - - potionMeta.addCustomEffect(new PotionEffect(effectType, duration, amplifier), true); + PotionEffect potionEffect = PotionEffectParser.deserializeSingular(effect); + potionMeta.addCustomEffect(potionEffect, true); } } } @@ -191,7 +183,7 @@ private static ItemStack deserializeNode(ConfigurationSection section) throws Pa } } case "display-name", "name": { - itemMeta.displayName(MiniMessage.miniMessage().deserialize(section.getString(meta))); + itemMeta.displayName(NO_ITALIC.append(MiniMessage.miniMessage().deserialize(section.getString(meta)))); } case "enchants", "enchantments": { for (String enchant : section.getStringList(meta)) { @@ -217,6 +209,7 @@ private static ItemStack deserializeNode(ConfigurationSection section) throws Pa List lore = section.getStringList(meta) .stream() .map(MiniMessage.miniMessage()::deserialize) + .map(NO_ITALIC::append) .toList(); itemMeta.lore(lore); @@ -233,16 +226,8 @@ private static ItemStack deserializeNode(ConfigurationSection section) throws Pa } for (String effect : section.getStringList(meta)) { - String[] effectSplit = effect.split(" "); - PotionEffectType effectType = PotionEffectType.getByName(effectSplit[0]); - if (effectType == null) { - throw new ParseException("Invalid potion effect " + effectSplit[0]); - } - - int duration = Integer.parseInt(effectSplit[1]) * 20; - int amplifier = Integer.parseInt(effectSplit[2]) - 1; - - potionMeta.addCustomEffect(new PotionEffect(effectType, duration, amplifier), true); + PotionEffect potionEffect = PotionEffectParser.deserializeSingular(effect); + potionMeta.addCustomEffect(potionEffect, true); } } } diff --git a/plugin/src/main/java/org/battleplugins/arena/config/ParseException.java b/plugin/src/main/java/org/battleplugins/arena/config/ParseException.java index a096a900..8409a867 100644 --- a/plugin/src/main/java/org/battleplugins/arena/config/ParseException.java +++ b/plugin/src/main/java/org/battleplugins/arena/config/ParseException.java @@ -112,7 +112,7 @@ public static void handle(ParseException exception) { builder.append("Stacktrace (Debug Mode):\n"); StringWriter writer = new StringWriter(); - exception.printStackTrace(new PrintWriter(writer)); + (exception.getCause() != null ? exception.getCause() : exception).printStackTrace(new PrintWriter(writer)); builder.append(writer); } diff --git a/plugin/src/main/java/org/battleplugins/arena/config/PotionEffectParser.java b/plugin/src/main/java/org/battleplugins/arena/config/PotionEffectParser.java new file mode 100644 index 00000000..649891d8 --- /dev/null +++ b/plugin/src/main/java/org/battleplugins/arena/config/PotionEffectParser.java @@ -0,0 +1,63 @@ +package org.battleplugins.arena.config; + +import org.bukkit.NamespacedKey; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; + +public class PotionEffectParser implements ArenaConfigParser.Parser { + + @Override + public PotionEffect parse(Object object) throws ParseException { + if (object instanceof String contents) { + return deserializeSingular(contents); + } + + throw new ParseException("Invalid PotionEffect for object: " + object) + .cause(ParseException.Cause.INVALID_TYPE) + .type(PotionEffectParser.class) + .userError(); + } + + public static PotionEffect deserializeSingular(String contents) throws ParseException { + SingularValueParser.ArgumentBuffer buffer = SingularValueParser.parseNamed(contents, SingularValueParser.BraceStyle.CURLY, ';'); + if (!buffer.hasNext()) { + throw new ParseException("No data found for PotionEffect") + .cause(ParseException.Cause.INVALID_TYPE) + .type(PotionEffectParser.class) + .userError(); + } + + PotionEffectType type; + int duration = 20; + int amplifier = 1; + boolean ambient = true; + boolean particles = true; + + SingularValueParser.Argument root = buffer.pop(); + if (root.key().equals("root")) { + type = PotionEffectType.getByKey(NamespacedKey.fromString(root.value())); + if (type == null) { + throw new ParseException("Invalid potion effect type " + root.value()) + .cause(ParseException.Cause.INVALID_VALUE) + .type(PotionEffectParser.class) + .userError(); + } + } else { + throw new ParseException("Invalid PotionEffect root tag " + root.key()) + .cause(ParseException.Cause.INTERNAL_ERROR) + .type(PotionEffectParser.class); + } + + while (buffer.hasNext()) { + SingularValueParser.Argument effectArgument = buffer.pop(); + switch (effectArgument.key()) { + case "duration" -> duration = Integer.parseInt(effectArgument.value()) * 20; + case "amplifier" -> amplifier = Integer.parseInt(effectArgument.value()) - 1; + case "ambient" -> ambient = Boolean.parseBoolean(effectArgument.value()); + case "particles" -> particles = Boolean.parseBoolean(effectArgument.value()); + } + } + + return new PotionEffect(type, duration, amplifier, ambient, particles); + } +} diff --git a/plugin/src/main/java/org/battleplugins/arena/config/SingularValueParser.java b/plugin/src/main/java/org/battleplugins/arena/config/SingularValueParser.java index 3865ff93..f11db30c 100644 --- a/plugin/src/main/java/org/battleplugins/arena/config/SingularValueParser.java +++ b/plugin/src/main/java/org/battleplugins/arena/config/SingularValueParser.java @@ -1,6 +1,8 @@ package org.battleplugins.arena.config; import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; import java.util.Queue; public final class SingularValueParser { @@ -16,7 +18,7 @@ public static ArgumentBuffer parseUnnamed(String contents, BraceStyle style, cha private static ArgumentBuffer parse(String contents, BraceStyle style, char separator, boolean named) throws ParseException { ArgumentBuffer buffer = new ArgumentBuffer(); if (named) { - String[] split = contents.split(style.open); + String[] split = contents.split(String.valueOf(new char[] { '\\', style.open }), 2); String dataType = split[0]; buffer.push("root", dataType); @@ -33,13 +35,15 @@ private static ArgumentBuffer parse(String contents, BraceStyle style, char sepa } private static ArgumentBuffer parseNamed(ArgumentBuffer buffer, String contents, BraceStyle style, char separator) throws ParseException { - contents = contents.replace(style.close, ""); - for (String argument : contents.split(Character.toString(separator))) { - String[] optionSplit = argument.split("="); + contents = contents.substring(0, contents.lastIndexOf(style.close)); + + for (String argument : parseArguments(contents, style, separator)) { + String[] optionSplit = argument.split("=", 2); if (optionSplit.length != 2) { throw new ParseException("Invalid argument length! Expected arguments in the form of =, but got " + argument) .cause(ParseException.Cause.INVALID_VALUE) .context("Provided argument", argument) + .context("Contents", contents) .userError(); } @@ -54,13 +58,36 @@ private static ArgumentBuffer parseUnnamed(ArgumentBuffer buffer, String content contents = contents.substring(0, contents.length() - 1); int index = 0; - for (String argument : contents.split(Character.toString(separator))) { + for (String argument : parseArguments(contents, style, separator)) { buffer.push(Integer.toString(index++), argument); } return buffer; } + private static List parseArguments(String contents, BraceStyle style, char separator) { + List arguments = new ArrayList<>(); + StringBuilder builder = new StringBuilder(); + int depth = 0; + for (char c : contents.toCharArray()) { + if (c == style.open) { + depth++; + } else if (c == style.close) { + depth--; + } + + if (depth == 0 && c == separator) { + arguments.add(builder.toString()); + builder = new StringBuilder(); + } else { + builder.append(c); + } + } + + arguments.add(builder.toString()); + return arguments; + } + public static final class ArgumentBuffer { private final Queue values = new ArrayDeque<>(); @@ -81,14 +108,14 @@ public record Argument(String key, String value) { } public enum BraceStyle { - CURLY("\\{", "}"), - SQUARE("\\[", "]"), - ROUND("\\(", ")"); + CURLY('{', '}'), + SQUARE('[', ']'), + ROUND('(', ')'); - private final String open; - private final String close; + private final char open; + private final char close; - BraceStyle(String open, String close) { + BraceStyle(char open, char close) { this.open = open; this.close = close; } diff --git a/plugin/src/main/java/org/battleplugins/arena/event/action/EventActionType.java b/plugin/src/main/java/org/battleplugins/arena/event/action/EventActionType.java index 62ad2156..60cbaf87 100644 --- a/plugin/src/main/java/org/battleplugins/arena/event/action/EventActionType.java +++ b/plugin/src/main/java/org/battleplugins/arena/event/action/EventActionType.java @@ -9,6 +9,7 @@ import org.battleplugins.arena.event.action.types.DelayAction; import org.battleplugins.arena.event.action.types.FlightAction; import org.battleplugins.arena.event.action.types.GiveEffectsAction; +import org.battleplugins.arena.event.action.types.GiveItemAction; import org.battleplugins.arena.event.action.types.HealthAction; import org.battleplugins.arena.event.action.types.JoinRandomTeamAction; import org.battleplugins.arena.event.action.types.KillEntitiesAction; @@ -47,6 +48,7 @@ public final class EventActionType { public static final EventActionType DELAY = new EventActionType<>("delay", DelayAction.class, DelayAction::new); public static final EventActionType FLIGHT = new EventActionType<>("flight", FlightAction.class, FlightAction::new); public static final EventActionType GIVE_EFFECTS = new EventActionType<>("give-effects", GiveEffectsAction.class, GiveEffectsAction::new); + public static final EventActionType GIVE_ITEM = new EventActionType<>("give-item", GiveItemAction.class, GiveItemAction::new); public static final EventActionType HEALTH = new EventActionType<>("health", HealthAction.class, HealthAction::new); public static final EventActionType JOIN_RANDOM_TEAM = new EventActionType<>("join-random-team", JoinRandomTeamAction.class, JoinRandomTeamAction::new); public static final EventActionType KILL_ENTITIES = new EventActionType<>("kill-entities", KillEntitiesAction.class, KillEntitiesAction::new); diff --git a/plugin/src/main/java/org/battleplugins/arena/event/action/types/GiveEffectsAction.java b/plugin/src/main/java/org/battleplugins/arena/event/action/types/GiveEffectsAction.java index 03352bf8..0ee13ae7 100644 --- a/plugin/src/main/java/org/battleplugins/arena/event/action/types/GiveEffectsAction.java +++ b/plugin/src/main/java/org/battleplugins/arena/event/action/types/GiveEffectsAction.java @@ -2,10 +2,11 @@ import org.battleplugins.arena.ArenaPlayer; import org.battleplugins.arena.config.ParseException; +import org.battleplugins.arena.config.PotionEffectParser; import org.battleplugins.arena.config.SingularValueParser; import org.battleplugins.arena.event.action.EventAction; import org.battleplugins.arena.resolver.Resolvable; -import org.bukkit.potion.PotionEffectType; +import org.bukkit.potion.PotionEffect; import java.util.Map; @@ -38,18 +39,20 @@ public void call(ArenaPlayer arenaPlayer, Resolvable resolvable) { while (buffer.hasNext()) { SingularValueParser.Argument argument = buffer.pop(); - String effect = argument.value(); - - String[] effectSplit = effect.split(" "); - PotionEffectType effectType = PotionEffectType.getByName(effectSplit[0]); - if (effectType == null) { - throw new IllegalArgumentException("Invalid potion effect " + effectSplit[0]); + String effectContents = argument.value(); + + try { + PotionEffect effect = PotionEffectParser.deserializeSingular(effectContents); + arenaPlayer.getPlayer().addPotionEffect(effect); + } catch (ParseException e) { + ParseException.handle(e + .context("Action", "GiveEffectsAction") + .context("Arena", arenaPlayer.getArena().getName()) + .context("Provided value", effectContents) + .cause(ParseException.Cause.INVALID_VALUE) + .userError() + ); } - - int duration = Integer.parseInt(effectSplit[1]) * 20; - int amplifier = Integer.parseInt(effectSplit[2]) - 1; - - arenaPlayer.getPlayer().addPotionEffect(effectType.createEffect(duration, amplifier)); } } } diff --git a/plugin/src/main/java/org/battleplugins/arena/event/action/types/GiveItemAction.java b/plugin/src/main/java/org/battleplugins/arena/event/action/types/GiveItemAction.java new file mode 100644 index 00000000..defe8cd0 --- /dev/null +++ b/plugin/src/main/java/org/battleplugins/arena/event/action/types/GiveItemAction.java @@ -0,0 +1,41 @@ +package org.battleplugins.arena.event.action.types; + +import org.battleplugins.arena.ArenaPlayer; +import org.battleplugins.arena.config.ItemStackParser; +import org.battleplugins.arena.config.ParseException; +import org.battleplugins.arena.event.action.EventAction; +import org.battleplugins.arena.resolver.Resolvable; +import org.bukkit.inventory.ItemStack; + +import java.util.Map; + +public class GiveItemAction extends EventAction { + private static final String ITEM_KEY = "item"; + private static final String SLOT_KEY = "slot"; + + public GiveItemAction(Map params) { + super(params, ITEM_KEY); + } + + @Override + public void call(ArenaPlayer arenaPlayer, Resolvable resolvable) { + String item = this.get(ITEM_KEY); + try { + ItemStack itemStack = ItemStackParser.deserializeSingular(item); + int slot = Integer.parseInt(this.getOrDefault(SLOT_KEY, "-1")); + if (slot == -1) { + arenaPlayer.getPlayer().getInventory().addItem(itemStack); + } else { + arenaPlayer.getPlayer().getInventory().setItem(slot, itemStack); + } + } catch (ParseException e) { + ParseException.handle(e + .context("Action", "GiveItemAction") + .context("Arena", arenaPlayer.getArena().getName()) + .context("Provided value", this.get(item)) + .cause(ParseException.Cause.INVALID_VALUE) + .userError() + ); + } + } +} diff --git a/plugin/src/main/resources/arenas/arena.yml b/plugin/src/main/resources/arenas/arena.yml index 01736c63..9e1b698a 100644 --- a/plugin/src/main/resources/arenas/arena.yml +++ b/plugin/src/main/resources/arenas/arena.yml @@ -56,7 +56,7 @@ phases: events: on-complete: - teleport{location=team_spawn} - - give-effects{effects=[speed 300 1]} + - give-effects{effects=[speed{duration=300;amplifier=1}]} - play-sound{sound=block.note_block.pling;pitch=2;volume=1} ingame: allow-join: false diff --git a/plugin/src/main/resources/arenas/battlegrounds.yml b/plugin/src/main/resources/arenas/battlegrounds.yml index 5f1e651f..185d944f 100644 --- a/plugin/src/main/resources/arenas/battlegrounds.yml +++ b/plugin/src/main/resources/arenas/battlegrounds.yml @@ -57,7 +57,7 @@ phases: events: on-complete: - teleport{location=team_spawn} - - give-effects{effects=[speed 300 1]} + - give-effects{effects=[speed{duration=300;amplifier=1}]} - play-sound{sound=block.note_block.pling;pitch=2;volume=1} ingame: allow-join: false diff --git a/plugin/src/main/resources/arenas/colosseum.yml b/plugin/src/main/resources/arenas/colosseum.yml index f24b0731..6bcfaccb 100644 --- a/plugin/src/main/resources/arenas/colosseum.yml +++ b/plugin/src/main/resources/arenas/colosseum.yml @@ -58,7 +58,7 @@ phases: events: on-complete: - teleport{location=team_spawn} - - give-effects{effects=[speed 300 1]} + - give-effects{effects=[speed{duration=300;amplifier=1}]} - play-sound{sound=block.note_block.pling;pitch=2;volume=1} ingame: allow-join: false diff --git a/plugin/src/main/resources/arenas/deathmatch.yml b/plugin/src/main/resources/arenas/deathmatch.yml index 643b4b9e..0eb3dfce 100644 --- a/plugin/src/main/resources/arenas/deathmatch.yml +++ b/plugin/src/main/resources/arenas/deathmatch.yml @@ -59,7 +59,7 @@ phases: - broadcast{message=[BattleArena] The Deathmatch event is about to start! Run /deathmatch join to join!} on-complete: - teleport{location=team_spawn} - - give-effects{effects=[speed 300 1]} + - give-effects{effects=[speed{duration=300;amplifier=1}]} - play-sound{sound=block.note_block.pling;pitch=2;volume=1} ingame: allow-join: false diff --git a/plugin/src/main/resources/arenas/ffa.yml b/plugin/src/main/resources/arenas/ffa.yml index 7cb9a140..86f6eec7 100644 --- a/plugin/src/main/resources/arenas/ffa.yml +++ b/plugin/src/main/resources/arenas/ffa.yml @@ -60,7 +60,7 @@ phases: - broadcast{message=[BattleArena] The Deathmatch event is about to start! Run /deathmatch join to join!} on-complete: - teleport{location=team_spawn} - - give-effects{effects=[speed 300 1]} + - give-effects{effects=[speed{duration=300;amplifier=1}]} - play-sound{sound=block.note_block.pling;pitch=2;volume=1} ingame: allow-join: false