diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java index 216bc6687d8..5db18a0f246 100644 --- a/src/main/java/ch/njol/skript/Skript.java +++ b/src/main/java/ch/njol/skript/Skript.java @@ -68,6 +68,9 @@ import org.skriptlang.skript.bukkit.registration.BukkitSyntaxInfos; import org.skriptlang.skript.bukkit.text.TextComponentParser; import org.skriptlang.skript.common.CommonModule; +import org.skriptlang.skript.common.elements.functions.MathFunctions; +import org.skriptlang.skript.common.elements.functions.StringFunctions; +import org.skriptlang.skript.common.elements.functions.TimeFunctions; import org.skriptlang.skript.docs.Origin; import org.skriptlang.skript.lang.comparator.Comparator; import org.skriptlang.skript.lang.comparator.Comparators; @@ -554,7 +557,6 @@ public void load(org.skriptlang.skript.addon.SkriptAddon addon) { throw new RuntimeException(e); } BukkitEventValues.register(eventValueRegistry); - new DefaultFunctions(); new DefaultOperations(); } diff --git a/src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java b/src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java deleted file mode 100644 index 818b402af0c..00000000000 --- a/src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java +++ /dev/null @@ -1,1084 +0,0 @@ -package ch.njol.skript.classes.data; - -import ch.njol.skript.Skript; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.KeyedValue; -import ch.njol.skript.lang.function.Functions; -import ch.njol.skript.lang.function.Parameter; -import ch.njol.skript.lang.function.SimpleJavaFunction; -import ch.njol.skript.lang.util.SimpleLiteral; -import ch.njol.skript.registrations.Classes; -import ch.njol.skript.registrations.DefaultClasses; -import ch.njol.skript.util.*; -import ch.njol.skript.util.Date; -import ch.njol.util.Math2; -import ch.njol.util.StringUtils; -import ch.njol.util.coll.CollectionUtils; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.OfflinePlayer; -import org.bukkit.World; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Player; -import org.bukkit.util.Vector; -import org.jetbrains.annotations.Nullable; -import org.joml.AxisAngle4f; -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; -import java.math.RoundingMode; -import java.text.DecimalFormat; -import java.util.*; -import java.util.concurrent.atomic.AtomicBoolean; - -public class DefaultFunctions { - - private static String str(double n) { - return StringUtils.toString(n, 4); - } - - private static final DecimalFormat DEFAULT_INTEGER_FORMAT = new DecimalFormat("###,###"); - private static final DecimalFormat DEFAULT_DECIMAL_FORMAT = new DecimalFormat("###,###.##"); - - static { - SkriptAddon skript = Skript.instance(); - Parameter[] numberParam = new Parameter[] {new Parameter<>("n", DefaultClasses.NUMBER, true, null)}; - Parameter[] numbersParam = new Parameter[] {new Parameter<>("ns", DefaultClasses.NUMBER, false, null)}; - - // basic math functions - - Functions.register(DefaultFunction.builder(skript, "floor", Long.class) - .description("Rounds a number down, i.e. returns the closest integer smaller than or equal to the argument.") - .examples("floor(2.34) = 2", "floor(2) = 2", "floor(2.99) = 2") - .since("2.2") - .parameter("n", Number.class) - .build(args -> { - Number value = args.get("n"); - - if (value instanceof Long l) - return l; - - return Math2.floor(value.doubleValue()); - })); - - Functions.registerFunction(new SimpleJavaFunction("round", new Parameter[] {new Parameter<>("n", DefaultClasses.NUMBER, true, null), new Parameter<>("d", DefaultClasses.NUMBER, true, new SimpleLiteral(0, false))}, DefaultClasses.NUMBER, true) { - @Override - public Number[] executeSimple(Object[][] params) { - if (params[0][0] instanceof Long longValue) - return new Long[] {longValue}; - double value = ((Number) params[0][0]).doubleValue(); - if (!Double.isFinite(value)) - return new Double[] {value}; - - double placementDouble = ((Number) params[1][0]).doubleValue(); - if (!Double.isFinite(placementDouble) || placementDouble >= Integer.MAX_VALUE || placementDouble <= Integer.MIN_VALUE) - return new Double[] {Double.NaN}; - int placement = (int) placementDouble; - if (placement == 0) - return new Long[] {Math2.round(value)}; - if (placement >= 0) { - BigDecimal decimal = new BigDecimal(Double.toString(value)); - decimal = decimal.setScale(placement, RoundingMode.HALF_UP); - return new Double[] {decimal.doubleValue()}; - } - long rounded = Math2.round(value); - return new Double[] {(int) Math2.round(rounded * Math.pow(10.0, placement)) / Math.pow(10.0, placement)}; - } - }.description("Rounds a number, i.e. returns the closest integer to the argument. Place a second argument to define the decimal placement.") - .examples("round(2.34) = 2", "round(2) = 2", "round(2.99) = 3", "round(2.5) = 3") - .since("2.2, 2.7 (decimal placement)")); - - Functions.registerFunction(new SimpleJavaFunction("ceil", numberParam, DefaultClasses.LONG, true) { - @Override - public Long[] executeSimple(Object[][] params) { - if (params[0][0] instanceof Long) - return new Long[] {(Long) params[0][0]}; - return new Long[] {Math2.ceil(((Number) params[0][0]).doubleValue())}; - } - }.description("Rounds a number up, i.e. returns the closest integer larger than or equal to the argument.") - .examples("ceil(2.34) = 3", "ceil(2) = 2", "ceil(2.99) = 3") - .since("2.2")); - - Functions.registerFunction(new SimpleJavaFunction("ceiling", numberParam, DefaultClasses.LONG, true) { - @Override - public Long[] executeSimple(Object[][] params) { - if (params[0][0] instanceof Long) - return new Long[] {(Long) params[0][0]}; - return new Long[] {Math2.ceil(((Number) params[0][0]).doubleValue())}; - } - }.description("Alias of ceil.") - .examples("ceiling(2.34) = 3", "ceiling(2) = 2", "ceiling(2.99) = 3") - .since("2.2")); - - Functions.registerFunction(new SimpleJavaFunction("abs", numberParam, DefaultClasses.NUMBER, true) { - @Override - public Number[] executeSimple(Object[][] params) { - Number n = (Number) params[0][0]; - if (n instanceof Byte || n instanceof Short || n instanceof Integer || n instanceof Long) - return new Long[] {Math.abs(n.longValue())}; - return new Double[] {Math.abs(n.doubleValue())}; - } - }.description("Returns the absolute value of the argument, i.e. makes the argument positive.") - .examples("abs(3) = 3", "abs(-2) = 2") - .since("2.2")); - - Functions.registerFunction(new SimpleJavaFunction("mod", new Parameter[] {new Parameter<>("d", DefaultClasses.NUMBER, true, null), new Parameter<>("m", DefaultClasses.NUMBER, true, null)}, DefaultClasses.NUMBER, true) { - @Override - public Number[] executeSimple(Object[][] params) { - Number d = (Number) params[0][0]; - Number m = (Number) params[1][0]; - double mm = m.doubleValue(); - if (mm == 0) - return new Double[] {Double.NaN}; - return new Double[] {Math2.mod(d.doubleValue(), mm)}; - } - }.description("Returns the modulo of the given arguments, i.e. the remainder of the division d/m, where d and m are the arguments of this function.", - "The returned value is always positive. Returns NaN (not a number) if the second argument is zero.") - .examples("mod(3, 2) = 1", "mod(256436, 100) = 36", "mod(-1, 10) = 9") - .since("2.2")); - - Functions.registerFunction(new SimpleJavaFunction("exp", numberParam, DefaultClasses.NUMBER, true) { - @Override - public Number[] executeSimple(Object[][] params) { - return new Double[] {Math.exp(((Number) params[0][0]).doubleValue())}; - } - }.description("The exponential function. You probably don't need this if you don't know what this is.") - .examples("exp(0) = 1", "exp(1) = " + str(Math.exp(1))) - .since("2.2")); - - Functions.registerFunction(new SimpleJavaFunction("ln", numberParam, DefaultClasses.NUMBER, true) { - @Override - public Number[] executeSimple(Object[][] params) { - return new Double[] {Math.log(((Number) params[0][0]).doubleValue())}; - } - }.description("The natural logarithm. You probably don't need this if you don't know what this is.", - "Returns NaN (not a number) if the argument is negative.") - .examples("ln(1) = 0", "ln(exp(5)) = 5", "ln(2) = " + StringUtils.toString(Math.log(2), 4)) - .since("2.2")); - - Functions.registerFunction(new SimpleJavaFunction("log", new Parameter[] {new Parameter<>("n", DefaultClasses.NUMBER, true, null), new Parameter<>("base", DefaultClasses.NUMBER, true, new SimpleLiteral(10, false))}, DefaultClasses.NUMBER, true) { - @Override - public Number[] executeSimple(Object[][] params) { - return new Double[] {Math.log10(((Number) params[0][0]).doubleValue()) / Math.log10(((Number) params[1][0]).doubleValue())}; - } - }.description("A logarithm, with base 10 if none is specified. This is the inverse operation to exponentiation (for positive bases only), i.e. log(base ^ exponent, base) = exponent for any positive number 'base' and any number 'exponent'.", - "Another useful equation is base ^ log(a, base) = a for any numbers 'base' and 'a'.", - "Please note that due to how numbers are represented in computers, these equations do not hold for all numbers, as the computed values may slightly differ from the correct value.", - "Returns NaN (not a number) if any of the arguments are negative.") - .examples("log(100) = 2 # 10^2 = 100", "log(16, 2) = 4 # 2^4 = 16") - .since("2.2")); - - Functions.registerFunction(new SimpleJavaFunction("sqrt", numberParam, DefaultClasses.NUMBER, true) { - @Override - public Number[] executeSimple(Object[][] params) { - return new Double[] {Math.sqrt(((Number) params[0][0]).doubleValue())}; - } - }.description("The square root, which is the inverse operation to squaring a number (for positive numbers only). This is the same as (argument) ^ (1/2) – other roots can be calculated via number ^ (1/root), e.g. set {_l} to {_volume}^(1/3).", - "Returns NaN (not a number) if the argument is negative.") - .examples("sqrt(4) = 2", "sqrt(2) = " + str(Math.sqrt(2)), "sqrt(-1) = " + str(Math.sqrt(-1))) - .since("2.2")); - - // trigonometry - - Functions.registerFunction(new SimpleJavaFunction("sin", numberParam, DefaultClasses.NUMBER, true) { - @Override - public Number[] executeSimple(Object[][] params) { - return new Double[] {Math.sin(Math.toRadians(((Number) params[0][0]).doubleValue()))}; - } - }.description("The sine function. It starts at 0° with a value of 0, goes to 1 at 90°, back to 0 at 180°, to -1 at 270° and then repeats every 360°. Uses degrees, not radians.") - .examples("sin(90) = 1", "sin(60) = " + str(Math.sin(Math.toRadians(60)))) - .since("2.2")); - - Functions.registerFunction(new SimpleJavaFunction("cos", numberParam, DefaultClasses.NUMBER, true) { - @Override - public Number[] executeSimple(Object[][] params) { - return new Double[] {Math.cos(Math.toRadians(((Number) params[0][0]).doubleValue()))}; - } - }.description("The cosine function. This is basically the sine shifted by 90°, i.e. cos(a) = sin(a + 90°), for any number a. Uses degrees, not radians.") - .examples("cos(0) = 1", "cos(90) = 0") - .since("2.2")); - - Functions.registerFunction(new SimpleJavaFunction("tan", numberParam, DefaultClasses.NUMBER, true) { - @Override - public Number[] executeSimple(Object[][] params) { - return new Double[] {Math.tan(Math.toRadians(((Number) params[0][0]).doubleValue()))}; - } - }.description("The tangent function. This is basically sin(arg)/cos(arg). Uses degrees, not radians.") - .examples("tan(0) = 0", "tan(45) = 1", "tan(89.99) = " + str(Math.tan(Math.toRadians(89.99)))) - .since("2.2")); - - Functions.registerFunction(new SimpleJavaFunction("asin", numberParam, DefaultClasses.NUMBER, true) { - @Override - public Number[] executeSimple(Object[][] params) { - return new Double[] {Math.toDegrees(Math.asin(((Number) params[0][0]).doubleValue()))}; - } - }.description("The inverse of the sine, also called arcsin. Returns result in degrees, not radians. Only returns values from -90 to 90.") - .examples("asin(0) = 0", "asin(1) = 90", "asin(0.5) = " + str(Math.toDegrees(Math.asin(0.5)))) - .since("2.2")); - - Functions.registerFunction(new SimpleJavaFunction("acos", numberParam, DefaultClasses.NUMBER, true) { - @Override - public Number[] executeSimple(Object[][] params) { - return new Double[] {Math.toDegrees(Math.acos(((Number) params[0][0]).doubleValue()))}; - } - }.description("The inverse of the cosine, also called arccos. Returns result in degrees, not radians. Only returns values from 0 to 180.") - .examples("acos(0) = 90", "acos(1) = 0", "acos(0.5) = " + str(Math.toDegrees(Math.asin(0.5)))) - .since("2.2")); - - Functions.registerFunction(new SimpleJavaFunction("atan", numberParam, DefaultClasses.NUMBER, true) { - @Override - public Number[] executeSimple(Object[][] params) { - return new Double[] {Math.toDegrees(Math.atan(((Number) params[0][0]).doubleValue()))}; - } - }.description("The inverse of the tangent, also called arctan. Returns result in degrees, not radians. Only returns values from -90 to 90.") - .examples("atan(0) = 0", "atan(1) = 45", "atan(10000) = " + str(Math.toDegrees(Math.atan(10000)))) - .since("2.2")); - - Functions.registerFunction(new SimpleJavaFunction("atan2", new Parameter[] { - new Parameter<>("x", DefaultClasses.NUMBER, true, null), - new Parameter<>("y", DefaultClasses.NUMBER, true, null) - }, DefaultClasses.NUMBER, true) { - @Override - public Number[] executeSimple(Object[][] params) { - return new Double[] {Math.toDegrees(Math.atan2(((Number) params[1][0]).doubleValue(), ((Number) params[0][0]).doubleValue()))}; - } - }.description("Similar to atan, but requires two coordinates and returns values from -180 to 180.", - "The returned angle is measured counterclockwise in a standard mathematical coordinate system (x to the right, y to the top).") - .examples("atan2(0, 1) = 0", "atan2(10, 0) = 90", "atan2(-10, 5) = " + str(Math.toDegrees(Math.atan2(-10, 5)))) - .since("2.2")); - - // more stuff - - Functions.registerFunction(new SimpleJavaFunction("sum", numbersParam, DefaultClasses.NUMBER, true) { - @Override - public Number[] executeSimple(Object[][] params) { - Object[] ns = params[0]; - double sum = ((Number) ns[0]).doubleValue(); - for (int i = 1; i < ns.length; i++) - sum += ((Number) ns[i]).doubleValue(); - return new Double[] {sum}; - } - }.description("Sums a list of numbers.") - .examples("sum(1) = 1", "sum(2, 3, 4) = 9", "sum({some list variable::*})", "sum(2, {_v::*}, and the player's y-coordinate)") - .since("2.2")); - - Functions.registerFunction(new SimpleJavaFunction("product", numbersParam, DefaultClasses.NUMBER, true) { - @Override - public Number[] executeSimple(Object[][] params) { - Object[] ns = params[0]; - double product = ((Number) ns[0]).doubleValue(); - for (int i = 1; i < ns.length; i++) - product *= ((Number) ns[i]).doubleValue(); - return new Double[] {product}; - } - }.description("Calculates the product of a list of numbers.") - .examples("product(1) = 1", "product(2, 3, 4) = 24", "product({some list variable::*})", "product(2, {_v::*}, and the player's y-coordinate)") - .since("2.2")); - - Functions.registerFunction(new SimpleJavaFunction("max", numbersParam, DefaultClasses.NUMBER, true) { - @Override - public Number[] executeSimple(Object[][] params) { - Object[] ns = params[0]; - double max = ((Number) ns[0]).doubleValue(); - for (int i = 1; i < ns.length; i++) { - double d = ((Number) ns[i]).doubleValue(); - if (d > max || Double.isNaN(max)) - max = d; - } - return new Double[] {max}; - } - }.description("Returns the maximum number from a list of numbers.") - .examples("max(1) = 1", "max(1, 2, 3, 4) = 4", "max({some list variable::*})") - .since("2.2")); - - Functions.registerFunction(new SimpleJavaFunction("min", numbersParam, DefaultClasses.NUMBER, true) { - @Override - public Number[] executeSimple(Object[][] params) { - Object[] ns = params[0]; - double min = ((Number) ns[0]).doubleValue(); - for (int i = 1; i < ns.length; i++) { - double d = ((Number) ns[i]).doubleValue(); - if (d < min || Double.isNaN(min)) - min = d; - } - return new Double[] {min}; - } - }.description("Returns the minimum number from a list of numbers.") - .examples("min(1) = 1", "min(1, 2, 3, 4) = 1", "min({some list variable::*})") - .since("2.2")); - - Functions.registerFunction(new SimpleJavaFunction("clamp", new Parameter[] { - new Parameter<>("values", DefaultClasses.NUMBER, false, null, true), - new Parameter<>("min", DefaultClasses.NUMBER, true, null), - new Parameter<>("max", DefaultClasses.NUMBER, true, null) - }, DefaultClasses.NUMBER, false, new Contract() { - - @Override - public boolean isSingle(Expression... arguments) { - return arguments[0].isSingle(); - } - - @Override - public Class getReturnType(Expression... arguments) { - return Number.class; - } - }) { - @Override - public @Nullable Number[] executeSimple(Object[][] params) { - //noinspection unchecked - KeyedValue[] values = (KeyedValue[]) params[0]; - Double[] clampedValues = new Double[values.length]; - double min = ((Number) params[1][0]).doubleValue(); - double max = ((Number) params[2][0]).doubleValue(); - // we'll be nice and swap them if they're in the wrong order - double trueMin = Math.min(min, max); - double trueMax = Math.max(min, max); - for (int i = 0; i < values.length; i++) { - double value = values[i].value().doubleValue(); - clampedValues[i] = Math.max(Math.min(value, trueMax), trueMin); - } - setReturnedKeys(KeyedValue.unzip(values).keys().toArray(new String[0])); - return clampedValues; - } - }).description("Clamps one or more values between two numbers.", "This function retains indices") - .examples( - "clamp(5, 0, 10) = 5", - "clamp(5.5, 0, 5) = 5", - "clamp(0.25, 0, 0.5) = 0.25", - "clamp(5, 7, 10) = 7", - "clamp((5, 0, 10, 9, 13), 7, 10) = (7, 7, 10, 9, 10)", - "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("2.14") - .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"); - 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("2.14") - .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"); - 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[] { - new Parameter<>("name", DefaultClasses.STRING, true, null) - }, DefaultClasses.WORLD, true) { - @Override - public World[] executeSimple(Object[][] params) { - World w = Bukkit.getWorld((String) params[0][0]); - return w == null ? new World[0] : new World[] {w}; - } - }).description("Gets a world from its name.") - .examples("set {_nether} to world(\"%{_world}%_nether\")") - .since("2.2"); - - Functions.register(DefaultFunction.builder(skript, "location", Location.class) - .description( - "Creates a location from a world and 3 coordinates, with an optional yaw and pitch.", - "If for whatever reason the world is not found, it will fallback to the server's main world." - ) - .examples(""" - # TELEPORTING - teleport player to location(1,1,1, world "world") - teleport player to location(1,1,1, world "world", 100, 0) - teleport player to location(1,1,1, world "world", yaw of player, pitch of player) - teleport player to location(1,1,1, world of player) - teleport player to location(1,1,1, world("world")) - teleport player to location({_x}, {_y}, {_z}, {_w}, {_yaw}, {_pitch}) - - # SETTING BLOCKS - set block at location(1,1,1, world "world") to stone - set block at location(1,1,1, world "world", 100, 0) to stone - set block at location(1,1,1, world of player) to stone - set block at location(1,1,1, world("world")) to stone - set block at location({_x}, {_y}, {_z}, {_w}) to stone - - # USING VARIABLES - set {_l1} to location(1,1,1) - set {_l2} to location(10,10,10) - set blocks within {_l1} and {_l2} to stone - if player is within {_l1} and {_l2}: - - # OTHER - kill all entities in radius 50 around location(1,65,1, world "world") - delete all entities in radius 25 around location(50,50,50, world "world_nether") - ignite all entities in radius 25 around location(1,1,1, world of player) - """ - ) - .since("2.2") - .parameter("x", Number.class) - .parameter("y", Number.class) - .parameter("z", Number.class) - .parameter("world", World.class, Modifier.OPTIONAL) - .parameter("yaw", Float.class, Modifier.OPTIONAL) - .parameter("pitch", Float.class, Modifier.OPTIONAL) - .build(args -> { - World world = args.getOrDefault("world", Bukkit.getWorlds().get(0)); - - return new Location(world, - args.get("x").doubleValue(), args.get("y").doubleValue(), args.get("z").doubleValue(), - args.getOrDefault("yaw", 0f), args.getOrDefault("pitch", 0f)); - })); - - Functions.registerFunction(new SimpleJavaFunction("date", new Parameter[] { - new Parameter<>("year", DefaultClasses.NUMBER, true, null), - new Parameter<>("month", DefaultClasses.NUMBER, true, null), - new Parameter<>("day", DefaultClasses.NUMBER, true, null), - new Parameter<>("hour", DefaultClasses.NUMBER, true, new SimpleLiteral(0, true)), - new Parameter<>("minute", DefaultClasses.NUMBER, true, new SimpleLiteral(0, true)), - new Parameter<>("second", DefaultClasses.NUMBER, true, new SimpleLiteral(0, true)), - new Parameter<>("millisecond", DefaultClasses.NUMBER, true, new SimpleLiteral(0, true)), - new Parameter<>("zone_offset", DefaultClasses.NUMBER, true, new SimpleLiteral(Double.NaN, true)), - new Parameter<>("dst_offset", DefaultClasses.NUMBER, true, new SimpleLiteral(Double.NaN, true)) - }, DefaultClasses.DATE, true) { - private final int[] fields = { - Calendar.YEAR, Calendar.MONTH, Calendar.DAY_OF_MONTH, - Calendar.HOUR_OF_DAY, Calendar.MINUTE, Calendar.SECOND, Calendar.MILLISECOND, - Calendar.ZONE_OFFSET, Calendar.DST_OFFSET - }; - private final int[] offsets = { - 0, -1, 0, - 0, 0, 0, 0, - 0, 0 - }; - private final double[] scale = { - 1, 1, 1, - 1, 1, 1, 1, - 1000 * 60, 1000 * 60 - }; - private final double[] relations = { - 1. / 12, 1. / 30, - 1. / 24, 1. / 60, 1. / 60, 1. / 1000, - 0, 0, - 0 - }; - - { - int length = getSignature().getMaxParameters(); - assert fields.length == length - && offsets.length == length - && scale.length == length - && relations.length == length; - } - - @Override - public Date[] executeSimple(Object[][] params) { - Calendar c = Calendar.getInstance(); - c.setLenient(true); - double carry = 0; - for (int i = 0; i < fields.length; i++) { - int field = fields[i]; - Number n = (Number) params[i][0]; - - double value = n.doubleValue() * scale[i] + offsets[i] + carry; - int v = (int) Math2.floor(value); - carry = (value - v) * relations[i]; - //noinspection MagicConstant - c.set(field, v); - } - - return new Date[] {new Date(c.getTimeInMillis(), c.getTimeZone())}; - } - }.description("Creates a date from a year, month, and day, and optionally also from hour, minute, second and millisecond.", - "A time zone and DST offset can be specified as well (in minutes), if they are left out the server's time zone and DST offset are used (the created date will not retain this information).") - .examples("date(2014, 10, 1) # 0:00, 1st October 2014", "date(1990, 3, 5, 14, 30) # 14:30, 5th May 1990", "date(1999, 12, 31, 23, 59, 59, 999, -3*60, 0) # almost year 2000 in parts of Brazil (-3 hours offset, no DST)") - .since("2.2")); - - Functions.register(DefaultFunction.builder(skript, "vector", Vector.class) - .description("Creates a vector from a single argument. Equivalent to vector(n, n, n).") - .examples("vector(1) # = vector(1, 1, 1)") - .since("2.15") - .parameter("n", Number.class) - .build(args -> { - double value = args.get("n").doubleValue(); - return new Vector(value, value, value); - })); - - Functions.register(DefaultFunction.builder(skript, "vector", Vector.class) - .description("Creates a new vector, which can be used with various expressions, effects and functions.") - .examples("vector(0, 0, 0)") - .since("2.2-dev23") - .parameter("x", Number.class) - .parameter("y", Number.class) - .parameter("z", Number.class) - .build(args -> new Vector( - args.get("x").doubleValue(), - args.get("y").doubleValue(), - args.get("z").doubleValue() - ))); - - Functions.registerFunction(new SimpleJavaFunction("calcExperience", new Parameter[] { - new Parameter<>("level", DefaultClasses.LONG, true, null) - }, DefaultClasses.LONG, true) { - @Override - public Long[] executeSimple(Object[][] params) { - long level = (long) params[0][0]; - long exp; - if (level <= 0) { - exp = 0; - } else if (level >= 1 && level <= 15) { - exp = level * level + 6 * level; - } else if (level >= 16 && level <= 30) { // Truncating decimal parts probably works - exp = (int) (2.5 * level * level - 40.5 * level + 360); - } else { // Half experience points do not exist, anyway - exp = (int) (4.5 * level * level - 162.5 * level + 2220); - } - - return new Long[] {exp}; - } - - }.description("Calculates the total amount of experience needed to achieve given level from scratch in Minecraft.") - .since("2.2-dev32")); - - Functions.register(DefaultFunction.builder(skript, "rgb", Color.class) - .description(""" - Returns a RGB color from the given red, green and blue parameters. - Alpha values can be added optionally but these only take affect in certain situations, like text display backgrounds.""") - .examples( - "dye player's leggings rgb(120, 30, 45)", - "set the colour of a text display to rgb(10, 50, 100, 50)" - ) - .since("2.5, 2.10 (alpha)") - .parameter("red", Long.class, Modifier.ranged(0, 255)) - .parameter("green", Long.class, Modifier.ranged(0, 255)) - .parameter("blue", Long.class, Modifier.ranged(0, 255)) - .parameter("alpha", Long.class, Modifier.ranged(0, 255), Modifier.OPTIONAL) - .build(args -> ColorRGB.fromRGBA( - args.get("red").intValue(), - args.get("green").intValue(), - args.get("blue").intValue(), - args.getOrDefault("alpha", 255L).intValue() - ))); - - Functions.register(DefaultFunction.builder(skript, "player", Player.class) - .description( - "Returns an online player from their name or UUID, if player is offline function will return nothing.", - "Setting 'getExactPlayer' parameter to true will return the player whose name is exactly equal to the provided name instead of returning a player that their name starts with the provided name." - ) - .examples( - "set {_p} to player(\"Notch\") # will return an online player whose name is or starts with 'Notch'", - "set {_p} to player(\"Notch\", true) # will return the only online player whose name is 'Notch'", - "set {_p} to player(\"069a79f4-44e9-4726-a5be-fca90e38aaf5\") # if player is offline" - ) - .since("2.8.0") - .parameter("nameOrUUID", String.class) - .parameter("getExactPlayer", Boolean.class, Modifier.OPTIONAL) - .build(args -> { - String name = args.get("nameOrUUID"); - boolean isExact = args.getOrDefault("getExactPlayer", false); - - UUID uuid = null; - if (name.length() > 16 || name.contains("-")) { - if (Utils.isValidUUID(name)) - uuid = UUID.fromString(name); - } - - return uuid != null ? Bukkit.getPlayer(uuid) : (isExact ? Bukkit.getPlayerExact(name) : Bukkit.getPlayer(name)); - })); - - { // offline player function - // TODO - remove this when Spigot support is dropped - boolean hasIfCached = Skript.methodExists(Bukkit.class, "getOfflinePlayerIfCached", String.class); - - List> params = new ArrayList<>(); - params.add(new Parameter<>("nameOrUUID", DefaultClasses.STRING, true, null)); - if (hasIfCached) - params.add(new Parameter<>("allowLookups", DefaultClasses.BOOLEAN, true, new SimpleLiteral<>(true, true))); - - Functions.registerFunction(new SimpleJavaFunction("offlineplayer", params.toArray(new Parameter[0]), - DefaultClasses.OFFLINE_PLAYER, true) { - @Override - public OfflinePlayer[] executeSimple(Object[][] params) { - String name = (String) params[0][0]; - UUID uuid = null; - if (name.length() > 16 || name.contains("-")) { // shortcut - if (Utils.isValidUUID(name)) - uuid = UUID.fromString(name); - } - OfflinePlayer result; - - if (uuid != null) { - result = Bukkit.getOfflinePlayer(uuid); // doesn't do lookups - } else if (hasIfCached && !((Boolean) params[1][0])) { - result = Bukkit.getOfflinePlayerIfCached(name); - if (result == null) - return new OfflinePlayer[0]; - } else { - result = Bukkit.getOfflinePlayer(name); - } - - return CollectionUtils.array(result); - } - - }).description( - "Returns a offline player from their name or UUID. This function will still return the player if they're online. " + - "If Paper 1.16.5+ is used, the 'allowLookup' parameter can be set to false to prevent this function from doing a " + - "web lookup for players who have not joined before. Lookups can cause lag spikes of up to multiple seconds, so " + - "use offline players with caution." - ) - .examples( - "set {_p} to offlineplayer(\"Notch\")", - "set {_p} to offlineplayer(\"069a79f4-44e9-4726-a5be-fca90e38aaf5\")", - "set {_p} to offlineplayer(\"Notch\", false)" - ) - .since("2.8.0, 2.9.0 (prevent lookups)"); - } // end offline player function - - Functions.register(DefaultFunction.builder(skript, "entity", Entity.class) - .description( - "Returns an entity from a given UUID.", - "If the entity is unloaded or an offline player, the function will return nothing." - ) - .examples( - "set {_entity} to entity({uuid})", - "kill entity({uuid})", - "set {_type} to type of entity({uuid})" - ) - .since("2.16") - .parameter("uuid", UUID.class) - .build(args -> { - UUID uuid = args.get("uuid"); - return Bukkit.getEntity(uuid); - }) - ); - - Functions.registerFunction(new SimpleJavaFunction("isNaN", numberParam, DefaultClasses.BOOLEAN, true) { - @Override - public Boolean[] executeSimple(Object[][] params) { - return new Boolean[] {Double.isNaN(((Number) params[0][0]).doubleValue())}; - } - }).description("Returns true if the input is NaN (not a number).") - .examples("isNaN(0) # false", "isNaN(0/0) # true", "isNaN(sqrt(-1)) # true") - .since("2.8.0"); - - Functions.register(DefaultFunction.builder(skript, "concat", String.class) - .description("Joins the provided texts (and other things) into a single text.") - .examples( - "concat(\"hello \", \"there\") # hello there", - "concat(\"foo \", 100, \" bar\") # foo 100 bar" - ) - .since("2.9.0") - .parameter("texts", Object[].class) - .build(args -> { - StringBuilder builder = new StringBuilder(); - Object[] objects = args.get("texts"); - for (Object object : objects) { - builder.append(Classes.toString(object)); - } - return builder.toString(); - })); - - // joml functions - for display entities - { - if (Skript.classExists("org.joml.Quaternionf")) { - Functions.registerFunction(new SimpleJavaFunction<>("quaternion", new Parameter[]{ - new Parameter<>("w", DefaultClasses.NUMBER, true, null), - new Parameter<>("x", DefaultClasses.NUMBER, true, null), - new Parameter<>("y", DefaultClasses.NUMBER, true, null), - new Parameter<>("z", DefaultClasses.NUMBER, true, null) - }, Classes.getExactClassInfo(Quaternionf.class), true) { - @Override - public Quaternionf[] executeSimple(Object[][] params) { - double w = ((Number) params[0][0]).doubleValue(); - double x = ((Number) params[1][0]).doubleValue(); - double y = ((Number) params[2][0]).doubleValue(); - double z = ((Number) params[3][0]).doubleValue(); - return CollectionUtils.array(new Quaternionf(x, y, z, w)); - } - }) - .description("Returns a quaternion from the given W, X, Y and Z parameters. ") - .examples("quaternion(1, 5.6, 45.21, 10)") - .since("2.10"); - } - - if (Skript.classExists("org.joml.AxisAngle4f")) { - Functions.registerFunction(new SimpleJavaFunction<>("axisAngle", new Parameter[]{ - new Parameter<>("angle", DefaultClasses.NUMBER, true, null), - new Parameter<>("axis", DefaultClasses.VECTOR, true, null) - }, Classes.getExactClassInfo(Quaternionf.class), true) { - @Override - public Quaternionf[] executeSimple(Object[][] params) { - float angle = (float) (((Number) params[0][0]).floatValue() / 180 * Math.PI); - Vector v = ((Vector) params[1][0]); - if (v.isZero() || !Double.isFinite(v.getX()) || !Double.isFinite(v.getY()) || !Double.isFinite(v.getZ())) - return new Quaternionf[0]; - Vector3f axis = ((Vector) params[1][0]).toVector3f(); - return CollectionUtils.array(new Quaternionf(new AxisAngle4f(angle, axis))); - } - }) - .description("Returns a quaternion from the given angle (in degrees) and axis (as a vector). This represents a rotation around the given axis by the given angle.") - .examples("axisangle(90, (vector from player's facing))") - .since("2.10"); - } - } // end joml functions - - Functions.registerFunction(new SimpleJavaFunction<>("formatNumber", new Parameter[]{ - new Parameter<>("number", DefaultClasses.NUMBER, true, null), - new Parameter<>("format", DefaultClasses.STRING, true, new SimpleLiteral<>("", true)) - }, DefaultClasses.STRING, true) { - @Override - public String[] executeSimple(Object[][] params) { - Number number = (Number) params[0][0]; - String format = (String) params[1][0]; - - if (format.isEmpty()) { - if (number instanceof Double || number instanceof Float) { - return new String[]{DEFAULT_DECIMAL_FORMAT.format(number)}; - } else { - return new String[]{DEFAULT_INTEGER_FORMAT.format(number)}; - } - } - - try { - return new String[]{new DecimalFormat(format).format(number)}; - } catch (IllegalArgumentException e) { - return null; // invalid format - } - } - }) - .description( - "Converts numbers to human-readable format. By default, '###,###' (e.g. '123,456,789') " + - "will be used for whole numbers and '###,###.##' (e.g. '123,456,789.00) will be used for decimal numbers. " + - "A hashtag '#' represents a digit, a comma ',' is used to separate numbers, and a period '.' is used for decimals. ", - "Will return none if the format is invalid.", - "For further reference, see this article.") - .examples( - "command /balance:", - "\taliases: bal", - "\texecutable by: players", - "\ttrigger:", - "\t\tset {_money} to formatNumber({money::%sender's uuid%})", - "\t\tsend \"Your balance: %{_money}%\" to sender") - .since("2.10"); - - Functions.registerFunction(new SimpleJavaFunction<>("uuid", new Parameter[]{ - new Parameter<>("uuid", DefaultClasses.STRING, true, null) - }, Classes.getExactClassInfo(UUID.class), true) { - @Override - public UUID[] executeSimple(Object[][] params) { - String uuid = (String) params[0][0]; - if (Utils.isValidUUID(uuid)) - return CollectionUtils.array(UUID.fromString(uuid)); - return new UUID[0]; - } - } - .description("Returns a UUID from the given string. The string must be in the format of a UUID.") - .examples("uuid(\"069a79f4-44e9-4726-a5be-fca90e38aaf5\")") - .since("2.11") - ); - - Functions.registerFunction(new SimpleJavaFunction("mean", new Parameter[]{ - new Parameter<>("numbers", DefaultClasses.NUMBER, false, null) - }, DefaultClasses.NUMBER, true) { - @Override - public Number @Nullable [] executeSimple(Object[][] params) { - Double total = 0d; - int length = params[0].length; - for (int i = 0; i < length; i++) { - Number number = (Number) params[0][i]; - if (Double.isInfinite(number.doubleValue()) || Double.isNaN(number.doubleValue())) - return null; - if (total.isInfinite() || total.isNaN()) - return null; - total += number.doubleValue() / length; - } - return new Number[]{total}; - } - }) - .description( - "Get the mean (average) of a list of numbers.", - "You cannot get the mean of a set of numbers that includes infinity or NaN." - ) - .examples( - "mean(1, 2, 3) = 2", - "mean(0, 5, 10) = 5", - "mean(13, 97, 376, 709) = 298.75" - ) - .since("2.11"); - - Functions.registerFunction(new SimpleJavaFunction("median", new Parameter[]{ - new Parameter<>("numbers", DefaultClasses.NUMBER, false, null) - }, DefaultClasses.NUMBER, true) { - @Override - public Number @Nullable [] executeSimple(Object[][] params) { - AtomicBoolean invalid = new AtomicBoolean(false); - // median requires the numbers to be sorted from lowest to biggest - Number[] sorted = Arrays.stream(params[0]) - .filter(object -> { - if (!(object instanceof Number number)) - return false; - if (Double.isNaN(number.doubleValue())) { - invalid.set(true); - return false; - } - return true; - }) - .map(object -> (Number) object) - .sorted(((o1, o2) -> { - Double n1 = o1.doubleValue(); - Double n2 = o2.doubleValue(); - return n1.compareTo(n2); - })) - .toArray(Number[]::new); - if (invalid.get()) - return null; - int size = sorted.length; - // If the size of numbers provided is odd, we can just grab the middle number - if (size % 2 == 1) - return new Number[]{sorted[Math2.ceil(size /2)]}; - // If not, we grab the rounded up and rounded down numbers, then get the average of those - int half = size / 2; - double first = (sorted[half - 1]).doubleValue(); - double second = (sorted[half]).doubleValue(); - double median = (first+second)/2; - return new Number[]{median}; - } - }) - .description( - "Get the middle value of a sorted list of numbers. " - + "If the list has an even number of values, the median is the average of the two middle numbers.", - "You cannot get the median of a set of numbers that includes NaN." - ) - .examples( - "median(1, 2, 3, 4, 5) = 3", - "median(1, 2, 3, 4, 5, 6) = 3.5", - "median(0, 123, 456, 789) = 289.5" - ) - .since("2.11"); - - Functions.registerFunction(new SimpleJavaFunction<>("factorial", new Parameter[]{ - new Parameter<>("number", DefaultClasses.NUMBER, true, null) - }, DefaultClasses.NUMBER, true) { - @Override - public Number @Nullable [] executeSimple(Object[][] params) { - Double number = ((Number) params[0][0]).doubleValue(); - if (number < 0) { - return null; - } else if (number <= 1) { // 0 and 1 - return new Number[]{1}; - } else if (number > 170) { - return new Number[]{Double.POSITIVE_INFINITY}; - } - Double result = 1d; - for (double i = number; i > 1; i--) { - if (result.isInfinite() || result.isNaN()) - break; - result *= i; - } - return new Number[]{result}; - } - }) - .description( - "Get the factorial of a number.", - "Getting the factorial of any number above 21 will return an approximation, not an exact value.", - "Any number after 170 will always return Infinity.", - "Should not be used to calculate permutations or combinations manually." - ) - .examples( - "factorial(0) = 1", - "factorial(3) = 3*2*1 = 6", - "factorial(5) = 5*4*3*2*1 = 120", - "factorial(171) = Infinity" - ) - .since("2.11"); - - Functions.registerFunction(new SimpleJavaFunction("root", new Parameter[]{ - new Parameter<>("n", DefaultClasses.NUMBER, true, null), - new Parameter<>("number", DefaultClasses.NUMBER, true, null) - }, DefaultClasses.NUMBER, true) { - @Override - public Number @Nullable [] executeSimple(Object[][] params) { - Double n = ((Number) params[0][0]).doubleValue(); - Double number = ((Number) params[1][0]).doubleValue(); - if (n == 0) { - return null; - } else if (n == 1) { - return new Number[]{number}; - } else if (n == 2) { - return new Number[]{Math.sqrt(number)}; - } - return new Number[]{Math.pow(number, (1 / n))}; - } - }) - .description("Calculates the nth root of a number.") - .examples( - "root(2, 4) = 2 # same as sqrt(4)", - "root(4, 16) = 2", - "root(-4, 16) = 0.5 # same as 16^(-1/4)" - ) - .since("2.11"); - - Functions.registerFunction(new SimpleJavaFunction("permutations", new Parameter[]{ - new Parameter<>("options", DefaultClasses.NUMBER, true, null), - new Parameter<>("selected", DefaultClasses.NUMBER, true, null) - }, DefaultClasses.NUMBER, true) { - @Override - public Number @Nullable [] executeSimple(Object[][] params) { - Double options = ((Number) params[0][0]).doubleValue(); - Double selected = ((Number) params[1][0]).doubleValue(); - if (selected > options || selected < 0) { // Illegal argument - return null; - } else if (selected.equals(0d)) { // Will always be 1 - return new Number[]{1}; - } else if (selected.equals(1d)) { // Will always be the number from 'options' - return new Number[]{options}; - } - // We can simplify this as there will always be a factorial that can cancel out - // Example: options = 10, selected = 2; 10!/(10-2)! = 10!/8! - // We can deduce that 10! = (10)(9)(8!) ; allowing the '8!' factorial to cancel out, leaving us with: (10)(9) - // Which allows us to start from 10 and go down to 10-2, but will never reach 8 as 'i' needs to be higher - Double result = 1d; - for (double i = options; i > options - selected; i--) { - if (result.isInfinite() || result.isNaN()) - break; - result *= i; - } - return new Number[]{result}; - } - }) - .description( - "Get the number of possible ordered arrangements from 1 to 'options' with each arrangement having a size equal to 'selected'", - "For example, permutations with 3 options and an arrangement size of 1, returns 3: (1), (2), (3)", - "Permutations with 3 options and an arrangement size of 2 returns 6: (1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)", - "Note that the bigger the 'options' and lower the 'selected' may result in approximations or even infinity values.", - "Permutations differ from combinations in that permutations account for the arrangement of elements within the set, " - + "whereas combinations focus on unique sets and ignore the order of elements.", - "Example: (1, 2) and (2, 1) are two distinct permutations because the positions of '1' and '2' are different, " - + "but they represent a single combination since order doesn't matter in combinations." - ) - .examples( - "permutations(10, 2) = 90", - "permutations(10, 4) = 5040", - "permutations(size of {some list::*}, 2)" - ) - .since("2.11"); - - Functions.registerFunction(new SimpleJavaFunction("combinations", new Parameter[]{ - new Parameter<>("options", DefaultClasses.NUMBER, true, null), - new Parameter<>("selected", DefaultClasses.NUMBER, true, null) - }, DefaultClasses.NUMBER, true) { - @Override - public Number @Nullable [] executeSimple(Object[][] params) { - Double options = ((Number) params[0][0]).doubleValue(); - Double selected = ((Number) params[1][0]).doubleValue(); - if (selected > options || selected < 0) { // Illegal arguments - return null; - } else if (selected.equals(0d)) { // Will always return 1 - return new Number[]{1}; - } else if (selected.equals(1d)) { // Will always be the number from 'options' - return new Number[]{options}; - } - // By the same reasoning from 'permutations' there will always be a factorial that can cancel out - // Example: options = 10, selected = 2 ; 10!/(10-2)!(2!) = 10!/(8!)(2!) - // 10! = (10)(9)(8!) ; the 8! cancel out, leaving us with: (10)(9)/2! - // 'top' will calculate the leftovers in the numerator: (10)(9) - Double top = 1d; - for (double i = options; i > options - selected; i--) { - if (top.isInfinite() || top.isNaN()) - return new Number[]{top}; - top *= i; - } - // 'bottom' will calculate the leftovers in the denominator: 2! - Double bottom = selected; - for (double i = selected - 1; i > 1; i--) { - if (bottom.isInfinite() || bottom.isNaN()) - break; - bottom *= i; - } - // Then we divide - return new Number[]{top/bottom}; - } - }) - .description( - "Get the number of possible sets from 1 to 'options' with each set having a size equal to 'selected'", - "For example, a combination with 3 options and a set size of 1, returns 3: (1), (2), (3)", - "A combination of 3 options with a set size of 2 returns 3: (1, 2), (1, 3), (2, 3)", - "Note that the bigger the 'options' and lower the 'selected' may result in approximations or even infinity values.", - "Combinations differ from permutations in that combinations focus on unique sets, ignoring the order of elements, " - + "whereas permutations account for the arrangement of elements within the set.", - "Example: (1, 2) and (2, 1) represent a single combination since order doesn't matter in combinations, " - + "but they are two distinct permutations because permutations consider the order." - ) - .examples( - "combinations(10, 8) = 45", - "combinations(5, 3) = 10", - "combinations(size of {some list::*}, 2)" - ) - .since("2.11"); - - } - -} diff --git a/src/main/java/ch/njol/skript/lang/function/ExprFunctionCall.java b/src/main/java/ch/njol/skript/lang/function/ExprFunctionCall.java index 3aa1baca66f..7e943162e1b 100644 --- a/src/main/java/ch/njol/skript/lang/function/ExprFunctionCall.java +++ b/src/main/java/ch/njol/skript/lang/function/ExprFunctionCall.java @@ -39,7 +39,7 @@ public ExprFunctionCall(FunctionReference reference, Class[] exp Class returnType = Utils.getComponentType(functionReturnType); if (CollectionUtils.containsSuperclass(expectedReturnTypes, returnType)) { // Function returns expected type already - this.returnTypes = new Class[] {returnType}; + this.returnTypes = new Class[]{returnType}; this.returnType = (Class) returnType; } else { // Return value needs to be converted @@ -55,12 +55,12 @@ public ExprFunctionCall(FunctionReference reference, Class[] exp if (execute == null) { values = null; } else if (!execute.getClass().isArray()) { - values = new Object[] {execute}; + values = new Object[]{execute}; } else { values = (Object[]) execute; } - String[] keys = reference.function().returnedKeys(); + String[] keys = reference.function().getReturnedKeys().toArray(new String[0]); reference.function().resetReturnValue(); //noinspection unchecked @@ -71,7 +71,7 @@ public ExprFunctionCall(FunctionReference reference, Class[] exp } Converters.convert(values, convertedValues, returnTypes); - if (keys != null) { + if (keys.length > 0) { for (int i = 0; i < convertedValues.length; i++) { if (convertedValues[i] == null) keys[i] = null; diff --git a/src/main/java/ch/njol/skript/lang/function/Function.java b/src/main/java/ch/njol/skript/lang/function/Function.java index ca9ccca28b1..b3eeb2e33d1 100644 --- a/src/main/java/ch/njol/skript/lang/function/Function.java +++ b/src/main/java/ch/njol/skript/lang/function/Function.java @@ -3,7 +3,6 @@ import ch.njol.skript.SkriptConfig; import ch.njol.skript.classes.ClassInfo; import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.KeyProviderExpression; import ch.njol.skript.lang.KeyedValue; import ch.njol.util.coll.CollectionUtils; import org.bukkit.Bukkit; @@ -16,6 +15,8 @@ import org.skriptlang.skript.common.function.ScriptParameter; import java.util.Arrays; +import java.util.Collections; +import java.util.SequencedCollection; /** * Functions can be called using arguments. @@ -188,6 +189,15 @@ public Class type() { return null; } + @Override + public final @NotNull SequencedCollection getReturnedKeys() { + String[] returnedKeys = returnedKeys(); + if (returnedKeys == null || returnedKeys.length == 0) { + return Collections.emptyList(); + } + return Arrays.asList(returnedKeys); + } + /** * Resets the return value of the {@code Function}. * Should be called right after execution. diff --git a/src/main/java/ch/njol/skript/lang/function/ScriptFunction.java b/src/main/java/ch/njol/skript/lang/function/ScriptFunction.java index 9bbba5cc8bb..1af513622bb 100644 --- a/src/main/java/ch/njol/skript/lang/function/ScriptFunction.java +++ b/src/main/java/ch/njol/skript/lang/function/ScriptFunction.java @@ -15,6 +15,7 @@ import org.skriptlang.skript.common.function.Parameters; import java.util.Arrays; +import java.util.SequencedCollection; public class ScriptFunction extends Function implements ReturnHandler { diff --git a/src/main/java/org/skriptlang/skript/bukkit/BukkitModule.java b/src/main/java/org/skriptlang/skript/bukkit/BukkitModule.java index ffa8d4bab03..8770ab62592 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/BukkitModule.java +++ b/src/main/java/org/skriptlang/skript/bukkit/BukkitModule.java @@ -13,6 +13,8 @@ import org.skriptlang.skript.bukkit.enchantments.EnchantmentModule; import org.skriptlang.skript.bukkit.entity.EntityModule; import org.skriptlang.skript.bukkit.fishing.FishingModule; +import org.skriptlang.skript.bukkit.functions.BukkitFunctions; +import org.skriptlang.skript.bukkit.functions.VectorFunctions; import org.skriptlang.skript.bukkit.input.InputModule; import org.skriptlang.skript.bukkit.item.ItemModule; import org.skriptlang.skript.bukkit.itemcomponents.ItemComponentModule; @@ -73,6 +75,9 @@ protected void initSelf(SkriptAddon addon) { Classes.registerClass(new PlayerClassInfo()); Classes.registerClass(new SlotClassInfo()); Classes.registerClass(new VectorClassInfo()); + + new BukkitFunctions(this, addon); + new VectorFunctions(this, addon); } @Override diff --git a/src/main/java/org/skriptlang/skript/bukkit/functions/BukkitFunctions.java b/src/main/java/org/skriptlang/skript/bukkit/functions/BukkitFunctions.java new file mode 100644 index 00000000000..4b396f454e8 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/functions/BukkitFunctions.java @@ -0,0 +1,187 @@ +package org.skriptlang.skript.bukkit.functions; + +import ch.njol.skript.lang.function.Functions; +import ch.njol.skript.util.Color; +import ch.njol.skript.util.ColorRGB; +import ch.njol.skript.util.Utils; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.OfflinePlayer; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.skriptlang.skript.addon.SkriptAddon; +import org.skriptlang.skript.bukkit.BukkitModule; +import org.skriptlang.skript.common.function.DefaultFunction; +import org.skriptlang.skript.common.function.Parameter.Modifier; + +import java.util.UUID; + +/** + * Contains all functions using Bukkit classes which cannot easily be grouped. + */ +public class BukkitFunctions { + + public BukkitFunctions(BukkitModule module, SkriptAddon addon) { + SkriptAddon skript = module.origin(addon).addon(); + + Functions.register(DefaultFunction.builder(skript, "world", World.class) + .description("Gets a world from its name.") + .examples("set {_nether} to world(\"%{_world}%_nether\")") + .since("2.2") + .parameter("name", String.class) + .build(args -> Bukkit.getWorld(args.get("name")))); + + Functions.register(DefaultFunction.builder(skript, "location", Location.class) + .description( + "Creates a location from a world and 3 coordinates, with an optional yaw and pitch.", + "If for whatever reason the world is not found, it will fallback to the server's main world." + ) + .examples(""" + # TELEPORTING + teleport player to location(1,1,1, world "world") + teleport player to location(1,1,1, world "world", 100, 0) + teleport player to location(1,1,1, world "world", yaw of player, pitch of player) + teleport player to location(1,1,1, world of player) + teleport player to location(1,1,1, world("world")) + teleport player to location({_x}, {_y}, {_z}, {_w}, {_yaw}, {_pitch}) + + # SETTING BLOCKS + set block at location(1,1,1, world "world") to stone + set block at location(1,1,1, world "world", 100, 0) to stone + set block at location(1,1,1, world of player) to stone + set block at location(1,1,1, world("world")) to stone + set block at location({_x}, {_y}, {_z}, {_w}) to stone + + # USING VARIABLES + set {_l1} to location(1,1,1) + set {_l2} to location(10,10,10) + set blocks within {_l1} and {_l2} to stone + if player is within {_l1} and {_l2}: + + # OTHER + kill all entities in radius 50 around location(1,65,1, world "world") + delete all entities in radius 25 around location(50,50,50, world "world_nether") + ignite all entities in radius 25 around location(1,1,1, world of player) + """ + ) + .since("2.2") + .parameter("x", Number.class) + .parameter("y", Number.class) + .parameter("z", Number.class) + .parameter("world", World.class, Modifier.OPTIONAL) + .parameter("yaw", Float.class, Modifier.OPTIONAL) + .parameter("pitch", Float.class, Modifier.OPTIONAL) + .build(args -> { + World world = args.getOrDefault("world", Bukkit.getWorlds().getFirst()); + + return new Location(world, + args.get("x").doubleValue(), args.get("y").doubleValue(), args.get("z").doubleValue(), + args.getOrDefault("yaw", 0f), args.getOrDefault("pitch", 0f)); + })); + + Functions.register(DefaultFunction.builder(skript, "calcExperience", Long.class) + .description("Calculates the total amount of experience needed to achieve given level from scratch in Minecraft.") + .since("2.2-dev32") + .parameter("level", Long.class, Modifier.ranged(0L, Long.MAX_VALUE)) + .build(args -> { + long level = args.get("level"); + long exp; + if (level <= 0) { + exp = 0; + } else if (level <= 15) { + exp = level * level + 6 * level; + } else if (level <= 30) { // Truncating decimal parts probably works + exp = (int) (2.5 * level * level - 40.5 * level + 360); + } else { // Half experience points do not exist, anyway + exp = (int) (4.5 * level * level - 162.5 * level + 2220); + } + + return exp; + })); + + Functions.register(DefaultFunction.builder(skript, "rgb", Color.class) + .description(""" + Returns a RGB color from the given red, green and blue parameters. + Alpha values can be added optionally but these only take affect in certain situations, like text display backgrounds.""") + .examples( + "dye player's leggings rgb(120, 30, 45)", + "set the colour of a text display to rgb(10, 50, 100, 50)" + ) + .since("2.5, 2.10 (alpha)") + .parameter("red", Long.class, Modifier.ranged(0, 255)) + .parameter("green", Long.class, Modifier.ranged(0, 255)) + .parameter("blue", Long.class, Modifier.ranged(0, 255)) + .parameter("alpha", Long.class, Modifier.ranged(0, 255), Modifier.OPTIONAL) + .build(args -> ColorRGB.fromRGBA( + args.get("red").intValue(), + args.get("green").intValue(), + args.get("blue").intValue(), + args.getOrDefault("alpha", 255L).intValue() + ))); + + Functions.register(DefaultFunction.builder(skript, "player", Player.class) + .description( + "Returns an online player from their name or UUID, if player is offline function will return nothing.", + "Setting 'getExactPlayer' parameter to true will return the player whose name is exactly equal to the provided name instead of returning a player that their name starts with the provided name." + ) + .examples( + "set {_p} to player(\"Notch\") # will return an online player whose name is or starts with 'Notch'", + "set {_p} to player(\"Notch\", true) # will return the only online player whose name is 'Notch'", + "set {_p} to player(\"069a79f4-44e9-4726-a5be-fca90e38aaf5\") # if player is offline" + ) + .since("2.8.0") + .parameter("nameOrUUID", String.class) + .parameter("getExactPlayer", Boolean.class, Modifier.OPTIONAL) + .build(args -> { + String name = args.get("nameOrUUID"); + boolean isExact = args.getOrDefault("getExactPlayer", false); + + UUID uuid = null; + if (name.length() > 16 || name.contains("-")) { + if (Utils.isValidUUID(name)) + uuid = UUID.fromString(name); + } + + if (uuid != null) + return Bukkit.getPlayer(uuid); + if (isExact) + return Bukkit.getPlayerExact(name); + return Bukkit.getPlayer(name); + })); + + Functions.register(DefaultFunction.builder(skript, "offlineplayer", OfflinePlayer.class) + .description( + "Returns a offline player from their name or UUID. This function will still return the player if they're online. " + + "If Paper 1.16.5+ is used, the 'allowLookup' parameter can be set to false to prevent this function from doing a " + + "web lookup for players who have not joined before. Lookups can cause lag spikes of up to multiple seconds, so " + + "use offline players with caution." + ) + .examples( + "set {_p} to offlineplayer(\"Notch\")", + "set {_p} to offlineplayer(\"069a79f4-44e9-4726-a5be-fca90e38aaf5\")", + "set {_p} to offlineplayer(\"Notch\", false)" + ) + .since("2.8.0, 2.9.0 (prevent lookups)") + .parameter("nameOrUUID", String.class) + .parameter("allowLookups", Boolean.class, Modifier.OPTIONAL) + .build(args -> { + String name = args.get("nameOrUUID"); + UUID uuid = null; + if (name.length() > 16 || name.contains("-")) { // shortcut + if (Utils.isValidUUID(name)) + uuid = UUID.fromString(name); + } + OfflinePlayer result; + + if (uuid != null) { + result = Bukkit.getOfflinePlayer(uuid); // doesn't do lookups + } else if (!args.getOrDefault("allowLookups", true)) { + result = Bukkit.getOfflinePlayerIfCached(name); + } else { + result = Bukkit.getOfflinePlayer(name); + } + + return result; + })); + } +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/functions/VectorFunctions.java b/src/main/java/org/skriptlang/skript/bukkit/functions/VectorFunctions.java new file mode 100644 index 00000000000..aa2a615cf64 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/functions/VectorFunctions.java @@ -0,0 +1,79 @@ +package org.skriptlang.skript.bukkit.functions; + +import ch.njol.skript.Skript; +import ch.njol.skript.lang.function.Functions; +import org.bukkit.util.Vector; +import org.joml.AxisAngle4f; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.skriptlang.skript.addon.SkriptAddon; +import org.skriptlang.skript.bukkit.BukkitModule; +import org.skriptlang.skript.common.function.DefaultFunction; + +/** + * Contains all functions using Bukkit vectors. + */ +public class VectorFunctions { + + public VectorFunctions(BukkitModule module, SkriptAddon addon) { + SkriptAddon skript = module.origin(addon).addon(); + + Functions.register(DefaultFunction.builder(skript, "vector", Vector.class) + .description("Creates a vector from a single argument. Equivalent to vector(n, n, n).") + .examples("vector(1) # = vector(1, 1, 1)") + .since("2.15") + .parameter("n", Number.class) + .build(args -> { + double value = args.get("n").doubleValue(); + return new Vector(value, value, value); + })); + + Functions.register(DefaultFunction.builder(skript, "vector", Vector.class) + .description("Creates a new vector, which can be used with various expressions, effects and functions.") + .examples("vector(0, 0, 0)") + .since("2.2-dev23") + .parameter("x", Number.class) + .parameter("y", Number.class) + .parameter("z", Number.class) + .build(args -> new Vector( + args.get("x").doubleValue(), + args.get("y").doubleValue(), + args.get("z").doubleValue() + ))); + + if (Skript.classExists("org.joml.Quaternionf")) { + Functions.register(DefaultFunction.builder(skript, "quaternion", Quaternionf.class) + .description("Returns a quaternion from the given W, X, Y and Z parameters. ") + .examples("quaternion(1, 5.6, 45.21, 10)") + .since("2.10") + .parameter("w", Number.class) + .parameter("x", Number.class) + .parameter("y", Number.class) + .parameter("z", Number.class) + .build(args -> { + double w = args.get("w").doubleValue(); + double x = args.get("x").doubleValue(); + double y = args.get("y").doubleValue(); + double z = args.get("z").doubleValue(); + return new Quaternionf(x, y, z, w); + })); + } + + if (Skript.classExists("org.joml.AxisAngle4f")) { + Functions.register(DefaultFunction.builder(skript, "axisAngle", Quaternionf.class) + .description("Returns a quaternion from the given angle (in degrees) and axis (as a vector). This represents a rotation around the given axis by the given angle.") + .examples("axisAngle(90, (vector from player's facing))") + .since("2.10") + .parameter("angle", Number.class) + .parameter("axis", Vector.class) + .build(args -> { + float angle = (float) (args.get("angle").floatValue() / 180 * Math.PI); + Vector v = args.get("axis"); + if (v.isZero() || !Double.isFinite(v.getX()) || !Double.isFinite(v.getY()) || !Double.isFinite(v.getZ())) + return null; + Vector3f axis = v.toVector3f(); + return new Quaternionf(new AxisAngle4f(angle, axis)); + })); + } + } +} diff --git a/src/main/java/org/skriptlang/skript/common/CommonModule.java b/src/main/java/org/skriptlang/skript/common/CommonModule.java index 762ec04c158..f8e65cf6877 100644 --- a/src/main/java/org/skriptlang/skript/common/CommonModule.java +++ b/src/main/java/org/skriptlang/skript/common/CommonModule.java @@ -5,6 +5,9 @@ import org.skriptlang.skript.addon.HierarchicalAddonModule; import org.skriptlang.skript.addon.SkriptAddon; import org.skriptlang.skript.common.elements.expressions.*; +import org.skriptlang.skript.common.elements.functions.MathFunctions; +import org.skriptlang.skript.common.elements.functions.StringFunctions; +import org.skriptlang.skript.common.elements.functions.TimeFunctions; import org.skriptlang.skript.common.properties.PropertiesModule; import org.skriptlang.skript.common.types.*; @@ -35,6 +38,10 @@ protected void loadSelf(SkriptAddon addon) { ExprRecursiveSize::register, ExprReplace::register ); + + new MathFunctions(); + new StringFunctions(); + new TimeFunctions(); } @Override diff --git a/src/main/java/org/skriptlang/skript/common/elements/functions/MathFunctions.java b/src/main/java/org/skriptlang/skript/common/elements/functions/MathFunctions.java new file mode 100644 index 00000000000..0418dfa3bc5 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/common/elements/functions/MathFunctions.java @@ -0,0 +1,568 @@ +package org.skriptlang.skript.common.elements.functions; + +import ch.njol.skript.Skript; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.KeyedValue; +import ch.njol.skript.lang.function.Functions; +import ch.njol.skript.util.Contract; +import ch.njol.util.Math2; +import ch.njol.util.StringUtils; +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; +import java.math.RoundingMode; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Contains all generic math functions. + */ +public class MathFunctions { + + /** + * @param n The number. + * @return a number in a string representation with at most 4 digits after the decimal point. + */ + private static String str(double n) { + return StringUtils.toString(n, 4); + } + + static { + SkriptAddon skript = Skript.instance(); + + Functions.register(DefaultFunction.builder(skript, "isNaN", Boolean.class) + .description("Returns true if the input is NaN (not a number).") + .examples("isNaN(0) # false", "isNaN(0/0) # true", "isNaN(sqrt(-1)) # true") + .since("2.8.0") + .parameter("n", Number.class) + .build(args -> Double.isNaN(args.get("n").doubleValue()))); + + Functions.register(DefaultFunction.builder(skript, "floor", Long.class) + .description("Rounds a number down, i.e. returns the closest integer smaller than or equal to the argument.") + .examples("floor(2.34) = 2", "floor(2) = 2", "floor(2.99) = 2") + .since("2.2") + .parameter("n", Number.class) + .build(args -> { + Number value = args.get("n"); + + if (value instanceof Long l) + return l; + + return Math2.floor(value.doubleValue()); + })); + + Functions.register(DefaultFunction.builder(skript, "round", Number.class) + .description("Rounds a number, i.e. returns the closest integer to the argument. Place a second argument to define the decimal placement.") + .examples("round(2.34) = 2", "round(2) = 2", "round(2.99) = 3", "round(2.5) = 3") + .since("2.2, 2.7 (decimal placement)") + .parameter("n", Number.class) + .parameter("decimals", Number.class, Modifier.OPTIONAL) + .build(args -> { + if (args.get("n") instanceof Long longValue) + return longValue; + double value = args.get("n").doubleValue(); + if (!Double.isFinite(value)) + return value; + + double placementDouble = args.getOrDefault("decimals", 0).doubleValue(); + if (!Double.isFinite(placementDouble) || placementDouble >= Integer.MAX_VALUE || placementDouble <= Integer.MIN_VALUE) + return Double.NaN; + int placement = (int) placementDouble; + if (placement == 0) + return Math2.round(value); + if (placement >= 0) { + BigDecimal decimal = new BigDecimal(Double.toString(value)); + decimal = decimal.setScale(placement, RoundingMode.HALF_UP); + return decimal.doubleValue(); + } + long rounded = Math2.round(value); + return (int) Math2.round(rounded * Math.pow(10.0, placement)) / Math.pow(10.0, placement); + })); + + Functions.register(DefaultFunction.builder(skript, "ceil", Long.class) + .description("Rounds a number up, i.e. returns the closest integer larger than or equal to the argument.") + .examples("ceil(2.34) = 3", "ceil(2) = 2", "ceil(2.99) = 3") + .since("2.2") + .parameter("n", Number.class) + .build(args -> { + Number num = args.get("n"); + if (num instanceof Long lng) + return lng; + return Math2.ceil(num.doubleValue()); + })); + + Functions.register(DefaultFunction.builder(skript, "ceiling", Long.class) + .description("Alias of ceil.") + .examples("ceiling(2.34) = 3", "ceiling(2) = 2", "ceiling(2.99) = 3") + .since("2.2") + .parameter("n", Number.class) + .build(args -> { + Number num = args.get("n"); + if (num instanceof Long lng) + return lng; + return Math2.ceil(num.doubleValue()); + })); + + Functions.register(DefaultFunction.builder(skript, "abs", Number.class) + .description("Returns the absolute value of the argument, i.e. makes the argument positive.") + .examples("abs(3) = 3", "abs(-2) = 2") + .since("2.2") + .parameter("n", Number.class) + .build(args -> { + Number n = args.get("n"); + if (n instanceof Byte || n instanceof Short || n instanceof Integer || n instanceof Long) + return Math.abs(n.longValue()); + return Math.abs(n.doubleValue()); + })); + + Functions.register(DefaultFunction.builder(skript, "mod", Number.class) + .description("Returns the modulo of the given arguments, i.e. the remainder of the division d/m, where d and m are the arguments of this function.", + "The returned value is always positive. Returns NaN (not a number) if the second argument is zero.") + .examples("mod(3, 2) = 1", "mod(256436, 100) = 36", "mod(-1, 10) = 9") + .since("2.2") + .parameter("d", Number.class) + .parameter("m", Number.class) + .build(args -> { + Number d = args.get("d"); + Number m = args.get("m"); + double mm = m.doubleValue(); + if (mm == 0) + return Double.NaN; + return Math2.mod(d.doubleValue(), mm); + })); + + Functions.register(DefaultFunction.builder(skript, "exp", Number.class) + .description("The exponential function.") + .examples("exp(0) = 1", "exp(1) = " + str(Math.exp(1))) + .since("2.2") + .parameter("n", Number.class) + .build(args -> Math.exp(args.get("n").doubleValue()))); + + Functions.register(DefaultFunction.builder(skript, "ln", Number.class) + .description("The natural logarithm.", + "Returns NaN (not a number) if the argument is negative.") + .examples("ln(1) = 0", "ln(exp(5)) = 5", "ln(2) = " + StringUtils.toString(Math.log(2), 4)) + .since("2.2") + .parameter("n", Number.class) + .build(args -> Math.log(args.get("n").doubleValue()))); + + Functions.register(DefaultFunction.builder(skript, "log", Number.class) + .description("A logarithm, with base 10 if none is specified. This is the inverse operation to exponentiation (for positive bases only), i.e. log(base ^ exponent, base) = exponent for any positive number 'base' and any number 'exponent'.", + "Another useful equation is base ^ log(a, base) = a for any numbers 'base' and 'a'.", + "Please note that due to how numbers are represented in computers, these equations do not hold for all numbers, as the computed values may slightly differ from the correct value.", + "Returns NaN (not a number) if any of the arguments are negative.") + .examples("log(100) = 2 # 10^2 = 100", "log(16, 2) = 4 # 2^4 = 16") + .since("2.2") + .parameter("n", Number.class) + .parameter("base", Number.class, Modifier.OPTIONAL) + .build(args -> { + double base = args.getOrDefault("base", 10).doubleValue(); + double n = args.get("n").doubleValue(); + return Math.log10(n) / Math.log10(base); + })); + + Functions.register(DefaultFunction.builder(skript, "sqrt", Number.class) + .description("The square root, which is the inverse operation to squaring a number (for positive numbers only). This is the same as (argument) ^ (1/2) – other roots can be calculated via number ^ (1/root), e.g. set {_l} to {_volume}^(1/3).", + "Returns NaN (not a number) if the argument is negative.") + .examples("sqrt(4) = 2", "sqrt(2) = " + str(Math.sqrt(2)), "sqrt(-1) = " + str(Math.sqrt(-1))) + .since("2.2") + .parameter("n", Number.class) + .build(args -> Math.sqrt(args.get("n").doubleValue()))); + + // trigonometry + + Functions.register(DefaultFunction.builder(skript, "sin", Number.class) + .description("The sine function. It starts at 0° with a value of 0, goes to 1 at 90°, back to 0 at 180°, to -1 at 270° and then repeats every 360°. Uses degrees, not radians.") + .examples("sin(90) = 1", "sin(60) = " + str(Math.sin(Math.toRadians(60)))) + .since("2.2") + .parameter("n", Number.class) + .build(args -> Math.sin(Math.toRadians(args.get("n").doubleValue())))); + + Functions.register(DefaultFunction.builder(skript, "cos", Number.class) + .description("The cosine function. This is basically the sine shifted by 90°, i.e. cos(a) = sin(a + 90°), for any number a. Uses degrees, not radians.") + .examples("cos(0) = 1", "cos(90) = 0") + .since("2.2") + .parameter("n", Number.class) + .build(args -> Math.cos(Math.toRadians(args.get("n").doubleValue())))); + + Functions.register(DefaultFunction.builder(skript, "tan", Number.class) + .description("The tangent function. This is basically sin(arg)/cos(arg). Uses degrees, not radians.") + .examples("tan(0) = 0", "tan(45) = 1", "tan(89.99) = " + str(Math.tan(Math.toRadians(89.99)))) + .since("2.2") + .parameter("n", Number.class) + .build(args -> Math.tan(Math.toRadians(args.get("n").doubleValue())))); + + Functions.register(DefaultFunction.builder(skript, "asin", Number.class) + .description("The inverse of the sine, also called arcsin. Returns result in degrees, not radians. Only returns values from -90 to 90.") + .examples("asin(0) = 0", "asin(1) = 90", "asin(0.5) = " + str(Math.toDegrees(Math.asin(0.5)))) + .since("2.2") + .parameter("n", Number.class) + .build(args -> Math.toDegrees(Math.asin(args.get("n").doubleValue())))); + + Functions.register(DefaultFunction.builder(skript, "acos", Number.class) + .description("The inverse of the cosine, also called arccos. Returns result in degrees, not radians. Only returns values from 0 to 180.") + .examples("acos(0) = 90", "acos(1) = 0", "acos(0.5) = " + str(Math.toDegrees(Math.asin(0.5)))) + .since("2.2") + .parameter("n", Number.class) + .build(args -> Math.toDegrees(Math.acos(args.get("n").doubleValue())))); + + Functions.register(DefaultFunction.builder(skript, "atan", Number.class) + .description("The inverse of the tangent, also called arctan. Returns result in degrees, not radians. Only returns values from -90 to 90.") + .examples("atan(0) = 0", "atan(1) = 45", "atan(10000) = " + str(Math.toDegrees(Math.atan(10000)))) + .since("2.2") + .parameter("n", Number.class) + .build(args -> Math.toDegrees(Math.atan(args.get("n").doubleValue())))); + + Functions.register(DefaultFunction.builder(skript, "atan2", Number.class) + .description("Similar to atan, but requires two coordinates and returns values from -180 to 180.", + "The returned angle is measured counterclockwise in a standard mathematical coordinate system (x to the right, y to the top).") + .examples("atan2(0, 1) = 0", "atan2(10, 0) = 90", "atan2(-10, 5) = " + str(Math.toDegrees(Math.atan2(-10, 5)))) + .since("2.2") + .parameter("x", Number.class) + .parameter("y", Number.class) + .build(args -> Math.toDegrees(Math.atan2(args.get("y").doubleValue(), args.get("x").doubleValue())))); + + // more stuff + + Functions.register(DefaultFunction.builder(skript, "sum", Number.class) + .description("Sums a list of numbers.") + .examples("sum(1) = 1", "sum(2, 3, 4) = 9", "sum({some list variable::*})", "sum(2, {_v::*}, and the player's y-coordinate)") + .since("2.2") + .parameter("ns", Number[].class) + .build(args -> { + Number[] ns = args.get("ns"); + if (ns.length == 0) + return null; + + double sum = ns[0].doubleValue(); + for (int i = 1; i < ns.length; i++) + sum += ns[i].doubleValue(); + return sum; + })); + + Functions.register(DefaultFunction.builder(skript, "product", Number.class) + .description("Calculates the product of a list of numbers.") + .examples("product(1) = 1", "product(2, 3, 4) = 24", "product({some list variable::*})", "product(2, {_v::*}, and the player's y-coordinate)") + .since("2.2") + .parameter("ns", Number[].class) + .build(args -> { + Number[] ns = args.get("ns"); + if (ns.length == 0) + return null; + + double product = ns[0].doubleValue(); + for (int i = 1; i < ns.length; i++) + product *= ns[i].doubleValue(); + return product; + })); + + Functions.register(DefaultFunction.builder(skript, "max", Number.class) + .description("Returns the maximum number from a list of numbers.") + .examples("max(1) = 1", "max(1, 2, 3, 4) = 4", "max({some list variable::*})") + .since("2.2") + .parameter("ns", Number[].class) + .build(args -> { + Number[] ns = args.get("ns"); + if (ns.length == 0) + return null; + + double max = ns[0].doubleValue(); + for (int i = 1; i < ns.length; i++) { + double d = ns[i].doubleValue(); + if (d > max || Double.isNaN(max)) + max = d; + } + return max; + })); + + Functions.register(DefaultFunction.builder(skript, "min", Number.class) + .description("Returns the minimum number from a list of numbers.") + .examples("min(1) = 1", "min(1, 2, 3, 4) = 1", "min({some list variable::*})") + .since("2.2") + .parameter("ns", Number[].class) + .build(args -> { + Number[] ns = args.get("ns"); + if (ns.length == 0) + return null; + + double min = ns[0].doubleValue(); + for (int i = 1; i < ns.length; i++) { + double d = ns[i].doubleValue(); + if (d < min || Double.isNaN(min)) + min = d; + } + return min; + })); + + Functions.register(DefaultFunction.keyedBuilder(skript, "clamp", Number[].class) + .description("Clamps one or more values between two numbers.", "This function retains indices") + .examples( + "clamp(5, 0, 10) = 5", + "clamp(5.5, 0, 5) = 5", + "clamp(0.25, 0, 0.5) = 0.25", + "clamp(5, 7, 10) = 7", + "clamp((5, 0, 10, 9, 13), 7, 10) = (7, 7, 10, 9, 10)", + "set {_clamped::*} to clamp({_values::*}, 0, 10)") + .since("2.8.0") + .parameter("values", Number[].class, Modifier.KEYED) + .parameter("min", Number.class) + .parameter("max", Number.class) + .contract(new Contract() { + @Override + public boolean isSingle(Expression... arguments) { + return arguments[0].isSingle(); + } + + @Override + public Class getReturnType(Expression... arguments) { + return Number.class; + } + }) + .build(args -> { + KeyedValue[] values = args.get("values"); + return KeyedValue.unzip(values).keys(); + }, args -> { + KeyedValue[] values = args.get("values"); + Double[] clampedValues = new Double[values.length]; + double min = args.get("min").doubleValue(); + double max = args.get("max").doubleValue(); + // we'll be nice and swap them if they're in the wrong order + double trueMin = Math.min(min, max); + double trueMax = Math.max(min, max); + for (int i = 0; i < values.length; i++) { + double value = values[i].value().doubleValue(); + if (!Double.isNaN(value) && !Double.isNaN(trueMin) && !Double.isNaN(trueMax)) { + clampedValues[i] = Math.clamp(value, trueMin, trueMax); + } else { + clampedValues[i] = Double.NaN; + } + } + return clampedValues; + })); + + // statistics + + Functions.register(DefaultFunction.builder(skript, "mean", Number.class) + .description( + "Get the mean (average) of a list of numbers.", + "You cannot get the mean of a set of numbers that includes infinity or NaN." + ) + .examples( + "mean(1, 2, 3) = 2", + "mean(0, 5, 10) = 5", + "mean(13, 97, 376, 709) = 298.75" + ) + .since("2.11") + .parameter("numbers", Number[].class) + .build(args -> { + Number[] numbers = args.get("numbers"); + Double total = 0d; + int length = numbers.length; + for (Number number : numbers) { + if (Double.isInfinite(number.doubleValue()) || Double.isNaN(number.doubleValue())) + return null; + if (total.isInfinite() || total.isNaN()) + return null; + total += number.doubleValue() / length; + } + return total; + })); + + Functions.register(DefaultFunction.builder(skript, "median", Number.class) + .description( + "Get the middle value of a sorted list of numbers. " + + "If the list has an even number of values, the median is the average of the two middle numbers.", + "You cannot get the median of a set of numbers that includes NaN." + ) + .examples( + "median(1, 2, 3, 4, 5) = 3", + "median(1, 2, 3, 4, 5, 6) = 3.5", + "median(0, 123, 456, 789) = 289.5" + ) + .since("2.11") + .parameter("numbers", Number[].class) + .build(args -> { + Number[] params = args.get("numbers"); + AtomicBoolean invalid = new AtomicBoolean(false); + // median requires the numbers to be sorted from lowest to biggest + Number[] sorted = Arrays.stream(params) + .filter(object -> { + if (!(object instanceof Number number)) + return false; + if (Double.isNaN(number.doubleValue())) { + invalid.set(true); + return false; + } + return true; + }) + .sorted(((o1, o2) -> { + Double n1 = o1.doubleValue(); + Double n2 = o2.doubleValue(); + return n1.compareTo(n2); + })) + .toArray(Number[]::new); + if (invalid.get()) + return null; + int size = sorted.length; + // If the size of numbers provided is odd, we can just grab the middle number + if (size % 2 == 1) + return sorted[Math2.ceil(size / 2)]; + // If not, we grab the rounded up and rounded down numbers, then get the average of those + int half = size / 2; + double first = (sorted[half - 1]).doubleValue(); + double second = (sorted[half]).doubleValue(); + return (first + second) / 2; + })); + + Functions.register(DefaultFunction.builder(skript, "factorial", Number.class) + .description( + "Get the factorial of a number.", + "Getting the factorial of any number above 21 will return an approximation, not an exact value.", + "Any number after 170 will always return Infinity.", + "Should not be used to calculate permutations or combinations manually." + ) + .examples( + "factorial(0) = 1", + "factorial(3) = 3*2*1 = 6", + "factorial(5) = 5*4*3*2*1 = 120", + "factorial(171) = Infinity" + ) + .since("2.11") + .parameter("number", Number.class, Modifier.ranged(0L, Long.MAX_VALUE)) + .build(args -> { + Double number = args.get("number").doubleValue(); + if (number < 0) { + return null; + } else if (number <= 1) { // 0 and 1 + return 1; + } else if (number > 170) { + return Double.POSITIVE_INFINITY; + } + Double result = 1d; + for (double i = number; i > 1; i--) { + if (result.isInfinite() || result.isNaN()) + break; + result *= i; + } + return result; + })); + + Functions.register(DefaultFunction.builder(skript, "root", Number.class) + .description("Calculates the nth root of a number.") + .examples( + "root(2, 4) = 2 # same as sqrt(4)", + "root(4, 16) = 2", + "root(-4, 16) = 0.5 # same as 16^(-1/4)" + ) + .since("2.11") + .parameter("n", Number.class) + .parameter("number", Number.class, Modifier.ranged(0L, Long.MAX_VALUE)) + .build(args -> { + Double n = args.get("n").doubleValue(); + Double number = args.get("number").doubleValue(); + if (n == 0) { + return null; + } else if (n == 1) { + return number; + } else if (n == 2) { + return Math.sqrt(number); + } + return Math.pow(number, (1 / n)); + })); + + Functions.register(DefaultFunction.builder(skript, "permutations", Number.class) + .description( + "Get the number of possible ordered arrangements from 1 to 'options' with each arrangement having a size equal to 'selected'", + "For example, permutations with 3 options and an arrangement size of 1, returns 3: (1), (2), (3)", + "Permutations with 3 options and an arrangement size of 2 returns 6: (1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)", + "Note that the bigger the 'options' and lower the 'selected' may result in approximations or even infinity values.", + "Permutations differ from combinations in that permutations account for the arrangement of elements within the set, " + + "whereas combinations focus on unique sets and ignore the order of elements.", + "Example: (1, 2) and (2, 1) are two distinct permutations because the positions of '1' and '2' are different, " + + "but they represent a single combination since order doesn't matter in combinations." + ) + .examples( + "permutations(10, 2) = 90", + "permutations(10, 4) = 5040", + "permutations(size of {some list::*}, 2)" + ) + .since("2.11") + .parameter("options", Number.class, Modifier.ranged(0L, Long.MAX_VALUE)) + .parameter("selected", Number.class, Modifier.ranged(0L, Long.MAX_VALUE)) + .build(args -> { + Double options = args.get("options").doubleValue(); + Double selected = args.get("selected").doubleValue(); + if (selected > options || selected < 0) { // Illegal argument + return null; + } else if (selected.equals(0d)) { // Will always be 1 + return 1; + } else if (selected.equals(1d)) { // Will always be the number from 'options' + return options; + } + // We can simplify this as there will always be a factorial that can cancel out + // Example: options = 10, selected = 2; 10!/(10-2)! = 10!/8! + // We can deduce that 10! = (10)(9)(8!) ; allowing the '8!' factorial to cancel out, leaving us with: (10)(9) + // Which allows us to start from 10 and go down to 10-2, but will never reach 8 as 'i' needs to be higher + Double result = 1d; + for (double i = options; i > options - selected; i--) { + if (result.isInfinite() || result.isNaN()) + break; + result *= i; + } + return result; + })); + + Functions.register(DefaultFunction.builder(skript, "combinations", Number.class) + .description( + "Get the number of possible sets from 1 to 'options' with each set having a size equal to 'selected'", + "For example, a combination with 3 options and a set size of 1, returns 3: (1), (2), (3)", + "A combination of 3 options with a set size of 2 returns 3: (1, 2), (1, 3), (2, 3)", + "Note that the bigger the 'options' and lower the 'selected' may result in approximations or even infinity values.", + "Combinations differ from permutations in that combinations focus on unique sets, ignoring the order of elements, " + + "whereas permutations account for the arrangement of elements within the set.", + "Example: (1, 2) and (2, 1) represent a single combination since order doesn't matter in combinations, " + + "but they are two distinct permutations because permutations consider the order." + ) + .examples( + "combinations(10, 8) = 45", + "combinations(5, 3) = 10", + "combinations(size of {some list::*}, 2)" + ) + .since("2.11") + .parameter("options", Number.class, Modifier.ranged(0L, Long.MAX_VALUE)) + .parameter("selected", Number.class, Modifier.ranged(0L, Long.MAX_VALUE)) + .build(args -> { + Double options = args.get("options").doubleValue(); + Double selected = args.get("selected").doubleValue(); + if (selected > options || selected < 0) { // Illegal arguments + return null; + } else if (selected.equals(0d)) { // Will always return 1 + return 1; + } else if (selected.equals(1d)) { // Will always be the number from 'options' + return options; + } + // By the same reasoning from 'permutations' there will always be a factorial that can cancel out + // Example: options = 10, selected = 2 ; 10!/(10-2)!(2!) = 10!/(8!)(2!) + // 10! = (10)(9)(8!) ; the 8! cancel out, leaving us with: (10)(9)/2! + // 'top' will calculate the leftovers in the numerator: (10)(9) + Double top = 1d; + for (double i = options; i > options - selected; i--) { + if (top.isInfinite() || top.isNaN()) + return top; + top *= i; + } + // 'bottom' will calculate the leftovers in the denominator: 2! + Double bottom = selected; + for (double i = selected - 1; i > 1; i--) { + if (bottom.isInfinite() || bottom.isNaN()) + break; + bottom *= i; + } + // Then we divide + return top / bottom; + })); + } + +} diff --git a/src/main/java/org/skriptlang/skript/common/elements/functions/StringFunctions.java b/src/main/java/org/skriptlang/skript/common/elements/functions/StringFunctions.java new file mode 100644 index 00000000000..2b0932a4749 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/common/elements/functions/StringFunctions.java @@ -0,0 +1,169 @@ +package org.skriptlang.skript.common.elements.functions; + +import ch.njol.skript.Skript; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.function.Functions; +import ch.njol.skript.registrations.Classes; +import ch.njol.skript.util.Contract; +import ch.njol.skript.util.Utils; +import org.skriptlang.skript.addon.SkriptAddon; +import org.skriptlang.skript.common.function.DefaultFunction; +import org.skriptlang.skript.common.function.Parameter.Modifier; + +import java.text.DecimalFormat; +import java.util.UUID; + +/** + * Contains all generic string functions. + */ +public class StringFunctions { + + private static final DecimalFormat DEFAULT_INTEGER_FORMAT = new DecimalFormat("###,###"); + private static final DecimalFormat DEFAULT_DECIMAL_FORMAT = new DecimalFormat("###,###.##"); + + static { + SkriptAddon skript = Skript.instance(); + + Functions.register(DefaultFunction.builder(skript, "formatNumber", String.class) + .description( + "Converts numbers to human-readable format. By default, '###,###' (e.g. '123,456,789') " + + "will be used for whole numbers and '###,###.##' (e.g. '123,456,789.00) will be used for decimal numbers. " + + "A hashtag '#' represents a digit, a comma ',' is used to separate numbers, and a period '.' is used for decimals. ", + "Will return none if the format is invalid.", + "For further reference, see this article.") + .examples(""" + command /balance: + aliases: bal + executable by: players + trigger: + set {_money} to formatNumber({money::%sender's uuid%}) + send "Your balance: %{_money}%" to sender + """) + .since("2.10") + .parameter("number", Number.class) + .parameter("format", String.class, Modifier.OPTIONAL) + .build(args -> { + Number number = args.get("number"); + String format = args.getOrDefault("format", ""); + + if (format.isEmpty()) { + if (number instanceof Double || number instanceof Float) { + return DEFAULT_DECIMAL_FORMAT.format(number); + } else { + return DEFAULT_INTEGER_FORMAT.format(number); + } + } + + try { + return new DecimalFormat(format).format(number); + } catch (IllegalArgumentException e) { + return null; // invalid format + } + })); + + Functions.register(DefaultFunction.builder(skript, "uuid", UUID.class) + .description("Returns a UUID from the given string. The string must be in the format of a UUID.") + .examples("uuid(\"069a79f4-44e9-4726-a5be-fca90e38aaf5\")") + .since("2.11") + .parameter("uuid", String.class) + .build(args -> { + String uuid = args.get("uuid"); + if (Utils.isValidUUID(uuid)) + return UUID.fromString(uuid); + return null; + })); + + Functions.register(DefaultFunction.builder(skript, "concat", String.class) + .description("Joins the provided texts (and other things) into a single text.") + .examples("concat(\"hello \", \"there\") # hello there", + "concat(\"foo \", 100, \" bar\") # foo 100 bar") + .since("2.9.0") + .parameter("texts", Object[].class) + .build(args -> { + StringBuilder builder = new StringBuilder(); + Object[] objects = args.get("texts"); + for (Object object : objects) { + builder.append(Classes.toString(object)); + } + return builder.toString(); + })); + + 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("2.14") + .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"); + 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("2.14") + .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"); + 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; + })); + } + +} diff --git a/src/main/java/org/skriptlang/skript/common/elements/functions/TimeFunctions.java b/src/main/java/org/skriptlang/skript/common/elements/functions/TimeFunctions.java new file mode 100644 index 00000000000..31dd843e308 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/common/elements/functions/TimeFunctions.java @@ -0,0 +1,77 @@ +package org.skriptlang.skript.common.elements.functions; + +import ch.njol.skript.Skript; +import ch.njol.skript.lang.function.Functions; +import ch.njol.skript.util.Date; +import ch.njol.util.Math2; +import org.skriptlang.skript.addon.SkriptAddon; +import org.skriptlang.skript.common.function.DefaultFunction; +import org.skriptlang.skript.common.function.Parameter.Modifier; + +import java.util.Calendar; + +/** + * Contains all generic time functions. + */ +public class TimeFunctions { + + static { + SkriptAddon skript = Skript.instance(); + + int[] fields = { + Calendar.YEAR, Calendar.MONTH, Calendar.DAY_OF_MONTH, + Calendar.HOUR_OF_DAY, Calendar.MINUTE, Calendar.SECOND, Calendar.MILLISECOND, + Calendar.ZONE_OFFSET, Calendar.DST_OFFSET + }; + int[] offsets = { + 0, -1, 0, + 0, 0, 0, 0, + 0, 0 + }; + double[] scale = { + 1, 1, 1, + 1, 1, 1, 1, + 1000 * 60, 1000 * 60 + }; + double[] relations = { + 1. / 12, 1. / 30, + 1. / 24, 1. / 60, 1. / 60, 1. / 1000, + 0, 0, + 0 + }; + String[] dateParamNames = {"year", "month", "day", "hour", "minute", "second", "millisecond", "zone_offset", "dst_offset"}; + + Functions.register(DefaultFunction.builder(skript, "date", Date.class) + .description("Creates a date from a year, month, and day, and optionally also from hour, minute, second and millisecond.", + "A time zone and DST offset can be specified as well (in minutes), if they are left out the server's time zone and DST offset are used (the created date will not retain this information).") + .examples("date(2014, 10, 1) # 0:00, 1st October 2014", + "date(1990, 3, 5, 14, 30) # 14:30, 5th May 1990", + "date(1999, 12, 31, 23, 59, 59, 999, -3*60, 0) # almost year 2000 in parts of Brazil (-3 hours offset, no DST)") + .since("2.2") + .parameter("year", Number.class) + .parameter("month", Number.class) + .parameter("day", Number.class) + .parameter("hour", Number.class, Modifier.OPTIONAL) + .parameter("minute", Number.class, Modifier.OPTIONAL) + .parameter("second", Number.class, Modifier.OPTIONAL) + .parameter("millisecond", Number.class, Modifier.OPTIONAL) + .parameter("zone_offset", Number.class, Modifier.OPTIONAL) + .parameter("dst_offset", Number.class, Modifier.OPTIONAL) + .build(args -> { + Calendar calendar = Calendar.getInstance(); + calendar.setLenient(true); + double carry = 0; + for (int i = 0; i < fields.length; i++) { + Number n = args.getOrDefault(dateParamNames[i], i < 3 ? 0 : (i == 8 ? Double.NaN : 0)); + double value = n.doubleValue() * scale[i] + offsets[i] + carry; + int v = (int) Math2.floor(value); + carry = (value - v) * relations[i]; + //noinspection MagicConstant + calendar.set(fields[i], v); + } + + return new Date(calendar.getTimeInMillis(), calendar.getTimeZone()); + })); + } + +} diff --git a/src/main/java/org/skriptlang/skript/common/function/DefaultFunction.java b/src/main/java/org/skriptlang/skript/common/function/DefaultFunction.java index ea806f86a0d..feaa94f6bac 100644 --- a/src/main/java/org/skriptlang/skript/common/function/DefaultFunction.java +++ b/src/main/java/org/skriptlang/skript/common/function/DefaultFunction.java @@ -6,6 +6,8 @@ import org.skriptlang.skript.addon.SkriptAddon; import org.skriptlang.skript.common.function.Parameter.Modifier; +import java.util.SequencedCollection; + /** * A function that has been implemented in Java, instead of in Skript. *

@@ -49,6 +51,20 @@ public sealed interface DefaultFunction return new DefaultFunctionImpl.BuilderImpl<>(source, name, returnType); } + /** + * Creates a new builder for a keyed function, + * or a function which is able to return the keys that should be used. + * + * @param name The name of the function. + * @param returnType The type of the function. + * @param The return type. + * @return The keyed builder for a function. + */ + @Contract("_, _, _ -> new") + static @NotNull KeyedBuilder keyedBuilder(@NotNull SkriptAddon source, @NotNull String name, @NotNull Class returnType) { + return new DefaultFunctionImpl.KeyedBuilderImpl<>(source, name, returnType); + } + /** * @return The addon this function was registered for. */ @@ -136,4 +152,90 @@ interface Builder { } + /** + * Represents a builder for {@link DefaultFunction DefaultFunctions} + * which changes a variable's keys during execution. + * + * @param The return type of the function. + */ + interface KeyedBuilder { + + /** + * Sets this function builder's {@link ch.njol.skript.util.Contract}. + * + * @param contract The contract. + * @return This builder. + */ + @Contract("_ -> this") + KeyedBuilder contract(@NotNull ch.njol.skript.util.Contract contract); + + /** + * Sets this function builder's description. + * + * @param description The description. + * @return This builder. + */ + @Contract("_ -> this") + KeyedBuilder description(@NotNull String @NotNull ... description); + + /** + * Sets this function builder's version history. + * + * @param since The version information. + * @return This builder. + */ + @Contract("_ -> this") + KeyedBuilder since(@NotNull String @NotNull ... since); + + /** + * Sets this function builder's examples. + * + * @param examples The examples. + * @return This builder. + */ + @Contract("_ -> this") + KeyedBuilder examples(@NotNull String @NotNull ... examples); + + /** + * Sets this function builder's keywords. + * + * @param keywords The keywords. + * @return This builder. + */ + @Contract("_ -> this") + KeyedBuilder keywords(@NotNull String @NotNull ... keywords); + + /** + * Sets this function builder's requires. + * + * @param requires The requirements. + * @return This builder. + */ + @Contract("_ -> this") + KeyedBuilder requires(@NotNull String @NotNull ... requires); + + /** + * Adds a parameter to this function builder. + * + * @param name The parameter name. + * @param type The type of the parameter. + * @param modifiers The {@link Modifier}s to apply to this parameter. + * @return This builder. + */ + @Contract("_, _, _ -> this") + KeyedBuilder parameter(@NotNull String name, @NotNull Class type, Modifier @NotNull ... modifiers); + + /** + * Completes this builder with the code to execute on call of this function. + * Adds a function for setting the returned keys by this function. + * + * @param keys The function which sets the return keys. + * @param execute The code to execute and the key consumer. + * @return The final function. + */ + DefaultFunction build(@NotNull java.util.function.Function> keys, + @NotNull java.util.function.Function execute); + + } + } 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 ff315999c30..37ca88545f0 100644 --- a/src/main/java/org/skriptlang/skript/common/function/DefaultFunctionImpl.java +++ b/src/main/java/org/skriptlang/skript/common/function/DefaultFunctionImpl.java @@ -19,6 +19,7 @@ final class DefaultFunctionImpl extends ch.njol.skript.lang.function.Function private final SkriptAddon source; private final SequencedMap> parameters; private final Function execute; + private final Function> keys; private final List description; private final List since; @@ -26,6 +27,8 @@ final class DefaultFunctionImpl extends ch.njol.skript.lang.function.Function private final List keywords; private final List requires; + private SequencedCollection returnedKeys; + DefaultFunctionImpl( SkriptAddon source, String name, @@ -33,6 +36,7 @@ final class DefaultFunctionImpl extends ch.njol.skript.lang.function.Function Class returnType, boolean single, @Nullable ch.njol.skript.util.Contract contract, Function execute, + Function> keys, String[] description, String[] since, String[] examples, String[] keywords, String[] requires ) { @@ -47,6 +51,7 @@ final class DefaultFunctionImpl extends ch.njol.skript.lang.function.Function this.source = source; this.parameters = parameters; this.execute = execute; + this.keys = keys; this.description = description != null ? List.of(description) : Collections.emptyList(); this.since = since != null ? List.of(since) : Collections.emptyList(); this.examples = examples != null ? List.of(examples) : Collections.emptyList(); @@ -100,6 +105,7 @@ final class DefaultFunctionImpl extends ch.njol.skript.lang.function.Function } FunctionArgumentsImpl arguments = new FunctionArgumentsImpl(args); + returnedKeys = keys.apply(arguments); T result = execute.apply(arguments); if (result == null) { @@ -133,6 +139,7 @@ public T execute(@NotNull FunctionEvent event, @NotNull FunctionArguments arg } } + returnedKeys = keys.apply(arguments); return execute.apply(arguments); } @@ -141,6 +148,11 @@ public boolean resetReturnValue() { return true; } + @Override + public @NotNull String @Nullable [] returnedKeys() { + return returnedKeys.toArray(new String[0]); + } + @Override public @NotNull String name() { return getName(); @@ -181,7 +193,7 @@ public boolean resetReturnValue() { return getSignature(); } - static class BuilderImpl implements DefaultFunctionImpl.Builder { + static class BuilderImpl implements Builder { private final SkriptAddon source; private final String name; @@ -273,30 +285,131 @@ public DefaultFunction build(@NotNull Function execute) Preconditions.checkNotNull(execute, "execute cannot be null"); return new DefaultFunctionImpl<>(source, name, parameters, - returnType, !returnType.isArray(), contract, execute, + returnType, !returnType.isArray(), contract, execute, (args) -> Collections.emptyList(), description, since, examples, keywords, requires); } - /** - * Checks whether the elements in a {@link String} array are null. - * - * @param strings The strings. - */ - private static void checkNotNull(@NotNull String[] strings, @NotNull String message) { - for (String string : strings) { - Preconditions.checkNotNull(string, message); - } + } + + static class KeyedBuilderImpl implements KeyedBuilder { + + private final SkriptAddon source; + private final String name; + private final Class returnType; + private final SequencedMap> parameters = new LinkedHashMap<>(); + + private ch.njol.skript.util.Contract contract = null; + + private String[] description; + private String[] since; + private String[] examples; + private String[] keywords; + private String[] requires; + + KeyedBuilderImpl(@NotNull SkriptAddon source, @NotNull String name, @NotNull Class returnType) { + Preconditions.checkNotNull(source, "source cannot be null"); + Preconditions.checkNotNull(name, "name cannot be null"); + Preconditions.checkNotNull(returnType, "return type cannot be null"); + + this.source = source; + this.name = name; + this.returnType = returnType; + } + + @Override + public KeyedBuilder contract(@NotNull ch.njol.skript.util.Contract contract) { + Preconditions.checkNotNull(contract, "contract cannot be null"); + + this.contract = contract; + return this; + } + + @Override + public KeyedBuilder description(@NotNull String @NotNull ... description) { + Preconditions.checkNotNull(description, "description cannot be null"); + checkNotNull(description, "description contents cannot be null"); + + this.description = description; + return this; + } + + @Override + public KeyedBuilder since(@NotNull String @NotNull ... since) { + Preconditions.checkNotNull(since, "since cannot be null"); + checkNotNull(since, "since contents cannot be null"); + + this.since = since; + return this; + } + + @Override + public KeyedBuilder examples(@NotNull String @NotNull ... examples) { + Preconditions.checkNotNull(examples, "examples cannot be null"); + checkNotNull(examples, "examples contents cannot be null"); + + this.examples = examples; + return this; + } + + @Override + public KeyedBuilder keywords(@NotNull String @NotNull ... keywords) { + Preconditions.checkNotNull(keywords, "keywords cannot be null"); + checkNotNull(keywords, "keywords contents cannot be null"); + + this.keywords = keywords; + return this; + } + + @Override + public KeyedBuilder requires(@NotNull String @NotNull ... requires) { + Preconditions.checkNotNull(keywords, "requires cannot be null"); + checkNotNull(keywords, "requires contents cannot be null"); + + this.requires = requires; + return this; + } + + @Override + public KeyedBuilder parameter(@NotNull String name, @NotNull Class type, Modifier @NotNull ... modifiers) { + Preconditions.checkNotNull(name, "name cannot be null"); + Preconditions.checkNotNull(type, "type cannot be null"); + + parameters.put(name, new DefaultParameter<>(name, type, modifiers)); + return this; + } + + @Override + public DefaultFunction build(@NotNull Function> keys, + @NotNull Function execute) { + Preconditions.checkNotNull(execute, "execute cannot be null"); + Preconditions.checkArgument(parameters.values().stream().anyMatch(it -> it.hasModifier(Modifier.KEYED)), + "build cannot be called without any keyed arguments"); + + return new DefaultFunctionImpl<>(source, name, parameters, + returnType, !returnType.isArray(), contract, execute, keys, + description, since, examples, keywords, requires); } } + /** + * Checks whether the elements in a {@link String} array are null. + * + * @param strings The strings. + */ + private static void checkNotNull(@NotNull String[] strings, @NotNull String message) { + for (String string : strings) { + Preconditions.checkNotNull(string, message); + } + } + /** * A parameter for a {@link DefaultFunction}. * - * @param name The name. - * @param type The type's class. + * @param name The name. + * @param type The type's class. * @param modifiers The modifiers. - * @param The type. + * @param The type. */ record DefaultParameter(String name, Class type, Set modifiers) implements Parameter { diff --git a/src/main/java/org/skriptlang/skript/common/function/Function.java b/src/main/java/org/skriptlang/skript/common/function/Function.java index 5266f57d2ad..4948427f244 100644 --- a/src/main/java/org/skriptlang/skript/common/function/Function.java +++ b/src/main/java/org/skriptlang/skript/common/function/Function.java @@ -5,7 +5,8 @@ import org.jetbrains.annotations.ApiStatus.Internal; import org.jetbrains.annotations.ApiStatus.NonExtendable; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; + +import java.util.SequencedCollection; /** * Represents a function implementation. @@ -21,7 +22,7 @@ public interface Function { /** * Executes this function with the given parameters. * - * @param event The event that is associated with this function execution. + * @param event The event that is associated with this function execution. * @param arguments The arguments to execute the function with. * @return The return value. */ @@ -42,6 +43,6 @@ public interface Function { * @return The returned keys. */ @Experimental - @NotNull String @Nullable [] returnedKeys(); + @NotNull SequencedCollection getReturnedKeys(); }