diff --git a/reactions/src/main/java/fun/reactions/module/basic/activators/ItemHeldActivator.java b/reactions/src/main/java/fun/reactions/module/basic/activators/ItemHeldActivator.java index f2b3b5ab..cc2e1fc0 100644 --- a/reactions/src/main/java/fun/reactions/module/basic/activators/ItemHeldActivator.java +++ b/reactions/src/main/java/fun/reactions/module/basic/activators/ItemHeldActivator.java @@ -74,8 +74,8 @@ public void saveOptions(@NotNull ConfigurationSection cfg) { @Override public String toString() { String sb = super.toString() + " (" + - "itemnew:" + (virtualItemNew == VirtualItem.EMPTY ? "-" : virtualItemNew) + - " itemprev:" + (virtualItemPrev == VirtualItem.EMPTY ? "-" : virtualItemPrev) + + "itemnew:" + (virtualItemNew == VirtualItem.ANY ? "-" : virtualItemNew) + + " itemprev:" + (virtualItemPrev == VirtualItem.ANY ? "-" : virtualItemPrev) + " slotnew:" + (slotNew + 1) + " slotprev:" + (slotPrev + 1) + ")"; diff --git a/reactions/src/main/java/fun/reactions/placeholders/ModernPlaceholdersManager.java b/reactions/src/main/java/fun/reactions/placeholders/ModernPlaceholdersManager.java index 33717723..eee615fa 100644 --- a/reactions/src/main/java/fun/reactions/placeholders/ModernPlaceholdersManager.java +++ b/reactions/src/main/java/fun/reactions/placeholders/ModernPlaceholdersManager.java @@ -65,11 +65,11 @@ private String parseGradually(@NotNull Environment env, @NotNull String text) { String substring = text.substring(stepIndex + 2, index); String processed = resolvePlaceholder(env, substring); if (processed != null) { - if (text.length() > index + 3 && text.charAt(index + 1) == '(') { + if (text.length() > index + 3 && text.charAt(index + 1) == '(') { // TODO Better escaping trigger String options = optionsSearch(text, index + 2); if (options != null) { index += options.length() + 2; - if (options.contains("prms")) processed = Parameters.escapeParameters(processed); + if (options.contains("prms")) processed = Parameters.escapeValue(processed); if (options.contains("phs")) processed = escapeSpecial(processed); } } diff --git a/reactions/src/main/java/fun/reactions/util/item/VirtualItem.java b/reactions/src/main/java/fun/reactions/util/item/VirtualItem.java index 7592690b..b8a9f69c 100644 --- a/reactions/src/main/java/fun/reactions/util/item/VirtualItem.java +++ b/reactions/src/main/java/fun/reactions/util/item/VirtualItem.java @@ -1,6 +1,5 @@ package fun.reactions.util.item; -import fun.reactions.util.Utils; import fun.reactions.util.item.aspects.*; import fun.reactions.util.naming.Aliased; import fun.reactions.util.num.NumberUtils; @@ -19,7 +18,7 @@ import java.util.stream.Collectors; public final class VirtualItem implements Parameterizable { - private static final Pattern SIMPLE_ITEM = Pattern.compile("([a-zA-Z_]+)(?::(\\d+))?(?:\\*(\\d+))?"); + private static final Pattern SIMPLE_ITEM = Pattern.compile("([a-zA-Z\\d_]+)(?::(\\d{1,9}))?(?:\\*(\\d{1,9}))?"); /** * A VirtualItem that accepts only null or air ItemStacks @@ -28,9 +27,9 @@ public final class VirtualItem implements Parameterizable { /** * A VirtualItem that accepts any ItemStacks but null or air */ - public static final VirtualItem EMPTY = new VirtualItem(null, -1, List.of(), Parameters.EMPTY); + public static final VirtualItem ANY = new VirtualItem(null, -1, List.of(), Parameters.EMPTY); /** - * A VirtualItem that accepts noting + * A VirtualItem that accepts nothing */ public static final VirtualItem INVALID = new VirtualItem(null, -1, List.of(new MetaAspect.Instance() { @Override @@ -181,18 +180,21 @@ public int getAmount() { ? null : itemValue.clone(); } else { - itemGenerated = true; if (type == null || !type.isItem()) { return null; } else { - itemValue = new ItemStack(type); - itemValue.setAmount(Math.max(amount, 1)); - if (!type.isEmpty() && !aspects.isEmpty()) { - ItemMeta meta = itemValue.getItemMeta(); - aspects.forEach(aspect -> aspect.apply(meta)); - itemValue.setItemMeta(meta); + ItemStack genItem = new ItemStack(type); + if (!type.isEmpty()) { + genItem.setAmount(Math.max(amount, 1)); + if (!aspects.isEmpty()) { + ItemMeta meta = genItem.getItemMeta(); + aspects.forEach(aspect -> aspect.apply(meta)); + genItem.setItemMeta(meta); + } } - return initClone ? itemValue.clone() : itemValue; + itemValue = genItem; + itemGenerated = true; + return initClone ? genItem.clone() : genItem; } } } @@ -304,7 +306,7 @@ public boolean isSimilar(@Nullable ItemStack compared, AmountCheck amountCheck) @Contract(pure = true) public static @NotNull VirtualItem fromParameters(@NotNull Parameters params) { - if (params.isEmpty()) return VirtualItem.EMPTY; + if (params.isEmpty()) return VirtualItem.ANY; List aspects = new ArrayList<>(); Material type = null; int amount = -1; @@ -327,10 +329,10 @@ public boolean isSimilar(@Nullable ItemStack compared, AmountCheck amountCheck) if (!matcher.matches()) break; type = ItemUtils.getMaterial(matcher.group(1)); if (type == null) return VirtualItem.INVALID; - if (!Utils.isStringEmpty(matcher.group(2))) { + if (matcher.group(2) != null) { aspects.add(ASPECTS_BY_NAME.get("durability").fromString(matcher.group(1))); } - if (!Utils.isStringEmpty(matcher.group(3))) { + if (matcher.group(3) != null) { amount = NumberUtils.asInteger(matcher.group(3), -1); } break; @@ -361,7 +363,7 @@ public boolean isSimilar(@Nullable ItemStack compared, AmountCheck amountCheck) ); } - @Contract("null -> null") + @Contract("null -> null; !null -> !null") public static @Nullable ItemStack asCleanItemStack(@Nullable ItemStack item) { if (item == null) return null; VirtualItem virtual = fromItemStack(item, false); @@ -369,7 +371,7 @@ public boolean isSimilar(@Nullable ItemStack compared, AmountCheck amountCheck) virtual.type, virtual.amount, virtual.aspects, - virtual.paramsValue + virtual.asParameters() ).asItemStack(false); } @@ -402,6 +404,19 @@ public String toString() { return asString(); } + @Override + public boolean equals(Object other) { + return this == other || other instanceof VirtualItem otherItem && + amount == otherItem.amount && + type == otherItem.type && + aspects.equals(otherItem.aspects); + } + + @Override + public int hashCode() { + return Objects.hash(type, amount, aspects); + } + public enum AmountCheck { /** * Skip amount check as a whole @@ -412,7 +427,7 @@ public enum AmountCheck { */ EQUAL, /** - * Checks if amount of ItemStack satisfies amount of VirtualItem + * Checks if amount of ItemStack satisfies (>=) amount of VirtualItem */ SATISFIES } diff --git a/reactions/src/main/java/fun/reactions/util/item/aspects/DurabilityAspect.java b/reactions/src/main/java/fun/reactions/util/item/aspects/DurabilityAspect.java index 09e92b6c..fcbf78d3 100644 --- a/reactions/src/main/java/fun/reactions/util/item/aspects/DurabilityAspect.java +++ b/reactions/src/main/java/fun/reactions/util/item/aspects/DurabilityAspect.java @@ -38,7 +38,7 @@ public void apply(@NotNull ItemMeta meta) { @Override public boolean isSimilar(@NotNull ItemMeta meta) { if (meta instanceof Damageable damageMeta) { - return value < 0 ? !damageMeta.hasDamage() : damageMeta.getDamage() >= value; + return value >= 0 ? damageMeta.getDamage() >= value : !damageMeta.hasDamage(); } return false; } diff --git a/reactions/src/main/java/fun/reactions/util/item/aspects/EnchantmentsAspect.java b/reactions/src/main/java/fun/reactions/util/item/aspects/EnchantmentsAspect.java index 2b289dfc..a819664d 100644 --- a/reactions/src/main/java/fun/reactions/util/item/aspects/EnchantmentsAspect.java +++ b/reactions/src/main/java/fun/reactions/util/item/aspects/EnchantmentsAspect.java @@ -23,7 +23,7 @@ public class EnchantmentsAspect implements MetaAspect { } @Override - public @NotNull EnchantmentsAspect.EnchantmentsInst fromString(@NotNull String value) { + public @NotNull MetaAspect.Instance fromString(@NotNull String value) { if (value.isEmpty()) { return EnchantmentsInst.EMPTY; } @@ -53,7 +53,7 @@ public class EnchantmentsAspect implements MetaAspect { } @Override - public EnchantmentsInst fromItem(@NotNull ItemMeta meta) { + public MetaAspect.Instance fromItem(@NotNull ItemMeta meta) { Map enchants = meta instanceof EnchantmentStorageMeta enchantmentMeta ? enchantmentMeta.getStoredEnchants() : meta.getEnchants(); diff --git a/reactions/src/main/java/fun/reactions/util/num/Is.java b/reactions/src/main/java/fun/reactions/util/num/Is.java index 11f6b4b9..c8ffd002 100644 --- a/reactions/src/main/java/fun/reactions/util/num/Is.java +++ b/reactions/src/main/java/fun/reactions/util/num/Is.java @@ -10,11 +10,6 @@ public final class Is { private Is() {} - /** - * Checks if number is an integer (has no floating value) - */ - public static final DoublePredicate INTEGER = v -> v == (long) v; - /** * Checks if number is positive */ @@ -23,7 +18,6 @@ private Is() {} * Check if number is positive or zero */ public static final DoublePredicate NON_NEGATIVE = v -> v >= 0; - /** * Checks if number is negative */ @@ -33,15 +27,6 @@ private Is() {} */ public static final DoublePredicate NON_POSITIVE = v -> v <= 0; - /** - * Checks if number is positive and integer - */ - public static final DoublePredicate POSITIVE_NATURAL = POSITIVE.and(INTEGER); - /** - * Checks if number is positive or zero and integer - */ - public static final DoublePredicate NATURAL = NON_NEGATIVE.and(INTEGER); - public static @NotNull DoublePredicate inRange(double min, double max) { return v -> min <= v && v < max; } diff --git a/reactions/src/main/java/fun/reactions/util/parameter/Parameters.java b/reactions/src/main/java/fun/reactions/util/parameter/Parameters.java index 4102d907..027a668f 100644 --- a/reactions/src/main/java/fun/reactions/util/parameter/Parameters.java +++ b/reactions/src/main/java/fun/reactions/util/parameter/Parameters.java @@ -21,10 +21,13 @@ import java.util.function.*; import java.util.regex.Pattern; +import static ink.glowing.text.InkyMessage.isEscapedAt; + public class Parameters implements Parameterizable { - public static final String ORIGIN = " :"; + public static final String ORIGIN = ": "; public static final Parameters EMPTY = new Parameters("", "", new CaseInsensitiveMap<>(1)); - private static final Pattern UNESCAPED = Pattern.compile("(? params; @@ -44,6 +47,10 @@ protected Parameters(@NotNull String origin, @NotNull String formatted, @NotNull this.formatted = formatted; } + public static @NotNull String originKey() { + return ORIGIN; + } + public static @NotNull Parameters fromConfiguration(@NotNull ConfigurationSection cfg) { return fromConfiguration(cfg, Set.of()); } @@ -104,7 +111,7 @@ protected Parameters(@NotNull String origin, @NotNull String formatted, @NotNull int next = i + 1; if (next < str.length()) { char n = str.charAt(next); - if (n == '{' || n == '}' || (n == '\\' && next + 1 < str.length() && str.charAt(next + 1) == '}')) { + if ("{}:\\".indexOf(n) != -1) { if (state == IterationState.SPACE) { bld = new StringBuilder(); state = IterationState.TEXT; @@ -195,7 +202,7 @@ private enum IterationState { public static @NotNull Parameters singleton(@NotNull String key, @NotNull String value) { Map params = new CaseInsensitiveMap<>(2); params.put(key, value); - String escaped = escapeParameters(value); + String escaped = escapeValue(value); String origin; if (requiresBrackets(escaped, value)) { origin = key + ":{" + escaped + "}"; @@ -210,7 +217,7 @@ private enum IterationState { map.forEach((key, value) -> { if (key.equals(ORIGIN)) return; bld.append(key).append(':'); - String escaped = escapeParameters(value); + String escaped = escapeValue(value); if (requiresBrackets(escaped, value)) { bld.append('{').append(escaped).append('}'); } else { @@ -226,7 +233,7 @@ public static boolean requiresBrackets(@NotNull String escaped, @NotNull String value.indexOf(' ') != -1 || value.indexOf(':') != -1 || value.charAt(0) == '{'; } - public static @NotNull String escapeParameters(@NotNull String str) { + public static @NotNull String escapeValue(@NotNull String str) { if (str.isEmpty()) return str; int brackets = 0; boolean escaped = false; @@ -244,14 +251,19 @@ public static boolean requiresBrackets(@NotNull String escaped, @NotNull String break; } } - if (str.charAt(str.length() - 1) == '\\' && (str.length() == 1 || str.charAt(str.length() - 2) != '\\')) { + if (str.charAt(str.length() - 1) == '\\' && (str.length() == 1 || !isEscapedAt(str, str.length() - 1))) { str += '\\'; } return brackets != 0 - ? UNESCAPED.matcher(str).replaceAll("\\\\$0") + ? VALUE_ESCAPE.matcher(str).replaceAll("\\\\$0") : str; } + public static @NotNull String escape(@NotNull String str) { + if (str.isEmpty()) return str; + return FULL_ESCAPE.matcher(str).replaceAll("\\\\$0"); + } + public @NotNull RichOptional getOptional(@NotNull String key, @NotNull Function converter) { String value = params.get(key); return value == null @@ -582,22 +594,18 @@ public int hashCode() { @Override @Contract("null -> false") public boolean equals(Object obj) { - if (obj == this) return true; - if (obj instanceof Parameters other) { - if (other.size() != size()) return false; - for (String key : keys()) { - if (!Objects.equals(getString(key), other.getString(key, null))) { - return false; - } - } - return true; - } - return false; + return obj instanceof Parameters otherParams && (otherParams == this || otherParams.origin.equals(origin)); } @Contract("null -> false") - public boolean equalsFull(@Nullable Parameters params) { - return params != null && (params == this || params.origin.equals(origin)); + public boolean isSimilar(@Nullable Parameters other) { + if (other == null || other.size() != size()) return false; + for (String key : keys()) { + if (!Objects.equals(getString(key), other.getString(key, null))) { + return false; + } + } + return true; } @Override diff --git a/reactions/src/test/java/fun/reactions/placeholders/ModernPlaceholdersManagerTest.java b/reactions/src/test/java/fun/reactions/placeholders/ModernPlaceholdersManagerTest.java index 5e0ea7ef..710d1806 100644 --- a/reactions/src/test/java/fun/reactions/placeholders/ModernPlaceholdersManagerTest.java +++ b/reactions/src/test/java/fun/reactions/placeholders/ModernPlaceholdersManagerTest.java @@ -1,23 +1,28 @@ package fun.reactions.placeholders; +import fun.reactions.ReActions; import fun.reactions.model.environment.Environment; import fun.reactions.model.environment.Variables; import fun.reactions.module.basic.placeholders.LocalVarPlaceholder; +import org.mockito.Mockito; import org.testng.annotations.Test; +import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; public class ModernPlaceholdersManagerTest { @Test public void testParsePlaceholders() { + var platform = Mockito.mock(ReActions.Platform.class); var mgr = new ModernPlaceholdersManager(); + when(platform.getPlaceholders()).thenReturn(mgr); mgr.registerPlaceholder(new LocalVarPlaceholder()); PlaceholdersManager.setCountLimit(16); Variables vars = new Variables(); vars.set("test", "y\\ay"); vars.set("another", "%[test]\\,"); assertEquals( - mgr.parse(new Environment(null, "", vars, null, 0), "Foo %[another] bar %[ignored]"), + mgr.parse(new Environment(platform, "", vars, null, 0), "Foo %[another] bar %[ignored]"), "Foo y\\ay\\, bar %[ignored]" ); } diff --git a/reactions/src/test/java/fun/reactions/util/NumberUtilsTest.java b/reactions/src/test/java/fun/reactions/util/NumberUtilsTest.java index 868805ae..243f74b9 100644 --- a/reactions/src/test/java/fun/reactions/util/NumberUtilsTest.java +++ b/reactions/src/test/java/fun/reactions/util/NumberUtilsTest.java @@ -16,10 +16,6 @@ public class NumberUtilsTest { public static Object[][] parseDoubleData() { return new Object[][]{ {"3.14", Is.POSITIVE, OptionalDouble.of(3.14)}, - {"-5.0", Is.POSITIVE_NATURAL, OptionalDouble.empty()}, - {"3.0", Is.NATURAL, OptionalDouble.of(3.0)}, - {"7", Is.INTEGER, OptionalDouble.of(7)}, - {"3.14", Is.NATURAL, OptionalDouble.empty()}, {"abc", Is.NEGATIVE, OptionalDouble.empty()} }; } diff --git a/reactions/src/test/java/fun/reactions/util/parameter/ParametersTest.java b/reactions/src/test/java/fun/reactions/util/parameter/ParametersTest.java index 97ed3114..a125ee4c 100644 --- a/reactions/src/test/java/fun/reactions/util/parameter/ParametersTest.java +++ b/reactions/src/test/java/fun/reactions/util/parameter/ParametersTest.java @@ -11,27 +11,51 @@ import java.util.Set; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; public class ParametersTest { @DataProvider public Object[][] fromStringData() { return new Object[][] { - {"test:{value} test2:value\\ test3:va:lue test4:value}", "test:value test2:{value\\\\} test3:{va:lue} test4:{value\\}}", 4}, - {"test:ignored test:value1 test2:\\{value2 test3:value3}", "test:value1 test2:{\\{value2} test3:{value3\\}}", 3}, - {"test:{{additional brackets}} empty:{} test2:\\{brackets2\\}", "test:{{additional brackets}} empty:{} test2:{{brackets2}}", 3}, - {"test:{value broken\\}", "", 0}, - {"test:{value{ broken}", "", 0}, - {"key:{s p a c e:fake\\}} e:test", "key:{s p a c e:fake\\}} e:test", 2}, - {"key:someverylongvaluewow!", "key:{someverylongvaluewow!}", 1} + { + "test:{value} test2:value\\ test3:va:lue test4:value}", + "test:value test2:{value\\\\} test3:{va:lue} test4:{value\\}}", 4 + }, + { + "test:ignored test:value1 test2:\\{value2 test3:value3}", + "test:value1 test2:{\\{value2} test3:{value3\\}}", 3 + }, + { + "test:{inside:{hallo world}} test2:{inside1:{hallo world} inside2:{howa u 2day}}", + "test:{inside:{hallo world}} test2:{inside1:{hallo world} inside2:{howa u 2day}}", 2 + }, + { + "test:{{additional brackets}} empty:{} test2:\\{brackets2\\}", + "test:{{additional brackets}} empty:{} test2:{{brackets2}}", 3}, + { + "test:{value broken\\}", + "", 0 + }, + { + "test:{value{ broken}", + "", 0 + }, + { + "key:{s p a c e:fake\\}} e:test", + "key:{s p a c e:fake\\}} e:test", 2 + }, + { + "key:someverylongvaluewow!", + "key:{someverylongvaluewow!}", 1 + } }; } @Test(dataProvider = "fromStringData") public void fromStringTest(String input, String expected, int size) { Parameters result = Parameters.fromString(input); - assertEquals( // Testing Parameters#equals too - result, - Parameters.fromString(expected) + assertTrue( // Testing Parameters#isSimilar too + result.isSimilar(Parameters.fromString(expected)) ); assertEquals( // We expect the result to be the same Parameters.fromString(result.originFormatted()).originFormatted(), @@ -50,7 +74,7 @@ public Object[][] fromMapData() { @Test(dataProvider = "fromMapData") public void fromMapTest(Map map, String paramsStr) { - assertEquals(Parameters.fromMap(map), Parameters.fromString(paramsStr)); + assertTrue(Parameters.fromMap(map).isSimilar(Parameters.fromString(paramsStr))); } @DataProvider @@ -88,9 +112,8 @@ public Object[][] fromConfigurationData() { public void fromConfigurationTest(String cfgStr, String expected) throws InvalidConfigurationException { FileConfiguration cfg = new YamlConfiguration(); cfg.loadFromString(cfgStr); - assertEquals( - Parameters.fromConfiguration(cfg, Set.of("ignored")), - Parameters.fromString(expected) + assertTrue( + Parameters.fromConfiguration(cfg, Set.of("ignored")).isSimilar(Parameters.fromString(expected)) ); } @@ -109,7 +132,7 @@ public void keyedListTest(String input, List expected) { } @DataProvider - public static Object[][] escapeParametersData() { + public static Object[][] escapeValueData() { return new Object[][] { {"basic text", "basic text"}, {"", ""}, @@ -117,8 +140,8 @@ public static Object[][] escapeParametersData() { {"}", "\\}"}, {"already\\{escaped", "already\\{escaped"}, {"on\\ly \\the last\\", "on\\ly \\the last\\\\"}, - {"{equal amount}", "{equal amount}"}, - {"{unequal amount}}", "\\{unequal amount\\}\\}"}, + {"test:{equal amount}", "test:{equal amount}"}, + {"test:{unequal amount}}", "test:\\{unequal amount\\}\\}"}, {"{{unequal amount}", "\\{\\{unequal amount\\}"}, {"{unequal escaped\\}}", "{unequal escaped\\}}"}, {"{unequal with last}}\\", "\\{unequal with last\\}\\}\\\\"}, @@ -126,11 +149,36 @@ public static Object[][] escapeParametersData() { }; } - @Test(dataProvider = "escapeParametersData") - public void escapeParametersTest(String input, String expected) { - String result = Parameters.escapeParameters(input); + @Test(dataProvider = "escapeValueData") + public void escapeValueTest(String input, String expected) { + String result = Parameters.escapeValue(input); + assertEquals(result, expected); + assertEquals(Parameters.escapeValue(result), expected); // Escaping the escaped should not work + } + + @DataProvider + public static Object[][] escapeData() { + return new Object[][] { + {"basic text", "basic text"}, + {"", ""}, + {"\\", "\\\\"}, + {"}", "\\}"}, + {"already\\{escaped", "already\\\\\\{escaped"}, + {"on\\ly \\the last\\", "on\\\\ly \\\\the last\\\\"}, + {"test:{equal amount}", "test\\:\\{equal amount\\}"}, + {"test:{unequal amount}}", "test\\:\\{unequal amount\\}\\}"}, + {"{{unequal amount}", "\\{\\{unequal amount\\}"}, + {"{unequal escaped\\}}", "\\{unequal escaped\\\\\\}\\}"}, + {"{unequal with last}}\\", "\\{unequal with last\\}\\}\\\\"}, + {"}wrong order{", "\\}wrong order\\{"} + }; + } + + @Test(dataProvider = "escapeData") + public void escapeTest(String input, String expected) { + String result = Parameters.escape(input); assertEquals(result, expected); - assertEquals(Parameters.escapeParameters(result), expected); // Escaping the escaped should not work + assertTrue(Parameters.fromString(result).isEmpty(), Parameters.fromString(result).keys().toString()); } @DataProvider @@ -154,7 +202,8 @@ public Object[][] getStringData() { return new Object[][] { {Parameters.fromString("key:abc anOther_value:123 EXTRA:{456} key:overridden"), "key", "overridden"}, {Parameters.fromString("key:abc anOther_value:123 EXTRA:{456} key:overridden"), "extra", "456"}, - {Parameters.fromString("nothing!"), "key", ""} + {Parameters.fromString("nothing!"), "key", ""}, + {Parameters.fromString("key:{Escaped \\{ \\} \\\\\\} \\: \\\\ \\ value}"), "key", "Escaped { } \\} : \\ \\ value"} }; }