From be891c4115a5c572028e7e88060a4e4207221d60 Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Sat, 6 Dec 2025 21:53:04 -0800 Subject: [PATCH 1/9] Base functions, to/from hex color expressions, function parameter ranges --- .../skript/classes/data/DefaultFunctions.java | 87 +++++++++++++- .../lang/function/FunctionReference.java | 28 +++-- .../njol/skript/lang/function/Parameter.java | 11 +- src/main/java/ch/njol/skript/util/Color.java | 7 ++ .../java/ch/njol/skript/util/ColorRGB.java | 17 +++ .../expressions/ExprColorFromHexCode.java | 45 +++++++ .../common/expressions/ExprHexCode.java | 40 +++++++ .../common/function/DefaultFunctionImpl.java | 10 +- .../skript/common/function/Parameter.java | 110 ++++++++++++++++++ .../tests/syntaxes/expressions/ExprHexCode.sk | 30 +++++ .../skript/tests/syntaxes/functions/bases.sk | 21 ++++ 11 files changed, 395 insertions(+), 11 deletions(-) create mode 100644 src/main/java/org/skriptlang/skript/common/expressions/ExprColorFromHexCode.java create mode 100644 src/main/java/org/skriptlang/skript/common/expressions/ExprHexCode.java create mode 100644 src/test/skript/tests/syntaxes/expressions/ExprHexCode.sk create mode 100644 src/test/skript/tests/syntaxes/functions/bases.sk diff --git a/src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java b/src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java index b05fceefd81..dcb52d5dce7 100644 --- a/src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java +++ b/src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java @@ -3,7 +3,6 @@ import ch.njol.skript.Skript; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.KeyedValue; -import org.skriptlang.skript.common.function.DefaultFunction; import ch.njol.skript.lang.function.Functions; import ch.njol.skript.lang.function.Parameter; import ch.njol.skript.lang.function.SimpleJavaFunction; @@ -26,6 +25,7 @@ import org.joml.Quaternionf; import org.joml.Vector3f; import org.skriptlang.skript.addon.SkriptAddon; +import org.skriptlang.skript.common.function.DefaultFunction; import org.skriptlang.skript.common.function.Parameter.Modifier; import java.math.BigDecimal; @@ -353,6 +353,91 @@ public Class getReturnType(Expression... arguments) { "set {_clamped::*} to clamp({_values::*}, 0, 10)") .since("2.8.0"); + Functions.register(DefaultFunction.builder(skript, "toBase", String[].class) + .description(""" + Turns a number in a string using a specific base (decimal, hexadecimal, octal). + For example, converting 32 to hexadecimal (base 16) would be 'toBase(32, 16)', which would return "20". + You can use any base between 2 and 36. + """) + .examples( + "send \"Decode this binary number for a prize! %toBase({_guess}, 2)%\"" + ) + .since("INSERT VERSION") + .parameter("n", Long[].class) + .parameter("base", Long.class, Modifier.ranged(2, 36)) + .contract(new Contract() { + @Override + public boolean isSingle(Expression... arguments) { + return arguments[0].isSingle(); + } + + @Override + public Class getReturnType(Expression... arguments) { + return String.class; + } + }) + .build(args -> { + Long[] n = args.get("n"); + Long base = args.get("base"); + if (n == null || base == null) + return null; + if (base > Character.MAX_RADIX || base < Character.MIN_RADIX) + return null; + String[] results = new String[n.length]; + for (int i = 0; i < n.length; i++) { + results[i] = Long.toString(n[i], base.intValue()); + } + return results; + })); + + Functions.register(DefaultFunction.builder(skript, "fromBase", Long[].class) + .description(""" + Turns a text version of a number in a specific base (decimal, hexadecimal, octal) into an actual number. + For example, converting "20" in hexadecimal (base 16) would be 'fromBase("20", 16)', which would return 32. + You can use any base between 2 and 36. + """) + .examples(""" + # /binaryText 01110011 01101011 01110010 01101001 01110000 01110100 00100001 + # sends "skript!" + command binaryText : + trigger: + set {_characters::*} to argument split at " " without trailing empty string + transform {_characters::*} with fromBase(input, 2) # convert to codepoints + transform {_characters::*} with character from codepoint input # convert to characters + send join {_characters::*} + """) + .since("INSERT VERSION") + .parameter("string value", String[].class) + .parameter("base", Long.class, Modifier.ranged(2, 36)) + .contract(new Contract() { + @Override + public boolean isSingle(Expression... arguments) { + return arguments[0].isSingle(); + } + + @Override + public Class getReturnType(Expression... arguments) { + return Long.class; + } + }) + .build(args -> { + String[] n = args.get("string value"); + Long base = args.get("base"); + if (n == null || base == null) + return null; + if (base > Character.MAX_RADIX || base < Character.MIN_RADIX) + return null; + Long[] results = new Long[n.length]; + try { + for (int i = 0; i < n.length; i++) { + results[i] = Long.parseLong(n[i], base.intValue()); + } + } catch (NumberFormatException e) { + return null; + } + return results; + })); + // misc Functions.registerFunction(new SimpleJavaFunction("world", new Parameter[] { diff --git a/src/main/java/ch/njol/skript/lang/function/FunctionReference.java b/src/main/java/ch/njol/skript/lang/function/FunctionReference.java index 478d2cd0bac..24e2b87a765 100644 --- a/src/main/java/ch/njol/skript/lang/function/FunctionReference.java +++ b/src/main/java/ch/njol/skript/lang/function/FunctionReference.java @@ -7,7 +7,6 @@ import ch.njol.skript.lang.*; import ch.njol.skript.lang.function.FunctionRegistry.Retrieval; import ch.njol.skript.lang.function.FunctionRegistry.RetrievalResult; -import ch.njol.skript.lang.parser.ParserInstance; import ch.njol.skript.log.RetainingLogHandler; import ch.njol.skript.log.SkriptLogger; import ch.njol.skript.registrations.Classes; @@ -16,8 +15,9 @@ import ch.njol.util.StringUtils; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; -import org.skriptlang.skript.lang.converter.Converters; import org.skriptlang.skript.common.function.Parameter.Modifier; +import org.skriptlang.skript.common.function.Parameter.Modifier.RangedModifier; +import org.skriptlang.skript.lang.converter.Converters; import org.skriptlang.skript.util.Executable; import java.util.*; @@ -229,17 +229,17 @@ public boolean validateFunction(boolean first) { // Check parameter types for (int i = 0; i < parameters.length; i++) { - Parameter p = sign.parameters[singleListParam ? 0 : i]; + Parameter signatureParam = sign.parameters[singleListParam ? 0 : i]; RetainingLogHandler log = SkriptLogger.startRetainingLog(); try { //noinspection unchecked - Expression e = parameters[i].getConvertedExpression(p.type()); - if (e == null) { + Expression exprParam = parameters[i].getConvertedExpression(signatureParam.type()); + if (exprParam == null) { if (first) { if (LiteralUtils.hasUnparsedLiteral(parameters[i])) { Skript.error("Can't understand this expression: " + parameters[i].toString()); } else { - String type = Classes.toString(getClassInfo(p.type())); + String type = Classes.toString(getClassInfo(signatureParam.type())); Skript.error("The " + StringUtils.fancyOrderNumber(i + 1) + " argument given to the function '" + stringified + "' is not of the required type " + type + "." + " Check the correct order of the arguments and put lists into parentheses if appropriate (e.g. 'give(player, (iron ore and gold ore))')." @@ -251,7 +251,7 @@ public boolean validateFunction(boolean first) { function = previousFunction; } return false; - } else if (p.single && !e.isSingle()) { + } else if (signatureParam.single && !exprParam.isSingle()) { if (first) { Skript.error("The " + StringUtils.fancyOrderNumber(i + 1) + " argument given to the function '" + functionName + "' is plural, " + "but a single argument was expected"); @@ -262,7 +262,19 @@ public boolean validateFunction(boolean first) { } return false; } - parameters[i] = e; + + // check ranged parameters + if (signatureParam.hasModifier(Modifier.RANGED) && exprParam instanceof Literal literalParam) { + RangedModifier range = signatureParam.getModifier(RangedModifier.class); + if (!range.inRange(literalParam.getArray())) { + Skript.error("The argument '" + signatureParam.name() +"' only accepts values between " + + Classes.toString(range.getMin()) + " and " + Classes.toString(range.getMax()) + ". " + + "Provided: " + literalParam.toString(null, Skript.debug())); + return false; + } + } + + parameters[i] = exprParam; } finally { log.printLog(); } diff --git a/src/main/java/ch/njol/skript/lang/function/Parameter.java b/src/main/java/ch/njol/skript/lang/function/Parameter.java index 4b3f9abed54..a4c12ef48b7 100644 --- a/src/main/java/ch/njol/skript/lang/function/Parameter.java +++ b/src/main/java/ch/njol/skript/lang/function/Parameter.java @@ -18,6 +18,7 @@ import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import org.skriptlang.skript.common.function.DefaultFunction; +import org.skriptlang.skript.common.function.Parameter.Modifier.RangedModifier; import java.util.*; import java.util.regex.Matcher; @@ -280,8 +281,16 @@ public String toString() { return toString(Skript.debug()); } + // name: type between min and max = default + // ns: numbers between 0 and 100 = 3 public String toString(boolean debug) { - return name + ": " + Utils.toEnglishPlural(type.getCodeName(), !single) + (def != null ? " = " + def.toString(null, debug) : ""); + String result = name + ": " + Utils.toEnglishPlural(type.getCodeName(), !single); + if (this.hasModifier(Modifier.RANGED)) { + RangedModifier range = this.getModifier(RangedModifier.class); + result += " between " + Classes.toString(range.getMin()) + " and " + Classes.toString(range.getMax()); + } + result += (def != null ? " = " + def.toString(null, debug) : ""); + return result; } @Override diff --git a/src/main/java/ch/njol/skript/util/Color.java b/src/main/java/ch/njol/skript/util/Color.java index b74bff53aa9..3f6fcad6b5e 100644 --- a/src/main/java/ch/njol/skript/util/Color.java +++ b/src/main/java/ch/njol/skript/util/Color.java @@ -51,4 +51,11 @@ default int asARGB() { return asBukkitColor().asARGB(); } + /** + * @return the colour as an RGB hex value: RRGGBB + */ + default String toHexString() { + return String.format("%02X%02X%02X", getRed(), getGreen(), getBlue()); + } + } diff --git a/src/main/java/ch/njol/skript/util/ColorRGB.java b/src/main/java/ch/njol/skript/util/ColorRGB.java index 483834940ba..8c589318f08 100644 --- a/src/main/java/ch/njol/skript/util/ColorRGB.java +++ b/src/main/java/ch/njol/skript/util/ColorRGB.java @@ -134,6 +134,23 @@ public String getName() { ); } + /** + * @param hex A [AA]RRGGBB hex string to parse into ARGB values. Must be either a length of 6 or 8. Omitting alpha will default it to 255 (FF). + * @return a color with the provided ARGB values, or null if parsing failed. + */ + public static @Nullable ColorRGB fromHexString(String hex) { + if (hex.length() != 6 && hex.length() != 8) + return null; + if (hex.length() == 6) + hex = "FF" + hex; // default alpha to 255 + try { + int argb = Integer.parseUnsignedInt(hex, 16); + return ColorRGB.fromRGBA(argb >> 16 & 255, argb >> 8 & 255, argb & 255, argb >> 24 & 255); + } catch (NumberFormatException e) { + return null; + } + } + @Override public Fields serialize() throws NotSerializableException { return new Fields(this, Variables.yggdrasil); diff --git a/src/main/java/org/skriptlang/skript/common/expressions/ExprColorFromHexCode.java b/src/main/java/org/skriptlang/skript/common/expressions/ExprColorFromHexCode.java new file mode 100644 index 00000000000..7dd5949f8c9 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/common/expressions/ExprColorFromHexCode.java @@ -0,0 +1,45 @@ +package org.skriptlang.skript.common.expressions; + +import ch.njol.skript.Skript; +import ch.njol.skript.expressions.base.SimplePropertyExpression; +import ch.njol.skript.lang.ExpressionType; +import ch.njol.skript.util.Color; +import ch.njol.skript.util.ColorRGB; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; + +public class ExprColorFromHexCode extends SimplePropertyExpression { + + static { + Skript.registerExpression(ExprColorFromHexCode.class, Color.class, ExpressionType.PROPERTY, + "[the] colo[u]r[s] (from|of) hex[adecimal] code[s] %strings%"); + } + + + @Override + public @Nullable Color convert(String from) { + if (from.startsWith("#")) // strip leading # + from = from.substring(1); + Color color = ColorRGB.fromHexString(from); + if (color == null) + error("Could not parse '" + from + "' as a hex code!"); + return color; + } + + @Override + public Class getReturnType() { + return Color.class; + } + + @Override + protected String getPropertyName() { + assert false; + return "ExprColorFromHexCode - UNSUSED"; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return "the color of hex code " + getExpr().toString(event, debug); + } + +} diff --git a/src/main/java/org/skriptlang/skript/common/expressions/ExprHexCode.java b/src/main/java/org/skriptlang/skript/common/expressions/ExprHexCode.java new file mode 100644 index 00000000000..2e09de53e15 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/common/expressions/ExprHexCode.java @@ -0,0 +1,40 @@ +package org.skriptlang.skript.common.expressions; + +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.expressions.base.SimplePropertyExpression; +import ch.njol.skript.util.Color; +import org.jetbrains.annotations.Nullable; + +@Name("Hex Code") +@Description(""" + Returns the hexadecimal value representing the given color[s]. + The hex value of a colour does not contain a leading #, just the RRGGBB value. + For those looking for hex values of numbers, see the asBase and fromBase functions. + """) +@Example("send formatted \"<#%hex code of rgb(100, 10, 10)%>darker red\" to all players") +@Since("INSERT VERSION") +public class ExprHexCode extends SimplePropertyExpression { + + static { + register(ExprHexCode.class, String.class, "hex[adecimal] code", "colors"); + } + + @Override + public @Nullable String convert(Color color) { + return color.toHexString(); + } + + @Override + public Class getReturnType() { + return String.class; + } + + @Override + protected String getPropertyName() { + return "hexadecimal code"; + } + +} diff --git a/src/main/java/org/skriptlang/skript/common/function/DefaultFunctionImpl.java b/src/main/java/org/skriptlang/skript/common/function/DefaultFunctionImpl.java index 2a89f4bd0e2..8d2c99e4477 100644 --- a/src/main/java/org/skriptlang/skript/common/function/DefaultFunctionImpl.java +++ b/src/main/java/org/skriptlang/skript/common/function/DefaultFunctionImpl.java @@ -8,6 +8,7 @@ import org.jetbrains.annotations.Unmodifiable; import org.skriptlang.skript.addon.SkriptAddon; import org.skriptlang.skript.common.function.Parameter.Modifier; +import org.skriptlang.skript.common.function.Parameter.Modifier.RangedModifier; import java.lang.reflect.Array; import java.util.*; @@ -69,7 +70,14 @@ final class DefaultFunctionImpl extends ch.njol.skript.lang.function.Function } } - if (arg.length == 1 || parameter.single()) { + if (parameter.hasModifier(Modifier.RANGED)) { + RangedModifier range = parameter.getModifier(RangedModifier.class); + if (!range.inRange(arg)) { + return null; + } + } + + if (arg.length == 1 && parameter.single()) { assert parameter.type().isAssignableFrom(arg[0].getClass()) : "argument type %s does not match parameter type %s".formatted(parameter.type().getSimpleName(), arg[0].getClass().getSimpleName()); diff --git a/src/main/java/org/skriptlang/skript/common/function/Parameter.java b/src/main/java/org/skriptlang/skript/common/function/Parameter.java index ccf790745dd..f12f4216137 100644 --- a/src/main/java/org/skriptlang/skript/common/function/Parameter.java +++ b/src/main/java/org/skriptlang/skript/common/function/Parameter.java @@ -1,8 +1,11 @@ package org.skriptlang.skript.common.function; +import com.google.common.base.Preconditions; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; import org.skriptlang.skript.common.function.DefaultFunction.Builder; +import org.skriptlang.skript.lang.converter.Converter; +import org.skriptlang.skript.lang.converter.Converters; import java.util.Set; @@ -37,6 +40,19 @@ default boolean hasModifier(Modifier modifier) { return modifiers().contains(modifier); } + /** + * Gets a modifier of the specified type if present. + * @param modifierClass The class of the modifier to retrieve + * @return The modifier instance, or null if not present + */ + default M getModifier(Class modifierClass) { + return modifiers().stream() + .filter(modifierClass::isInstance) + .map(modifierClass::cast) + .findFirst() + .orElse(null); + } + /** * @return Whether this parameter is for single values. */ @@ -70,6 +86,100 @@ static Modifier of() { */ Modifier KEYED = of(); + /** + * A modifier to use for checking if a parameter is ranged. + * Do NOT use for declaring a parameter to be ranged, use {@link Modifier#ranged(Comparable, Comparable)} + * Accessing the min and the max values can be done via {@link Parameter#getModifier(Class)}. + */ + Modifier RANGED = new RangedModifier<>(0,0); // 0 and 0 are just dummy values. + + /** + * Creates a range modifier with inclusive min and max bounds. + */ + static > RangedModifier ranged(T min, T max) { + return new RangedModifier<>(min, max); + } + + /** + * Modifier specifying valid range bounds for numeric parameters. + * Note that ALL instances will have the same hashCode and will be equal to {@link Modifier#RANGED}. + * Avoid comparing these objects or putting multiple into a HashSet or HashMap! + */ + class RangedModifier> implements Modifier { + private final T min; + private final T max; + + /** + * Inclusive range between min and max + * @param min min value + * @param max max value + */ + private RangedModifier(T min, T max) { + Preconditions.checkState(min.compareTo(max) < 1, "Min value cannot be greater than max value!"); + this.min = min; + this.max = max; + } + + /** + * @return Min value of the range (inclusive) + */ + public T getMin() { return min; } + + /** + * @return Max value of the range (inclusive) + */ + public T getMax() { return max; } + + /** + * @param input The value to test. + * @return Whether input is between min and max. + */ + public boolean inRange(Object input) { + return inRange(new Object[]{ input }); + } + + /** + * @param inputs The values to test. + * @return Whether all the inputs are between min and max. + */ + @SuppressWarnings("unchecked") + public boolean inRange(Object @NotNull [] inputs) { + if (inputs.length == 0) + return false; + for (Object input : inputs) { + // convert to right type + if (!min.getClass().isInstance(input)) { + Converter converter = (Converter) Converters.getConverter(input.getClass(), min.getClass()); + if (converter == null) + return false; + input = converter.convert(input); + if (input == null) + return false; + } + // compare + if (!(((T) input).compareTo(min) > -1 && ((T) input).compareTo(max) < 1)) + return false; + } + return true; + } + + @Override + public boolean equals(Object obj) { + // equal to the RANGED singleton for hasModifier checks + return obj == Modifier.RANGED || ((obj instanceof RangedModifier range) && range.max == this.max && range.min == this.min); + } + + @Override + public int hashCode() { + return 439824729; // all should be equal + } + + @Override + public String toString() { + return "RangeModifier(min=" + min + ", max=" + max + ")"; + } + + } } } diff --git a/src/test/skript/tests/syntaxes/expressions/ExprHexCode.sk b/src/test/skript/tests/syntaxes/expressions/ExprHexCode.sk new file mode 100644 index 00000000000..e191473a355 --- /dev/null +++ b/src/test/skript/tests/syntaxes/expressions/ExprHexCode.sk @@ -0,0 +1,30 @@ + +using error catching + +test "colour hex codes": + + assert hex code of black is "1D1D21" with "black hex code was not correct" + assert hex code of dark grey is "474F52" with "dark grey hex code was not correct" + assert hex code of grey is "9D9D97" with "grey hex code was not correct" + assert hex code of white is "F9FFFE" with "white hex code was not correct" + assert hex code of blue is "3C44AA" with "blue hex code was not correct" + assert hex code of brown is "835432" with "brown hex code was not correct" + assert hex code of cyan is "169C9C" with "cyan hex code was not correct" + assert hex code of light cyan is "3AB3DA" with "light cyan hex code was not correct" + assert hex code of green is "5E7C16" with "green hex code was not correct" + assert hex code of light green is "80C71F" with "light green hex code was not correct" + assert hex code of yellow is "FED83D" with "yellow hex code was not correct" + assert hex code of orange is "F9801D" with "orange hex code was not correct" + assert hex code of red is "B02E26" with "red hex code was not correct" + assert hex code of pink is "F38BAA" with "pink hex code was not correct" + assert hex code of purple is "8932B8" with "purple hex code was not correct" + assert hex code of magenta is "C74EBD" with "magenta hex code was not correct" + + assert hex code of {_x} is not set with "hex code of an invalid type shouldn't return anything" + + assert color from hex code "#8932B8" is purple with "purple hex code was not correct" + assert colour of hexadecimal code "C74EBD" is magenta with "magenta hex code was not correct" + + catch runtime errors: + assert colour of hex code "hello!" is not set with "hello! should not be a valid hex code" + assert last caught errors is "Could not parse 'hello!' as a hex code!" with "hello! should error" diff --git a/src/test/skript/tests/syntaxes/functions/bases.sk b/src/test/skript/tests/syntaxes/functions/bases.sk new file mode 100644 index 00000000000..218f3f26f2c --- /dev/null +++ b/src/test/skript/tests/syntaxes/functions/bases.sk @@ -0,0 +1,21 @@ +test "bases": + loop 1000 times: + set {_value} to random integer between -1000 and 1000 + set {_base} to random number between 2 and 36 + set {_str} to toBase({_value}, {_base}) + set {_test} to fromBase({_str}, {_base}) + assert {_test} is {_value} with "Failed conversion to/from %{_base}%" + + assert toBase({_}, {_}) is not set with "toBase unset inputs provided output" + assert fromBase({_}, {_}) is not set with "fromBase unset inputs provided output" + + assert toBase(32, 16) is "20" with "basic toBase failed" + assert fromBase("20", 16) is 32 with "basic fromBase failed" + + assert toBase(-1, 16) is "-1" with "negative toBase failed" + assert fromBase(toBase(-1, 16), 16) is -1 with "negative toBase/fromBase failed" + + assert toBase(1, 0), toBase(1, -1), toBase(1, 1), and toBase(1, 64) are not set with "too-small bases and too-large bases returned values" + assert fromBase("1", 0), fromBase("1", -1), fromBase("1", 1), and fromBase("1", 64) are not set with "too-small bases and too-large bases returned values" + + From d6c046e551885cbcd8098397911dafa3b420f91e Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Sat, 6 Dec 2025 22:15:55 -0800 Subject: [PATCH 2/9] Fix ranged modifier equals --- .../java/org/skriptlang/skript/common/function/Parameter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/skriptlang/skript/common/function/Parameter.java b/src/main/java/org/skriptlang/skript/common/function/Parameter.java index f12f4216137..4a51d6fe809 100644 --- a/src/main/java/org/skriptlang/skript/common/function/Parameter.java +++ b/src/main/java/org/skriptlang/skript/common/function/Parameter.java @@ -166,7 +166,7 @@ public boolean inRange(Object @NotNull [] inputs) { @Override public boolean equals(Object obj) { // equal to the RANGED singleton for hasModifier checks - return obj == Modifier.RANGED || ((obj instanceof RangedModifier range) && range.max == this.max && range.min == this.min); + return obj == Modifier.RANGED || ((obj instanceof RangedModifier range) && (this == Modifier.RANGED || range.max == this.max && range.min == this.min)); } @Override @@ -176,7 +176,7 @@ public int hashCode() { @Override public String toString() { - return "RangeModifier(min=" + min + ", max=" + max + ")"; + return "RangedModifier(min=" + min + ", max=" + max + ")"; } } From a72197bcbe41d004880cf9f0205ce99df13d1baf Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Sat, 6 Dec 2025 22:31:00 -0800 Subject: [PATCH 3/9] docs and simplification --- .../expressions/ExprColorFromHexCode.java | 22 +++++++++++++++++++ .../common/expressions/ExprHexCode.java | 11 ++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/main/java/org/skriptlang/skript/common/expressions/ExprColorFromHexCode.java b/src/main/java/org/skriptlang/skript/common/expressions/ExprColorFromHexCode.java index 7dd5949f8c9..2a717163bee 100644 --- a/src/main/java/org/skriptlang/skript/common/expressions/ExprColorFromHexCode.java +++ b/src/main/java/org/skriptlang/skript/common/expressions/ExprColorFromHexCode.java @@ -1,13 +1,27 @@ package org.skriptlang.skript.common.expressions; import ch.njol.skript.Skript; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; import ch.njol.skript.expressions.base.SimplePropertyExpression; +import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.ExpressionType; +import ch.njol.skript.lang.Literal; +import ch.njol.skript.lang.simplification.SimplifiedLiteral; import ch.njol.skript.util.Color; import ch.njol.skript.util.ColorRGB; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; +@Name("Color from Hex Code") +@Description("Returns a proper argb color from a hex code string. The hex code must contain RRGGBB values, but can also " + + "contain a leading # or AARRGGBB format. Invalid codes will cause runtime errors.") +@Example("send color from hex code \"#FFBBA7\"") +@Example("send color from hex code \"FFBBA7\"") +@Example("send color from hex code \"#AAFFBBA7\"") +@Since("INSERT VERSION") public class ExprColorFromHexCode extends SimplePropertyExpression { static { @@ -42,4 +56,12 @@ public String toString(@Nullable Event event, boolean debug) { return "the color of hex code " + getExpr().toString(event, debug); } + @Override + public Expression simplify() { + if (getExpr() instanceof Literal) { + return SimplifiedLiteral.fromExpression(this); + } + return this; + } + } diff --git a/src/main/java/org/skriptlang/skript/common/expressions/ExprHexCode.java b/src/main/java/org/skriptlang/skript/common/expressions/ExprHexCode.java index 2e09de53e15..c051269667d 100644 --- a/src/main/java/org/skriptlang/skript/common/expressions/ExprHexCode.java +++ b/src/main/java/org/skriptlang/skript/common/expressions/ExprHexCode.java @@ -5,6 +5,9 @@ import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; import ch.njol.skript.expressions.base.SimplePropertyExpression; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.Literal; +import ch.njol.skript.lang.simplification.SimplifiedLiteral; import ch.njol.skript.util.Color; import org.jetbrains.annotations.Nullable; @@ -37,4 +40,12 @@ protected String getPropertyName() { return "hexadecimal code"; } + @Override + public Expression simplify() { + if (getExpr() instanceof Literal) { + return SimplifiedLiteral.fromExpression(this); + } + return this; + } + } From 84dd45ab6af04605c8838567ac0ec4b1e4bc2134 Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Sat, 6 Dec 2025 22:40:03 -0800 Subject: [PATCH 4/9] cleanup --- .../skript/common/expressions/ExprColorFromHexCode.java | 1 - src/test/skript/tests/syntaxes/functions/bases.sk | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/main/java/org/skriptlang/skript/common/expressions/ExprColorFromHexCode.java b/src/main/java/org/skriptlang/skript/common/expressions/ExprColorFromHexCode.java index 2a717163bee..ebc7827b334 100644 --- a/src/main/java/org/skriptlang/skript/common/expressions/ExprColorFromHexCode.java +++ b/src/main/java/org/skriptlang/skript/common/expressions/ExprColorFromHexCode.java @@ -29,7 +29,6 @@ public class ExprColorFromHexCode extends SimplePropertyExpression Date: Sun, 7 Dec 2025 00:43:30 -0800 Subject: [PATCH 5/9] Update bases.sk --- src/test/skript/tests/syntaxes/functions/bases.sk | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/skript/tests/syntaxes/functions/bases.sk b/src/test/skript/tests/syntaxes/functions/bases.sk index 2633536e90e..6a3243d4715 100644 --- a/src/test/skript/tests/syntaxes/functions/bases.sk +++ b/src/test/skript/tests/syntaxes/functions/bases.sk @@ -15,5 +15,6 @@ test "bases": assert toBase(-1, 16) is "-1" with "negative toBase failed" assert fromBase(toBase(-1, 16), 16) is -1 with "negative toBase/fromBase failed" - assert toBase(1, 0), toBase(1, -1), toBase(1, 1), and toBase(1, 64) are not set with "too-small bases and too-large bases returned values" - assert fromBase("1", 0), fromBase("1", -1), fromBase("1", 1), and fromBase("1", 64) are not set with "too-small bases and too-large bases returned values" + loop 0, -1, 1, and 64: + assert toBase(1, loop-value) is not set with "too-small bases and too-large bases returned values" + assert fromBase("1", loop-value) is not set with "too-small bases and too-large bases returned values" From 6b9a3bc8b6a15c1988c8a896b3b8604a4990833f Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:21:17 -0800 Subject: [PATCH 6/9] Fix runtime error catching, turn simplification runtimes into parse times --- .../ch/njol/skript/lang/SkriptParser.java | 46 +++++++++++++------ .../njol/skript/sections/SecCatchErrors.java | 14 +++--- .../log/runtime/RuntimeErrorCatcher.java | 14 +++++- .../log/runtime/RuntimeErrorManager.java | 1 + .../tests/syntaxes/expressions/ExprHexCode.sk | 3 +- 5 files changed, 57 insertions(+), 21 deletions(-) diff --git a/src/main/java/ch/njol/skript/lang/SkriptParser.java b/src/main/java/ch/njol/skript/lang/SkriptParser.java index 713fc52f674..6df0074f054 100644 --- a/src/main/java/ch/njol/skript/lang/SkriptParser.java +++ b/src/main/java/ch/njol/skript/lang/SkriptParser.java @@ -15,7 +15,6 @@ import ch.njol.skript.lang.function.FunctionReference; import ch.njol.skript.lang.function.FunctionRegistry; import ch.njol.skript.lang.function.Functions; -import ch.njol.skript.lang.function.Signature; import ch.njol.skript.lang.parser.DefaultValueData; import ch.njol.skript.lang.parser.ParseStackOverflowException; import ch.njol.skript.lang.parser.ParserInstance; @@ -49,24 +48,18 @@ import org.skriptlang.skript.lang.converter.Converters; import org.skriptlang.skript.lang.experiment.ExperimentSet; import org.skriptlang.skript.lang.experiment.ExperimentalSyntax; -import org.skriptlang.skript.lang.script.Script; import org.skriptlang.skript.lang.script.ScriptWarning; +import org.skriptlang.skript.log.runtime.RuntimeError; +import org.skriptlang.skript.log.runtime.RuntimeErrorCatcher; import org.skriptlang.skript.registration.SyntaxInfo; import org.skriptlang.skript.registration.SyntaxRegistry; import java.lang.reflect.Array; -import java.util.ArrayList; -import java.util.Deque; -import java.util.EnumMap; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; +import java.util.logging.Level; import java.util.regex.MatchResult; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -280,9 +273,12 @@ public boolean hasTag(String tag) { break; } log.printLog(); - if (doSimplification && element instanceof Simplifiable simplifiable) + if (doSimplification && element instanceof Simplifiable simplifiable) { //noinspection unchecked - return (T) simplifiable.simplify(); + element = (T) simplify(simplifiable); + if (element == null) + continue; + } return element; } } @@ -294,6 +290,30 @@ public boolean hasTag(String tag) { } } + /** + * Returns a simplified version of element, unless a runtime error is thrown, in which case a parse error is printed + * and null is returned. + * @param element The element to simplify + * @return The simplified element, or null if simplification failed. Elements unable to simplify will return themselves. + * @param The element type. + */ + private @Nullable T simplify(@NotNull Simplifiable element) { + // add runtime consumer to catch runtime errors and turn them into parse time errors + T simplified; + try (RuntimeErrorCatcher catcher = new RuntimeErrorCatcher().start()) { + simplified = element.simplify(); + // we can assume that if a single simplification throws many errors, the first non-warning will be at least somewhat representative + RuntimeError error = catcher.getCachedErrors().stream() + .filter(err -> err.level() == Level.SEVERE) + .findFirst().orElse(null); + if (error == null) + return simplified; + // found errors + Skript.error(error.error()); + return null; + } + } + /** * Checks whether the given element is restricted to specific events, and if so, whether the current event is allowed. * Prints errors. diff --git a/src/main/java/ch/njol/skript/sections/SecCatchErrors.java b/src/main/java/ch/njol/skript/sections/SecCatchErrors.java index fad8e88caf0..27bc6c19ca9 100644 --- a/src/main/java/ch/njol/skript/sections/SecCatchErrors.java +++ b/src/main/java/ch/njol/skript/sections/SecCatchErrors.java @@ -63,12 +63,14 @@ public boolean isSatisfiedBy(ExperimentSet experimentSet) { @Override protected @Nullable TriggerItem walk(Event event) { - RuntimeErrorCatcher catcher = new RuntimeErrorCatcher().start(); - last.setNext(null); - TriggerItem.walk(first, event); - ExprCaughtErrors.lastErrors = catcher.getCachedErrors().stream().map(RuntimeError::error).toArray(String[]::new); - catcher.clearCachedErrors() - .stop(); + // don't try to run the section if we are uncertain about its boundaries + if (first != null && last != null) { + try (RuntimeErrorCatcher catcher = new RuntimeErrorCatcher().start()) { + last.setNext(null); + TriggerItem.walk(first, event); + ExprCaughtErrors.lastErrors = catcher.getCachedErrors().stream().map(RuntimeError::error).toArray(String[]::new); + } + } return walk(event, false); } diff --git a/src/main/java/org/skriptlang/skript/log/runtime/RuntimeErrorCatcher.java b/src/main/java/org/skriptlang/skript/log/runtime/RuntimeErrorCatcher.java index 2367c47d901..c8e53725f3a 100644 --- a/src/main/java/org/skriptlang/skript/log/runtime/RuntimeErrorCatcher.java +++ b/src/main/java/org/skriptlang/skript/log/runtime/RuntimeErrorCatcher.java @@ -15,7 +15,7 @@ * A {@link RuntimeErrorConsumer} to be used in {@link RuntimeErrorManager} to catch {@link RuntimeError}s. * This should always be used with {@link #start()} and {@link #stop()}. */ -public class RuntimeErrorCatcher implements RuntimeErrorConsumer { +public class RuntimeErrorCatcher implements RuntimeErrorConsumer, AutoCloseable{ private List storedConsumers = new ArrayList<>(); @@ -24,6 +24,8 @@ public class RuntimeErrorCatcher implements RuntimeErrorConsumer { // hard limit on stored errors to prevent a runaway loop from filling up memory, for example. private static final int ERROR_LIMIT = 1000; + private boolean stopped = false; + public RuntimeErrorCatcher() {} /** @@ -57,6 +59,7 @@ public RuntimeErrorCatcher start() { * Prints all cached {@link RuntimeError}s, {@link #cachedErrors}. */ public void stop() { + stopped = true; if (!getManager().removeConsumer(this)) { SkriptLogger.LOGGER.severe("[Skript] A 'RuntimeErrorCatcher' was stopped incorrectly."); return; @@ -92,4 +95,13 @@ public void printFrameOutput(FrameOutput output, Level level) { // do nothing, this won't be called since we have no filter. } + /** + * Stops the catcher and clears the cached errors. + */ + @Override + public void close() { + if (!stopped) + this.clearCachedErrors().stop(); + } + } diff --git a/src/main/java/org/skriptlang/skript/log/runtime/RuntimeErrorManager.java b/src/main/java/org/skriptlang/skript/log/runtime/RuntimeErrorManager.java index 33e9858a04b..56b988833d0 100644 --- a/src/main/java/org/skriptlang/skript/log/runtime/RuntimeErrorManager.java +++ b/src/main/java/org/skriptlang/skript/log/runtime/RuntimeErrorManager.java @@ -189,6 +189,7 @@ public List removeAllConsumers() { List currentConsumers = new ArrayList<>(); for (var set : filterMap.values()) currentConsumers.addAll(set); + filterMap.clear(); return currentConsumers; } } diff --git a/src/test/skript/tests/syntaxes/expressions/ExprHexCode.sk b/src/test/skript/tests/syntaxes/expressions/ExprHexCode.sk index e191473a355..4a7d0b50671 100644 --- a/src/test/skript/tests/syntaxes/expressions/ExprHexCode.sk +++ b/src/test/skript/tests/syntaxes/expressions/ExprHexCode.sk @@ -26,5 +26,6 @@ test "colour hex codes": assert colour of hexadecimal code "C74EBD" is magenta with "magenta hex code was not correct" catch runtime errors: - assert colour of hex code "hello!" is not set with "hello! should not be a valid hex code" + set {_hello} to "hello!" + assert colour of hex code {_hello} is not set with "hello! should not be a valid hex code" assert last caught errors is "Could not parse 'hello!' as a hex code!" with "hello! should error" From cda4bb4d0715e7c6db1d099321be07b80e765609 Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:29:30 -0800 Subject: [PATCH 7/9] throw warnings too --- .../ch/njol/skript/lang/SkriptParser.java | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/main/java/ch/njol/skript/lang/SkriptParser.java b/src/main/java/ch/njol/skript/lang/SkriptParser.java index 6df0074f054..5f4cc8ad3c6 100644 --- a/src/main/java/ch/njol/skript/lang/SkriptParser.java +++ b/src/main/java/ch/njol/skript/lang/SkriptParser.java @@ -49,7 +49,6 @@ import org.skriptlang.skript.lang.experiment.ExperimentSet; import org.skriptlang.skript.lang.experiment.ExperimentalSyntax; import org.skriptlang.skript.lang.script.ScriptWarning; -import org.skriptlang.skript.log.runtime.RuntimeError; import org.skriptlang.skript.log.runtime.RuntimeErrorCatcher; import org.skriptlang.skript.registration.SyntaxInfo; import org.skriptlang.skript.registration.SyntaxRegistry; @@ -58,6 +57,7 @@ import java.util.*; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; import java.util.logging.Level; import java.util.regex.MatchResult; @@ -302,15 +302,23 @@ public boolean hasTag(String tag) { T simplified; try (RuntimeErrorCatcher catcher = new RuntimeErrorCatcher().start()) { simplified = element.simplify(); - // we can assume that if a single simplification throws many errors, the first non-warning will be at least somewhat representative - RuntimeError error = catcher.getCachedErrors().stream() + // we can assume that if a single simplification throws many errors, the first will be at least somewhat representative + AtomicBoolean error = new AtomicBoolean(false); + catcher.getCachedErrors().stream() .filter(err -> err.level() == Level.SEVERE) - .findFirst().orElse(null); - if (error == null) - return simplified; - // found errors - Skript.error(error.error()); - return null; + .findFirst() + .ifPresent(err -> { + Skript.error(err.error()); + error.set(true); + }); + // same for warnings. + catcher.getCachedErrors().stream() + .filter(err -> err.level() == Level.WARNING) + .findFirst() + .ifPresent(warning -> Skript.warning(warning.error())); + if (error.get()) + return null; + return simplified; } } From 778826e73311f83ae134300c13528e0ebcb38806 Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:43:20 -0800 Subject: [PATCH 8/9] Requested changes --- .../ch/njol/skript/classes/data/DefaultFunctions.java | 8 -------- src/main/java/ch/njol/skript/lang/function/Parameter.java | 3 +++ .../skript/common/expressions/ExprColorFromHexCode.java | 2 +- .../skriptlang/skript/common/expressions/ExprHexCode.java | 2 +- .../skript/common/function/DefaultFunctionImpl.java | 6 +++++- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java b/src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java index dcb52d5dce7..83934361b1d 100644 --- a/src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java +++ b/src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java @@ -379,10 +379,6 @@ public Class getReturnType(Expression... arguments) { .build(args -> { Long[] n = args.get("n"); Long base = args.get("base"); - if (n == null || base == null) - return null; - if (base > Character.MAX_RADIX || base < Character.MIN_RADIX) - return null; String[] results = new String[n.length]; for (int i = 0; i < n.length; i++) { results[i] = Long.toString(n[i], base.intValue()); @@ -423,10 +419,6 @@ public Class getReturnType(Expression... arguments) { .build(args -> { String[] n = args.get("string value"); Long base = args.get("base"); - if (n == null || base == null) - return null; - if (base > Character.MAX_RADIX || base < Character.MIN_RADIX) - return null; Long[] results = new Long[n.length]; try { for (int i = 0; i < n.length; i++) { diff --git a/src/main/java/ch/njol/skript/lang/function/Parameter.java b/src/main/java/ch/njol/skript/lang/function/Parameter.java index a4c12ef48b7..ac2e9b53b68 100644 --- a/src/main/java/ch/njol/skript/lang/function/Parameter.java +++ b/src/main/java/ch/njol/skript/lang/function/Parameter.java @@ -281,7 +281,10 @@ public String toString() { return toString(Skript.debug()); } + // toString output format: // name: type between min and max = default + // + // Example: // ns: numbers between 0 and 100 = 3 public String toString(boolean debug) { String result = name + ": " + Utils.toEnglishPlural(type.getCodeName(), !single); diff --git a/src/main/java/org/skriptlang/skript/common/expressions/ExprColorFromHexCode.java b/src/main/java/org/skriptlang/skript/common/expressions/ExprColorFromHexCode.java index ebc7827b334..f4dfa7d60f7 100644 --- a/src/main/java/org/skriptlang/skript/common/expressions/ExprColorFromHexCode.java +++ b/src/main/java/org/skriptlang/skript/common/expressions/ExprColorFromHexCode.java @@ -47,7 +47,7 @@ public Class getReturnType() { @Override protected String getPropertyName() { assert false; - return "ExprColorFromHexCode - UNSUSED"; + return "ExprColorFromHexCode - UNUSED"; } @Override diff --git a/src/main/java/org/skriptlang/skript/common/expressions/ExprHexCode.java b/src/main/java/org/skriptlang/skript/common/expressions/ExprHexCode.java index c051269667d..0bda32076f9 100644 --- a/src/main/java/org/skriptlang/skript/common/expressions/ExprHexCode.java +++ b/src/main/java/org/skriptlang/skript/common/expressions/ExprHexCode.java @@ -13,7 +13,7 @@ @Name("Hex Code") @Description(""" - Returns the hexadecimal value representing the given color[s]. + Returns the hexadecimal value representing the given color(s). The hex value of a colour does not contain a leading #, just the RRGGBB value. For those looking for hex values of numbers, see the asBase and fromBase functions. """) diff --git a/src/main/java/org/skriptlang/skript/common/function/DefaultFunctionImpl.java b/src/main/java/org/skriptlang/skript/common/function/DefaultFunctionImpl.java index 8d2c99e4477..3b3cd137e40 100644 --- a/src/main/java/org/skriptlang/skript/common/function/DefaultFunctionImpl.java +++ b/src/main/java/org/skriptlang/skript/common/function/DefaultFunctionImpl.java @@ -77,7 +77,11 @@ final class DefaultFunctionImpl extends ch.njol.skript.lang.function.Function } } - if (arg.length == 1 && parameter.single()) { + // check parameter plurality before arg length, since plural params accept arrays of length 1 + if (parameter.single()) { + if (arg.length != 1) + return null; + assert parameter.type().isAssignableFrom(arg[0].getClass()) : "argument type %s does not match parameter type %s".formatted(parameter.type().getSimpleName(), arg[0].getClass().getSimpleName()); From 1a2c19248574c35b532862b9a0656c52888a2f0f Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Tue, 16 Dec 2025 14:43:59 -0800 Subject: [PATCH 9/9] Update src/main/java/org/skriptlang/skript/log/runtime/RuntimeErrorCatcher.java Co-authored-by: Efnilite <35348263+Efnilite@users.noreply.github.com> --- .../org/skriptlang/skript/log/runtime/RuntimeErrorCatcher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/skriptlang/skript/log/runtime/RuntimeErrorCatcher.java b/src/main/java/org/skriptlang/skript/log/runtime/RuntimeErrorCatcher.java index c8e53725f3a..694c66f07af 100644 --- a/src/main/java/org/skriptlang/skript/log/runtime/RuntimeErrorCatcher.java +++ b/src/main/java/org/skriptlang/skript/log/runtime/RuntimeErrorCatcher.java @@ -15,7 +15,7 @@ * A {@link RuntimeErrorConsumer} to be used in {@link RuntimeErrorManager} to catch {@link RuntimeError}s. * This should always be used with {@link #start()} and {@link #stop()}. */ -public class RuntimeErrorCatcher implements RuntimeErrorConsumer, AutoCloseable{ +public class RuntimeErrorCatcher implements RuntimeErrorConsumer, AutoCloseable { private List storedConsumers = new ArrayList<>();