diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d0120dc3e6d..1e4199f2734 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -66,3 +66,4 @@ # Functions /src/main/java/ch/njol/skript/lang/function @Efnilite @skriptlang/core-developers +/src/main/java/org/skriptlang/skript/lang/function @Efnilite @skriptlang/core-developers diff --git a/src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java b/src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java index a28f3c4edf9..9a0207c4966 100644 --- a/src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java +++ b/src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java @@ -1,15 +1,17 @@ package ch.njol.skript.classes.data; import ch.njol.skript.Skript; -import ch.njol.skript.expressions.base.EventValueExpression; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.KeyedValue; -import ch.njol.skript.lang.function.*; +import ch.njol.skript.lang.function.DefaultFunction; +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.Date; 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; @@ -23,6 +25,8 @@ import org.joml.AxisAngle4f; import org.joml.Quaternionf; import org.joml.Vector3f; +import org.skriptlang.skript.addon.SkriptAddon; +import org.skriptlang.skript.lang.function.Parameter.Modifier; import java.math.BigDecimal; import java.math.RoundingMode; @@ -32,6 +36,8 @@ public class DefaultFunctions { + private static final SkriptAddon SKRIPT = Skript.getAddonInstance(); + private static String str(double n) { return StringUtils.toString(n, 4); } @@ -45,16 +51,20 @@ private static String str(double n) { // basic math functions - Functions.registerFunction(new SimpleJavaFunction("floor", 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.floor(((Number) params[0][0]).doubleValue())}; - } - }.description("Rounds a number down, i.e. returns the closest integer smaller than or equal to the argument.") + 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")); + .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()); + }) + .register(); 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 @@ -353,53 +363,54 @@ public World[] executeSimple(Object[][] params) { .examples("set {_nether} to world(\"%{_world}%_nether\")") .since("2.2"); - Functions.registerFunction(new JavaFunction("location", new Parameter[] { - new Parameter<>("x", DefaultClasses.NUMBER, true, null), - new Parameter<>("y", DefaultClasses.NUMBER, true, null), - new Parameter<>("z", DefaultClasses.NUMBER, true, null), - new Parameter<>("world", DefaultClasses.WORLD, true, new EventValueExpression<>(World.class)), - new Parameter<>("yaw", DefaultClasses.NUMBER, true, new SimpleLiteral(0, true)), - new Parameter<>("pitch", DefaultClasses.NUMBER, true, new SimpleLiteral(0, true)) - }, DefaultClasses.LOCATION, true) { - @Override - @Nullable - public Location[] execute(FunctionEvent event, Object[][] params) { - for (int i : new int[] {0, 1, 2, 4, 5}) { - if (params[i] == null || params[i].length == 0 || params[i][0] == null) - return null; - } - - World world = params[3].length == 1 ? (World) params[3][0] : Bukkit.getWorlds().get(0); // fallback to main world of server - - return new Location[] {new Location(world, - ((Number) params[0][0]).doubleValue(), ((Number) params[1][0]).doubleValue(), ((Number) params[2][0]).doubleValue(), - ((Number) params[4][0]).floatValue(), ((Number) params[5][0]).floatValue())}; - } - }.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")); + 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)); + }) + .register(); Functions.registerFunction(new SimpleJavaFunction("date", new Parameter[] { new Parameter<>("year", DefaultClasses.NUMBER, true, null), @@ -465,23 +476,19 @@ public Date[] executeSimple(Object[][] params) { .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.registerFunction(new SimpleJavaFunction("vector", new Parameter[] { - new Parameter<>("x", DefaultClasses.NUMBER, true, null), - new Parameter<>("y", DefaultClasses.NUMBER, true, null), - new Parameter<>("z", DefaultClasses.NUMBER, true, null) - }, DefaultClasses.VECTOR, true) { - @Override - public Vector[] executeSimple(Object[][] params) { - return new Vector[] {new Vector( - ((Number)params[0][0]).doubleValue(), - ((Number)params[1][0]).doubleValue(), - ((Number)params[2][0]).doubleValue() - )}; - } - - }.description("Creates a new vector, which can be used with various expressions, effects and functions.") + 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")); + .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() + )) + .register(); Functions.registerFunction(new SimpleJavaFunction("calcExperience", new Parameter[] { new Parameter<>("level", DefaultClasses.LONG, true, null) @@ -529,27 +536,32 @@ public ColorRGB[] executeSimple(Object[][] params) { ) .since("2.5, 2.10 (alpha)"); - Functions.registerFunction(new SimpleJavaFunction("player", new Parameter[] { - new Parameter<>("nameOrUUID", DefaultClasses.STRING, true, null), - new Parameter<>("getExactPlayer", DefaultClasses.BOOLEAN, true, new SimpleLiteral(false, true)) // getExactPlayer -- grammar ¯\_ (ツ)_/¯ - }, DefaultClasses.PLAYER, true) { - @Override - public Player[] executeSimple(Object[][] params) { - String name = (String) params[0][0]; - boolean isExact = (boolean) params[1][0]; + 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("-")) { // shortcut + if (name.length() > 16 || name.contains("-")) { if (Utils.isValidUUID(name)) uuid = UUID.fromString(name); } - return CollectionUtils.array(uuid != null ? Bukkit.getPlayer(uuid) : (isExact ? Bukkit.getPlayerExact(name) : Bukkit.getPlayer(name))); - } - }).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"); + + return uuid != null ? Bukkit.getPlayer(uuid) : (isExact ? Bukkit.getPlayerExact(name) : Bukkit.getPlayer(name)); + }) + .register(); { // offline player function // TODO - remove this when Spigot support is dropped @@ -608,22 +620,23 @@ public Boolean[] executeSimple(Object[][] params) { .examples("isNaN(0) # false", "isNaN(0/0) # true", "isNaN(sqrt(-1)) # true") .since("2.8.0"); - Functions.registerFunction(new SimpleJavaFunction("concat", new Parameter[] { - new Parameter<>("texts", DefaultClasses.OBJECT, false, null) - }, DefaultClasses.STRING, true) { - @Override - public String[] executeSimple(Object[][] params) { - StringBuilder builder = new StringBuilder(); - for (Object object : params[0]) { - builder.append(Classes.toString(object)); - } - return new String[] {builder.toString()}; - } - }).description("Joins the provided texts (and other things) into a single text.") + 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"); + ) + .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(); + }) + .register(); // joml functions - for display entities { diff --git a/src/main/java/ch/njol/skript/doc/Documentation.java b/src/main/java/ch/njol/skript/doc/Documentation.java index 4032c909bf2..b64c7671d3d 100644 --- a/src/main/java/ch/njol/skript/doc/Documentation.java +++ b/src/main/java/ch/njol/skript/doc/Documentation.java @@ -6,6 +6,7 @@ import ch.njol.skript.lang.ExpressionInfo; import ch.njol.skript.lang.SkriptEventInfo; import ch.njol.skript.lang.SyntaxElementInfo; +import ch.njol.skript.lang.function.DefaultFunction; import ch.njol.skript.lang.function.Functions; import ch.njol.skript.lang.function.JavaFunction; import ch.njol.skript.lang.function.Parameter; @@ -18,12 +19,7 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.jetbrains.annotations.Nullable; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -import java.io.UnsupportedEncodingException; +import java.io.*; import java.util.ArrayList; import java.util.function.Function; import java.util.regex.Matcher; @@ -157,7 +153,7 @@ private static void asSql(final PrintWriter pw) { "examples VARCHAR(2000) NOT NULL," + "since VARCHAR(100) NOT NULL" + ");"); - for (final JavaFunction func : Functions.getJavaFunctions()) { + for (ch.njol.skript.lang.function.Function func : Functions.getDefaultFunctions()) { assert func != null; insertFunction(pw, func); } @@ -381,15 +377,29 @@ private static void insertClass(final PrintWriter pw, final ClassInfo info) { since); } - private static void insertFunction(final PrintWriter pw, final JavaFunction func) { - final StringBuilder params = new StringBuilder(); - for (final Parameter p : func.getParameters()) { + private static void insertFunction(PrintWriter pw, ch.njol.skript.lang.function.Function func) { + String[] typeSince, typeDescription, typeExamples; + if (func instanceof DefaultFunction defaultFunction) { + typeSince = defaultFunction.since(); + typeDescription = defaultFunction.description(); + typeExamples = defaultFunction.examples(); + } else if (func instanceof JavaFunction javaFunction) { + typeSince = javaFunction.getSince() != null ? javaFunction.getSince().split("\n") : null; + typeDescription = javaFunction.getDescription(); + typeExamples = javaFunction.getExamples(); + } else { + assert false; + return; + } + + StringBuilder params = new StringBuilder(); + for (Parameter p : func.getParameters()) { if (params.length() != 0) params.append(", "); params.append(p.toString()); } - final String desc = validateHTML(StringUtils.join(func.getDescription(), "
"), "functions"); - final String since = validateHTML(func.getSince(), "functions"); + String desc = validateHTML(StringUtils.join(typeDescription, "
"), "functions"); + String since = validateHTML(StringUtils.join(typeSince, "\n"), "functions"); if (desc == null || since == null) { Skript.warning("Function " + func.getName() + "'s description or 'since' is invalid"); return; @@ -398,7 +408,7 @@ private static void insertFunction(final PrintWriter pw, final JavaFunction f escapeHTML(func.getName()), escapeHTML(params.toString()), desc, - escapeHTML(StringUtils.join(func.getExamples(), "\n")), + escapeHTML(StringUtils.join(typeExamples, "\n")), since); } diff --git a/src/main/java/ch/njol/skript/doc/HTMLGenerator.java b/src/main/java/ch/njol/skript/doc/HTMLGenerator.java index 163a57b2824..fa21ee43b01 100644 --- a/src/main/java/ch/njol/skript/doc/HTMLGenerator.java +++ b/src/main/java/ch/njol/skript/doc/HTMLGenerator.java @@ -3,20 +3,15 @@ import ch.njol.skript.Skript; import ch.njol.skript.SkriptAPIException; import ch.njol.skript.classes.ClassInfo; -import ch.njol.skript.lang.Condition; -import ch.njol.skript.lang.Effect; -import ch.njol.skript.lang.EffectSection; -import ch.njol.skript.lang.ExpressionInfo; -import ch.njol.skript.lang.Section; -import ch.njol.skript.lang.SkriptEventInfo; -import ch.njol.skript.lang.SyntaxElementInfo; +import ch.njol.skript.lang.*; +import ch.njol.skript.lang.function.DefaultFunction; +import ch.njol.skript.lang.function.Function; import ch.njol.skript.lang.function.Functions; import ch.njol.skript.lang.function.JavaFunction; import ch.njol.skript.registrations.Classes; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.io.Files; - import org.bukkit.event.Cancellable; import org.bukkit.event.Event; import org.bukkit.event.block.BlockCanBuildEvent; @@ -161,12 +156,12 @@ public int compare(@Nullable ClassInfo o1, @Nullable ClassInfo o2) { /** * Sorts functions by their names, alphabetically. */ - private static class FunctionComparator implements Comparator> { + private static class FunctionComparator implements Comparator> { public FunctionComparator() {} @Override - public int compare(@Nullable JavaFunction o1, @Nullable JavaFunction o2) { + public int compare(@Nullable Function o1, @Nullable Function o2) { // Nullness check if (o1 == null || o2 == null) { assert false; @@ -339,9 +334,9 @@ else if (!filesInside.getName().matches("(?i)(.*)\\.(html?|js|css|json)")) { } } if (genType.equals("functions") || isDocsPage) { - List> functions = new ArrayList<>(Functions.getJavaFunctions()); + List> functions = new ArrayList<>(Functions.getDefaultFunctions()); functions.sort(functionComparator); - for (JavaFunction info : functions) { + for (Function info : functions) { assert info != null; generated.append(generateFunction(descTemp, info)); } @@ -813,19 +808,35 @@ private String generateClass(String descTemp, ClassInfo info, @Nullable Strin return desc; } - private String generateFunction(String descTemp, JavaFunction info) { + private String generateFunction(String descTemp, Function info) { String desc = ""; + String[] typeSince, typeDescription, typeExamples, typeKeywords; + if (info instanceof DefaultFunction defaultFunction) { + typeSince = defaultFunction.since(); + typeDescription = defaultFunction.description(); + typeExamples = defaultFunction.examples(); + typeKeywords = defaultFunction.keywords(); + } else if (info instanceof JavaFunction javaFunction) { + typeSince = javaFunction.getSince() != null ? javaFunction.getSince().split("\n") : null; + typeDescription = javaFunction.getDescription(); + typeExamples = javaFunction.getExamples(); + typeKeywords = javaFunction.getKeywords(); + } else { + assert false; + return ""; + } + // Name String docName = getDefaultIfNullOrEmpty(info.getName(), "Unknown Name"); desc = descTemp.replace("${element.name}", docName); // Since - String since = getDefaultIfNullOrEmpty(info.getSince(), "Unknown"); - desc = desc.replace("${element.since}", since); + String[] since = getDefaultIfNullOrEmpty(typeSince, "Unknown"); + desc = desc.replace("${element.since}", Joiner.on("
").join(since)); // Description - String[] description = getDefaultIfNullOrEmpty(info.getDescription(), "Missing description."); + String[] description = getDefaultIfNullOrEmpty(typeDescription, "Missing description."); desc = desc.replace("${element.desc}", Joiner.on("
").join(description)); desc = desc .replace("${element.desc-safe}", Joiner.on("
").join(description) @@ -838,13 +849,13 @@ private String generateFunction(String descTemp, JavaFunction info) { desc = handleIf(desc, "${if by-addon}", false); // Examples - String[] examples = getDefaultIfNullOrEmpty(info.getExamples(), "Missing examples."); + String[] examples = getDefaultIfNullOrEmpty(typeExamples, "Missing examples."); desc = desc.replace("${element.examples}", Joiner.on("\n
").join(Documentation.escapeHTML(examples))); desc = desc .replace("${element.examples-safe}", Joiner.on("
").join(examples) .replace("\\", "\\\\").replace("\"", "\\\"").replace("\t", " ")); - String[] keywords = info.getKeywords(); + String[] keywords = typeKeywords; desc = desc.replace("${element.keywords}", keywords == null ? "" : Joiner.on(", ").join(keywords)); // Documentation ID @@ -864,7 +875,7 @@ private String generateFunction(String descTemp, JavaFunction info) { desc = replaceReturnType(desc, returnType); // New Elements - desc = handleIf(desc, "${if new-element}", NEW_TAG_PATTERN.matcher(since).find()); + desc = handleIf(desc, "${if new-element}", NEW_TAG_PATTERN.matcher(Joiner.on(" ").join(since)).find()); // Type desc = desc.replace("${element.type}", "Function"); diff --git a/src/main/java/ch/njol/skript/doc/JSONGenerator.java b/src/main/java/ch/njol/skript/doc/JSONGenerator.java index d30d0f039ac..d96b7647aa8 100644 --- a/src/main/java/ch/njol/skript/doc/JSONGenerator.java +++ b/src/main/java/ch/njol/skript/doc/JSONGenerator.java @@ -5,12 +5,15 @@ import ch.njol.skript.lang.SkriptEventInfo; import ch.njol.skript.lang.SyntaxElement; import ch.njol.skript.lang.SyntaxElementInfo; +import ch.njol.skript.lang.function.DefaultFunction; +import ch.njol.skript.lang.function.Function; import ch.njol.skript.lang.function.Functions; import ch.njol.skript.lang.function.JavaFunction; import ch.njol.skript.registrations.Classes; import ch.njol.skript.registrations.EventValues; import ch.njol.skript.registrations.EventValues.EventValueInfo; import ch.njol.skript.util.Version; +import ch.njol.util.StringUtils; import com.google.common.collect.Multimap; import com.google.gson.*; import org.bukkit.event.Cancellable; @@ -320,15 +323,22 @@ private static JsonArray generateClassInfoArray(Iterator> classInfo * @param function the JavaFunction to generate the JsonObject of * @return the JsonObject of the JavaFunction */ - private static JsonObject generateFunctionElement(JavaFunction function) { + private static JsonObject generateFunctionElement(Function function) { JsonObject functionJsonObject = new JsonObject(); functionJsonObject.addProperty("id", DocumentationIdProvider.getId(function)); functionJsonObject.addProperty("name", function.getName()); - functionJsonObject.addProperty("since", function.getSince()); - functionJsonObject.add("returnType", getReturnType(function)); - functionJsonObject.add("description", convertToJsonArray(function.getDescription())); - functionJsonObject.add("examples", convertToJsonArray(function.getExamples())); + if (function instanceof DefaultFunction defaultFunction) { + functionJsonObject.addProperty("since", StringUtils.join(defaultFunction.since(), "\n")); + functionJsonObject.add("description", convertToJsonArray(defaultFunction.description())); + functionJsonObject.add("examples", convertToJsonArray(defaultFunction.examples())); + } else if (function instanceof JavaFunction javaFunction) { + functionJsonObject.addProperty("since", javaFunction.getSince()); + functionJsonObject.add("description", convertToJsonArray(javaFunction.getDescription())); + functionJsonObject.add("examples", convertToJsonArray(javaFunction.getExamples())); + } + + functionJsonObject.add("returnType", getReturnType(function)); String functionSignature = function.getSignature().toString(false, false); functionJsonObject.add("patterns", convertToJsonArray(functionSignature)); @@ -341,7 +351,7 @@ private static JsonObject generateFunctionElement(JavaFunction function) { * @param function the JavaFunction to get the return type of * @return the JsonObject representing the return type of the JavaFunction */ - private static JsonObject getReturnType(JavaFunction function) { + private static JsonObject getReturnType(Function function) { JsonObject object = new JsonObject(); ClassInfo returnType = function.getReturnType(); @@ -360,7 +370,7 @@ private static JsonObject getReturnType(JavaFunction function) { * @param functions the functions to generate documentation for * @return a JsonArray containing the documentation JsonObjects for each function */ - private static JsonArray generateFunctionArray(Iterator> functions) { + private static JsonArray generateFunctionArray(Iterator> functions) { JsonArray syntaxArray = new JsonArray(); functions.forEachRemaining(function -> syntaxArray.add(generateFunctionElement(function))); return syntaxArray; @@ -414,7 +424,7 @@ public void generate() { jsonDocs.add("structures", generateStructureElementArray(structuresExcludingEvents.iterator())); jsonDocs.add("sections", generateSyntaxElementArray(Skript.getSections().iterator())); - jsonDocs.add("functions", generateFunctionArray(Functions.getJavaFunctions().iterator())); + jsonDocs.add("functions", generateFunctionArray(Functions.getDefaultFunctions().iterator())); saveDocs(outputDir.toPath().resolve("docs.json"), jsonDocs); } diff --git a/src/main/java/ch/njol/skript/lang/function/DefaultFunction.java b/src/main/java/ch/njol/skript/lang/function/DefaultFunction.java new file mode 100644 index 00000000000..c79cb6a7f1e --- /dev/null +++ b/src/main/java/ch/njol/skript/lang/function/DefaultFunction.java @@ -0,0 +1,346 @@ +package ch.njol.skript.lang.function; + +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.registrations.Classes; +import com.google.common.base.Preconditions; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.addon.SkriptAddon; +import org.skriptlang.skript.lang.function.DefaultParameter; +import org.skriptlang.skript.lang.function.Parameter; +import org.skriptlang.skript.lang.function.Parameter.Modifier; + +import java.lang.reflect.Array; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; + +/** + * A function that has been implemented in Java, instead of in Skript. + *

+ * An example implementation is stated below. + *


+ * DefaultFunction.builder(addon, "floor", Long.class)
+ * 	.description("Rounds a number down.")
+ * 	.examples("floor(2.34) = 2")
+ * 	.since("3.0")
+ * 	.parameter("n", Number.class)
+ * 	.build(args -> {
+ * 		Object value = args.get("n");
+ *
+ * 		if (value instanceof Long l)
+ * 			return l;
+ *
+ * 		return Math2.floor(((Number) value).doubleValue());
+ *    })
+ *    .register();
+ * 
+ *

+ * + * @param The return type. + * @see #builder(SkriptAddon, String, Class) + */ +public final class DefaultFunction extends ch.njol.skript.lang.function.Function { + + /** + * Creates a new builder for a function. + * + * @param name The name of the function. + * @param returnType The type of the function. + * @param The return type. + * @return The builder for a function. + */ + public static Builder builder(@NotNull SkriptAddon source, @NotNull String name, @NotNull Class returnType) { + return new Builder<>(source, name, returnType); + } + + private final Parameter[] parameters; + private final Function execute; + + private final String[] description; + private final String[] since; + private final String[] examples; + private final String[] keywords; + + private DefaultFunction( + String name, Parameter[] parameters, + Class returnType, boolean single, + @Nullable ch.njol.skript.util.Contract contract, Function execute, + String[] description, String[] since, String[] examples, String[] keywords + ) { + super(new Signature<>(null, name, parameters, returnType, single, contract)); + + Preconditions.checkNotNull(name, "name cannot be null"); + Preconditions.checkNotNull(parameters, "parameters cannot be null"); + Preconditions.checkNotNull(returnType, "return type cannot be null"); + Preconditions.checkNotNull(execute, "execute cannot be null"); + + this.parameters = parameters; + this.execute = execute; + this.description = description; + this.since = since; + this.examples = examples; + this.keywords = keywords; + } + + @Override + public T @Nullable [] execute(FunctionEvent event, Object[][] params) { + Map args = new LinkedHashMap<>(); + + int length = Math.min(parameters.length, params.length); + for (int i = 0; i < length; i++) { + Object[] arg = params[i]; + org.skriptlang.skript.lang.function.Parameter parameter = parameters[i]; + + if (arg == null || arg.length == 0) { + if (parameter.modifiers().contains(Modifier.OPTIONAL)) { + continue; + } else { + return null; + } + } + + if (arg.length == 1 || parameter.single()) { + assert parameter.type().isAssignableFrom(arg[0].getClass()) + : "argument type %s does not match parameter type %s".formatted(parameter.type().getSimpleName(), + arg[0].getClass().getSimpleName()); + + args.put(parameter.name(), arg[0]); + } else { + assert parameter.type().isAssignableFrom(arg.getClass()) + : "argument type %s does not match parameter type %s".formatted(parameter.type().getSimpleName(), + arg.getClass().getSimpleName()); + + args.put(parameter.name(), arg); + } + } + + FunctionArguments arguments = new FunctionArguments(args); + T result = execute.apply(arguments); + + if (result == null) { + return null; + } else if (result.getClass().isArray()) { + //noinspection unchecked + return (T[]) result; + } else { + //noinspection unchecked + T[] array = (T[]) Array.newInstance(result.getClass(), 1); + array[0] = result; + return array; + } + } + + @Override + public boolean resetReturnValue() { + return true; + } + + /** + * Returns this function's description. + * + * @return The description. + */ + public @NotNull String @NotNull [] description() { + return description; + } + + /** + * Returns this function's version history. + * + * @return The version history. + */ + public @NotNull String @NotNull [] since() { + return since; + } + + /** + * Returns this function's examples. + * + * @return The examples. + */ + public @NotNull String @NotNull [] examples() { + return examples; + } + + /** + * Returns this function's keywords. + * + * @return The keywords. + */ + public @NotNull String @NotNull [] keywords() { + return keywords; + } + + /** + * Registers this function. + * + * @return This function. + */ + @Contract(" -> this") + public DefaultFunction register() { + Functions.register(this); + + return this; + } + + public static class Builder { + + private final SkriptAddon source; + private final String name; + private final Class returnType; + private final Map> parameters = new LinkedHashMap<>(); + + private ch.njol.skript.util.Contract contract = null; + + private String[] description, since, examples, keywords; + + private Builder(@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; + } + + /** + * Sets this function builder's {@link Contract}. + * + * @param contract The contract. + * @return This builder. + */ + @Contract("_ -> this") + public Builder contract(@NotNull ch.njol.skript.util.Contract contract) { + Preconditions.checkNotNull(contract, "contract cannot be null"); + + this.contract = contract; + return this; + } + + /** + * Sets this function builder's description. + * + * @param description The description. + * @return This builder. + */ + @Contract("_ -> this") + public Builder description(@NotNull String @NotNull ... description) { + Preconditions.checkNotNull(description, "description cannot be null"); + checkNotNull(description, "description contents cannot be null"); + + this.description = description; + return this; + } + + /** + * Sets this function builder's version history. + * + * @param since The version information. + * @return This builder. + */ + @Contract("_ -> this") + public Builder since(@NotNull String @NotNull ... since) { + Preconditions.checkNotNull(since, "since cannot be null"); + checkNotNull(since, "since contents cannot be null"); + + this.since = since; + return this; + } + + /** + * Sets this function builder's examples. + * + * @param examples The examples. + * @return This builder. + */ + @Contract("_ -> this") + public Builder examples(@NotNull String @NotNull ... examples) { + Preconditions.checkNotNull(examples, "examples cannot be null"); + checkNotNull(examples, "examples contents cannot be null"); + + this.examples = examples; + return this; + } + + /** + * Sets this function builder's keywords. + * + * @param keywords The keywords. + * @return This builder. + */ + @Contract("_ -> this") + public Builder keywords(@NotNull String @NotNull ... keywords) { + Preconditions.checkNotNull(keywords, "keywords cannot be null"); + checkNotNull(keywords, "keywords contents cannot be null"); + + this.keywords = keywords; + return this; + } + + /** + * 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); + } + } + + /** + * 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") + public Builder 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; + } + + /** + * Completes this builder with the code to execute on call of this function. + * + * @param execute The code to execute. + * @return The final function. + */ + public DefaultFunction build(@NotNull Function execute) { + Preconditions.checkNotNull(execute, "execute cannot be null"); + + return new DefaultFunction<>(name, parameters.values().toArray(new Parameter[0]), returnType, + !returnType.isArray(), contract, execute, description, since, examples, keywords); + } + + } + + /** + * Returns the {@link ClassInfo} of the non-array type of {@code cls}. + * + * @param cls The class. + * @param The type of class. + * @return The non-array {@link ClassInfo} of {@code cls}. + */ + static ClassInfo getClassInfo(Class cls) { + ClassInfo classInfo; + if (cls.isArray()) { + //noinspection unchecked + classInfo = (ClassInfo) Classes.getExactClassInfo(cls.componentType()); + } else { + classInfo = Classes.getExactClassInfo(cls); + } + if (classInfo == null) { + throw new IllegalArgumentException("No type found for " + cls.getSimpleName()); + } + return classInfo; + } + +} diff --git a/src/main/java/ch/njol/skript/lang/function/DynamicFunctionReference.java b/src/main/java/ch/njol/skript/lang/function/DynamicFunctionReference.java index 00dd6b5bb59..4fc4d553c2f 100644 --- a/src/main/java/ch/njol/skript/lang/function/DynamicFunctionReference.java +++ b/src/main/java/ch/njol/skript/lang/function/DynamicFunctionReference.java @@ -176,7 +176,7 @@ else if (parameters.length < signature.getMinParameters()) for (int i = 0; i < parameters.length; i++) { Parameter parameter = signature.parameters[varArgs ? 0 : i]; //noinspection unchecked - Expression expression = parameters[i].getConvertedExpression(parameter.type.getC()); + Expression expression = parameters[i].getConvertedExpression(parameter.type()); if (expression == null) { return null; } else if (parameter.single && !expression.isSingle()) { 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 37301f2b03f..43cedf37be9 100644 --- a/src/main/java/ch/njol/skript/lang/function/Function.java +++ b/src/main/java/ch/njol/skript/lang/function/Function.java @@ -8,6 +8,8 @@ import org.bukkit.Bukkit; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.addon.SkriptAddon; +import org.skriptlang.skript.lang.function.Parameter.Modifier; import java.util.Arrays; @@ -89,11 +91,11 @@ public boolean isSingle() { // Execute parameters or default value expressions for (int i = 0; i < parameters.length; i++) { Parameter parameter = parameters[i]; - Object[] parameterValue = parameter.keyed ? convertToKeyed(parameterValues[i]) : parameterValues[i]; - if (parameterValue == null) { // Go for default value + Object[] parameterValue = parameter.modifiers().contains(Modifier.KEYED) ? convertToKeyed(parameterValues[i]) : parameterValues[i]; + if (!(this instanceof DefaultFunction) && parameterValue == null) { // Go for default value assert parameter.def != null; // Should've been parse error Object[] defaultValue = parameter.def.getArray(event); - if (parameter.keyed && KeyProviderExpression.areKeysRecommended(parameter.def)) { + if (parameter.modifiers().contains(Modifier.KEYED) && KeyProviderExpression.areKeysRecommended(parameter.def)) { String[] keys = ((KeyProviderExpression) parameter.def).getArrayKeys(event); parameterValue = KeyedValue.zip(defaultValue, keys); } else { @@ -107,7 +109,7 @@ public boolean isSingle() { * really have a concept of nulls, it was changed. The config * option may be removed in future. */ - if (!executeWithNulls && parameterValue.length == 0) + if (!(this instanceof DefaultFunction) && !executeWithNulls && parameterValue.length == 0) return null; parameterValues[i] = parameterValue; } diff --git a/src/main/java/ch/njol/skript/lang/function/FunctionArguments.java b/src/main/java/ch/njol/skript/lang/function/FunctionArguments.java new file mode 100644 index 00000000000..48afb05356b --- /dev/null +++ b/src/main/java/ch/njol/skript/lang/function/FunctionArguments.java @@ -0,0 +1,113 @@ +package ch.njol.skript.lang.function; + +import com.google.common.base.Preconditions; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.Collections; +import java.util.Map; +import java.util.function.Supplier; + +/** + * A class containing all arguments in a function call. + */ +public final class FunctionArguments { + + private final @Unmodifiable @NotNull Map arguments; + + public FunctionArguments(@NotNull Map arguments) { + Preconditions.checkNotNull(arguments, "arguments cannot be null"); + + this.arguments = Collections.unmodifiableMap(arguments); + } + + /** + * Gets a specific argument by name. + *

+ * This method automatically conforms to your expected type, + * to avoid having to cast from Object. Use this method as follows. + *


+	 * Number value = args.get("n");
+	 * Boolean value = args.get("b");
+	 * Number[] value = args.get("ns");
+	 * args.get("b"); // inline
+	 * 
+ *

+ * + * @param name The name of the parameter. + * @param The type to return. + * @return The value present, or null if no value is present. + */ + public T get(@NotNull String name) { + Preconditions.checkNotNull(name, "name cannot be null"); + + //noinspection unchecked + return (T) arguments.get(name); + } + + /** + * Gets a specific argument by name, or a default value if no value is found. + *

+ * This method automatically conforms to your expected type, + * to avoid having to cast from Object. Use this method as follows. + *


+	 * Number value = args.getOrDefault("n", 3.0);
+	 * boolean value = args.getOrDefault("b", false);
+	 * args.getOrDefault("b", () -> false); // inline
+	 * 
+ *

+ * + * @param name The name of the parameter. + * @param defaultValue The default value. + * @param The type to return. + * @return The value present, or the default value if no value is present. + */ + public T getOrDefault(@NotNull String name, T defaultValue) { + Preconditions.checkNotNull(name, "name cannot be null"); + + //noinspection unchecked + return (T) arguments.getOrDefault(name, defaultValue); + } + + /** + * Gets a specific argument by name, or calculates the default value if no value is found. + *

+ * This method automatically conforms to your expected type, + * to avoid having to cast from Object. Use this method as follows. + *


+	 * Number value = args.getOrDefault("n", () -> 3.0);
+	 * boolean value = args.getOrDefault("b", () -> false);
+	 * args.getOrDefault("b", () -> false); // inline
+	 * 
+ *

+ * + * @param name The name of the parameter. + * @param defaultValue A supplier that calculates the default value if no existing value is found. + * @param The type to return. + * @return The value present, or the calculated default value if no value is present. + */ + public T getOrDefault(@NotNull String name, Supplier defaultValue) { + Preconditions.checkNotNull(name, "name cannot be null"); + + Object existing = arguments.get(name); + if (existing == null) { + return defaultValue.get(); + } else { + //noinspection unchecked + return (T) existing; + } + } + + /** + * Returns whether this method call contained the following argument. + * + * @param name The argument. + * @return True if the argument is present. + */ + public boolean has(@NotNull String name) { + Preconditions.checkNotNull(name, "name cannot be null"); + + return arguments.containsKey(name); + } + +} diff --git a/src/main/java/ch/njol/skript/lang/function/FunctionReference.java b/src/main/java/ch/njol/skript/lang/function/FunctionReference.java index 7cc7fdfc745..3fc4a368692 100644 --- a/src/main/java/ch/njol/skript/lang/function/FunctionReference.java +++ b/src/main/java/ch/njol/skript/lang/function/FunctionReference.java @@ -19,6 +19,7 @@ import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.lang.converter.Converters; +import org.skriptlang.skript.lang.function.Parameter.Modifier; import org.skriptlang.skript.util.Executable; import java.util.*; @@ -234,13 +235,15 @@ public boolean validateFunction(boolean first) { RetainingLogHandler log = SkriptLogger.startRetainingLog(); try { //noinspection unchecked - Expression e = parameters[i].getConvertedExpression(p.type.getC()); + Expression e = parameters[i].getConvertedExpression(p.type()); if (e == null) { if (first) { if (LiteralUtils.hasUnparsedLiteral(parameters[i])) { Skript.error("Can't understand this expression: " + parameters[i].toString()); } else { - Skript.error("The " + StringUtils.fancyOrderNumber(i + 1) + " argument given to the function '" + stringified + "' is not of the required type " + p.type + "." + String type = Classes.toString(DefaultFunction.getClassInfo(p.type())); + + Skript.error("The " + StringUtils.fancyOrderNumber(i + 1) + " argument given to the function '" + stringified + "' is not of the required type " + type + "." + " Check the correct order of the arguments and put lists into parentheses if appropriate (e.g. 'give(player, (iron ore and gold ore))')." + " Please note that storing the value in a variable and then using that variable as parameter may suppress this error, but it still won't work."); } @@ -292,7 +295,7 @@ private void parseParameters() { } /** - * Attempts to get this function's signature. + * Attempts to geoopst this function's signature. */ private Signature getRegisteredSignature() { parseParameters(); @@ -368,10 +371,10 @@ public boolean resetReturnValue() { // Prepare parameter values for calling Object[][] params = new Object[singleListParam ? 1 : parameters.length][]; if (singleListParam && parameters.length > 1) { // All parameters to one list - params[0] = evaluateSingleListParameter(parameters, event, function.getParameter(0).keyed); + params[0] = evaluateSingleListParameter(parameters, event, function.getParameter(0).modifiers().contains(Modifier.KEYED)); } else { // Use parameters in normal way for (int i = 0; i < parameters.length; i++) - params[i] = evaluateParameter(parameters[i], event, function.getParameter(i).keyed); + params[i] = evaluateParameter(parameters[i], event, function.getParameter(i).modifiers().contains(Modifier.KEYED)); } // Execute the function diff --git a/src/main/java/ch/njol/skript/lang/function/FunctionRegistry.java b/src/main/java/ch/njol/skript/lang/function/FunctionRegistry.java index 0274257cea5..1ab10a1df35 100644 --- a/src/main/java/ch/njol/skript/lang/function/FunctionRegistry.java +++ b/src/main/java/ch/njol/skript/lang/function/FunctionRegistry.java @@ -631,11 +631,11 @@ static FunctionIdentifier of(@NotNull Signature signature) { int optionalArgs = 0; for (int i = 0; i < signatureParams.length; i++) { Parameter param = signatureParams[i]; - if (param.def != null) { + if (param.isOptional()) { optionalArgs++; } - Class type = param.getType().getC(); + Class type = param.type(); if (param.isSingleValue()) { parameters[i] = type; } else { diff --git a/src/main/java/ch/njol/skript/lang/function/Functions.java b/src/main/java/ch/njol/skript/lang/function/Functions.java index 909ae8127da..68300c85a07 100644 --- a/src/main/java/ch/njol/skript/lang/function/Functions.java +++ b/src/main/java/ch/njol/skript/lang/function/Functions.java @@ -53,11 +53,33 @@ private Functions() {} static boolean callFunctionEvents = false; + /** - * Registers a function written in Java. + * Registers a {@link DefaultFunction}. * - * @return The passed function + * @param function The function to register. + * @return The registered function. + */ + static DefaultFunction register(DefaultFunction function) { + Skript.checkAcceptRegistrations(); + + String name = function.getName(); + if (!name.matches(functionNamePattern)) + throw new SkriptAPIException("Invalid function name '%s'".formatted(name)); + + javaNamespace.addSignature(function.getSignature()); + javaNamespace.addFunction(function); + globalFunctions.put(function.getName(), javaNamespace); + + FunctionRegistry.getRegistry().register(null, function); + + return function; + } + + /** + * @deprecated Use {@link DefaultFunction#register()} or {@link #register(DefaultFunction)} instead. */ + @Deprecated(forRemoval = true, since = "INSERT VERSION") public static JavaFunction registerFunction(JavaFunction function) { Skript.checkAcceptRegistrations(); String name = function.getName(); @@ -161,11 +183,11 @@ public static JavaFunction registerFunction(JavaFunction function) { Parameter[] parameters = signature.parameters; if (parameters.length == 1 && !parameters[0].isSingleValue()) { - existing = FunctionRegistry.getRegistry().getSignature(signature.script, signature.getName(), parameters[0].type.getC().arrayType()); + existing = FunctionRegistry.getRegistry().getSignature(signature.script, signature.getName(), parameters[0].type().arrayType()); } else { Class[] types = new Class[parameters.length]; for (int i = 0; i < parameters.length; i++) { - types[i] = parameters[i].type.getC(); + types[i] = parameters[i].type(); } existing = FunctionRegistry.getRegistry().getSignature(signature.script, signature.getName(), types); @@ -408,12 +430,25 @@ public static void clearFunctions() { toValidate.clear(); } + /** + * @deprecated Use {@link #getDefaultFunctions()} instead. + */ @SuppressWarnings({"unchecked"}) + @Deprecated(forRemoval = true, since = "INSERT VERSION") public static Collection> getJavaFunctions() { // We know there are only Java functions in that namespace return (Collection>) (Object) javaNamespace.getFunctions(); } + /** + * Returns all functions registered using Java. + * + * @return All {@link JavaFunction} or {@link DefaultFunction} functions. + */ + public static Collection> getDefaultFunctions() { + return javaNamespace.getFunctions(); + } + /** * Normally, function calls do not cause actual Bukkit events to be * called. If an addon requires such functionality, it should call this diff --git a/src/main/java/ch/njol/skript/lang/function/JavaFunction.java b/src/main/java/ch/njol/skript/lang/function/JavaFunction.java index c7414f19b25..ef4b39e1a3c 100644 --- a/src/main/java/ch/njol/skript/lang/function/JavaFunction.java +++ b/src/main/java/ch/njol/skript/lang/function/JavaFunction.java @@ -6,6 +6,10 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +/** + * @deprecated Use {@link DefaultFunction} instead. + */ +@Deprecated(since = "INSERT VERSION", forRemoval = true) public abstract class JavaFunction extends Function { private @NotNull String @Nullable [] returnedKeys; diff --git a/src/main/java/ch/njol/skript/lang/function/Parameter.java b/src/main/java/ch/njol/skript/lang/function/Parameter.java index 58471540de2..2dfe50eaad7 100644 --- a/src/main/java/ch/njol/skript/lang/function/Parameter.java +++ b/src/main/java/ch/njol/skript/lang/function/Parameter.java @@ -14,15 +14,15 @@ import ch.njol.skript.util.Utils; import ch.njol.util.NonNullPair; import ch.njol.util.StringUtils; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; -public final class Parameter { +public final class Parameter implements org.skriptlang.skript.lang.function.Parameter { public final static Pattern PARAM_PATTERN = Pattern.compile("^\\s*([^:(){}\",]+?)\\s*:\\s*([a-zA-Z ]+?)\\s*(?:\\s*=\\s*(.+))?\\s*$"); @@ -32,12 +32,12 @@ public final class Parameter { * If {@link SkriptConfig#caseInsensitiveVariables} is {@code true}, * then the valid variable names may not necessarily match this string in casing. */ - final String name; + private final String name; /** * Type of the parameter. */ - final ClassInfo type; + private final ClassInfo type; /** * Expression that will provide default value of this parameter @@ -50,30 +50,88 @@ public final class Parameter { */ final boolean single; + private final Set modifiers; + /** - * Whether this parameter takes in key-value pairs. - *
- * If this is true, a {@link ch.njol.skript.lang.KeyedValue} array containing key-value pairs will be passed to - * {@link Function#execute(FunctionEvent, Object[][])} rather than a value-only object array. + * @deprecated Use {@link org.skriptlang.skript.lang.function.Parameter} + * or {@link ch.njol.skript.lang.function.DefaultFunction.Builder#parameter(String, Class, Modifier...)} + * instead. */ - final boolean keyed; - + @Deprecated(since = "INSERT VERSION", forRemoval = true) public Parameter(String name, ClassInfo type, boolean single, @Nullable Expression def) { this(name, type, single, def, false); } + /** + * @deprecated Use {@link org.skriptlang.skript.lang.function.Parameter} + * or {@link ch.njol.skript.lang.function.DefaultFunction.Builder#parameter(String, Class, Modifier...)} + * instead. + */ + @Deprecated(since = "INSERT VERSION", forRemoval = true) public Parameter(String name, ClassInfo type, boolean single, @Nullable Expression def, boolean keyed) { this.name = name; this.type = type; this.def = def; this.single = single; - this.keyed = keyed; + this.modifiers = new HashSet<>(); + + if (def != null) { + modifiers.add(Modifier.OPTIONAL); + } + if (keyed) { + modifiers.add(Modifier.KEYED); + } + } + + /** + * @deprecated Use {@link org.skriptlang.skript.lang.function.Parameter} + * or {@link ch.njol.skript.lang.function.DefaultFunction.Builder#parameter(String, Class, Modifier...)} + * instead. + */ + @Deprecated(since = "INSERT VERSION", forRemoval = true) + public Parameter(String name, ClassInfo type, boolean single, @Nullable Expression def, boolean keyed, boolean optional) { + this.name = name; + this.type = type; + this.def = def; + this.single = single; + this.modifiers = new HashSet<>(); + + if (optional) { + modifiers.add(Modifier.OPTIONAL); + } + if (keyed) { + modifiers.add(Modifier.KEYED); + } + } + + /** + * Constructs a new parameter for script functions. + * + * @param name The name. + * @param type The type of the parameter. + * @param single Whether the parameter is single. + * @param def The default value. + */ + Parameter(String name, ClassInfo type, boolean single, @Nullable Expression def, Modifier... modifiers) { + this.name = name; + this.type = type; + this.def = def; + this.single = single; + this.modifiers = Set.of(modifiers); } /** - * Get the Type of this parameter. - * @return Type of the parameter + * Returns whether this parameter is optional or not. + * @return Whether this parameter is optional or not. */ + public boolean isOptional() { + return modifiers.contains(Modifier.OPTIONAL); + } + + /** + * @deprecated Use {@link #type()} instead. + */ + @Deprecated(forRemoval = true, since = "INSERT VERSION") public ClassInfo getType() { return type; } @@ -101,7 +159,16 @@ public ClassInfo getType() { log.stop(); } } - return new Parameter<>(name, type, single, d, !single); + + Set modifiers = new HashSet<>(); + if (d != null) { + modifiers.add(Modifier.OPTIONAL); + } + if (!single) { + modifiers.add(Modifier.KEYED); + } + + return new Parameter<>(name, type, single, d, modifiers.toArray(new Modifier[0])); } /** @@ -167,10 +234,9 @@ public ClassInfo getType() { } /** - * Get the name of this parameter. - *

Will be used as name for the local variable that contains value of it inside function.

- * @return Name of this parameter + * @deprecated Use {@link #name()} instead. */ + @Deprecated(forRemoval = true, since = "INSERT VERSION") public String getName() { return name; } @@ -191,6 +257,19 @@ public boolean isSingleValue() { return single; } + @Override + public boolean equals(Object o) { + if (!(o instanceof Parameter parameter)) { + return false; + } + + return modifiers.equals(parameter.modifiers) + && single == parameter.single + && name.equals(parameter.name) + && type.equals(parameter.type) + && Objects.equals(def, parameter.def); + } + @Override public String toString() { return toString(Skript.debug()); @@ -200,4 +279,19 @@ public String toString(boolean debug) { return name + ": " + Utils.toEnglishPlural(type.getCodeName(), !single) + (def != null ? " = " + def.toString(null, debug) : ""); } + @Override + public @NotNull String name() { + return name; + } + + @Override + public @NotNull Class type() { + return type.getC(); + } + + @Override + public @Unmodifiable @NotNull Set modifiers() { + return Collections.unmodifiableSet(modifiers); + } + } 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 ec493bdbd6c..0ac8328eda4 100644 --- a/src/main/java/ch/njol/skript/lang/function/ScriptFunction.java +++ b/src/main/java/ch/njol/skript/lang/function/ScriptFunction.java @@ -37,11 +37,11 @@ public ScriptFunction(Signature sign, SectionNode node) { try { hintManager.enterScope(false); for (Parameter parameter : sign.getParameters()) { - String hintName = parameter.getName(); + String hintName = parameter.name(); if (!parameter.isSingleValue()) { hintName += Variable.SEPARATOR + "*"; } - hintManager.set(hintName, parameter.getType().getC()); + hintManager.set(hintName, parameter.type()); } trigger = loadReturnableTrigger(node, "function " + sign.getName(), new SimpleEvent()); } finally { @@ -60,11 +60,11 @@ public ScriptFunction(Signature sign, SectionNode node) { Parameter parameter = parameters[i]; Object[] val = params[i]; if (parameter.single && val.length > 0) { - Variables.setVariable(parameter.name, val[0], event, true); + Variables.setVariable(parameter.name(), val[0], event, true); } else { for (Object value : val) { KeyedValue keyedValue = (KeyedValue) value; - Variables.setVariable(parameter.name + "::" + keyedValue.key(), keyedValue.value(), event, true); + Variables.setVariable(parameter.name() + "::" + keyedValue.key(), keyedValue.value(), event, true); } } } diff --git a/src/main/java/ch/njol/skript/lang/function/Signature.java b/src/main/java/ch/njol/skript/lang/function/Signature.java index cdad3b18cee..202d42a9455 100644 --- a/src/main/java/ch/njol/skript/lang/function/Signature.java +++ b/src/main/java/ch/njol/skript/lang/function/Signature.java @@ -5,6 +5,7 @@ import ch.njol.skript.util.Utils; import ch.njol.skript.util.Contract; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.lang.function.Parameter.Modifier; import java.util.Collection; import java.util.Collections; @@ -63,7 +64,7 @@ public class Signature { */ final @Nullable Contract contract; - public Signature(String script, + public Signature(@Nullable String script, String name, Parameter[] parameters, boolean local, @Nullable ClassInfo returnType, @@ -82,6 +83,45 @@ public Signature(String script, calls = Collections.newSetFromMap(new WeakHashMap<>()); } + /** + * Creates a new signature. + * + * @param script The script of this signature. + * @param name The name of the function. + * @param parameters The parameters. + * @param returnType The return type class. + * @param contract A {@link Contract} that may belong to this signature. + */ + public Signature(@Nullable String script, + String name, + org.skriptlang.skript.lang.function.Parameter[] parameters, + @Nullable Class returnType, + boolean single, + @Nullable Contract contract) { + this.parameters = new Parameter[parameters.length]; + for (int i = 0; i < parameters.length; i++) { + org.skriptlang.skript.lang.function.Parameter parameter = parameters[i]; + this.parameters[i] = new Parameter<>(parameter.name(), + DefaultFunction.getClassInfo(parameter.type()), parameter.single(), + null, + parameter.modifiers().toArray(new Modifier[0])); + } + + this.script = script; + this.name = name; + this.local = script != null; + if (returnType != null) { + this.returnType = DefaultFunction.getClassInfo(returnType); + } else { + this.returnType = null; + } + this.single = single; + this.contract = contract; + this.originClassPath = ""; + + calls = Collections.newSetFromMap(new WeakHashMap<>()); + } + public Signature(String script, String name, Parameter[] parameters, boolean local, @@ -94,7 +134,7 @@ public Signature(String script, public Signature(String script, String name, Parameter[] parameters, boolean local, @Nullable ClassInfo returnType, boolean single) { this(script, name, parameters, local, returnType, single, null); } - + public String getName() { return name; } @@ -120,6 +160,10 @@ public boolean isSingle() { return single; } + /** + * @deprecated Unused and unsafe. + */ + @Deprecated(forRemoval = true, since = "INSERT VERSION") public String getOriginClassPath() { return originClassPath; } @@ -145,7 +189,7 @@ public int getMaxParameters() { */ public int getMinParameters() { for (int i = parameters.length - 1; i >= 0; i--) { - if (parameters[i].def == null) + if (!parameters[i].isOptional()) return i + 1; } return 0; // No-args function diff --git a/src/main/java/ch/njol/skript/lang/function/SimpleJavaFunction.java b/src/main/java/ch/njol/skript/lang/function/SimpleJavaFunction.java index b83c911945b..b0fc05e5526 100644 --- a/src/main/java/ch/njol/skript/lang/function/SimpleJavaFunction.java +++ b/src/main/java/ch/njol/skript/lang/function/SimpleJavaFunction.java @@ -7,10 +7,9 @@ import org.jetbrains.annotations.Nullable; /** - * A {@link JavaFunction} which doesn't make use of - * the {@link FunctionEvent} instance and that cannot - * accept empty / {@code null} parameters. + * @deprecated Use {@link DefaultFunction} instead. */ +@Deprecated(since = "INSERT VERSION", forRemoval = true) public abstract class SimpleJavaFunction extends JavaFunction { public SimpleJavaFunction(Signature sign) { diff --git a/src/main/java/org/skriptlang/skript/lang/function/DefaultParameter.java b/src/main/java/org/skriptlang/skript/lang/function/DefaultParameter.java new file mode 100644 index 00000000000..1f535db03e3 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/lang/function/DefaultParameter.java @@ -0,0 +1,20 @@ +package org.skriptlang.skript.lang.function; + +import java.util.Set; + +/** + * A parameter for a {@link ch.njol.skript.lang.function.DefaultFunction}. + * + * @param name The name. + * @param type The type's class. + * @param modifiers The modifiers. + * @param The type. + */ +public record DefaultParameter(String name, Class type, Set modifiers) + implements Parameter { + + public DefaultParameter(String name, Class type, Modifier... modifiers) { + this(name, type, Set.of(modifiers)); + } + +} diff --git a/src/main/java/org/skriptlang/skript/lang/function/Parameter.java b/src/main/java/org/skriptlang/skript/lang/function/Parameter.java new file mode 100644 index 00000000000..f8a1916b384 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/lang/function/Parameter.java @@ -0,0 +1,64 @@ +package org.skriptlang.skript.lang.function; + +import ch.njol.skript.lang.function.DefaultFunction.Builder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.Set; + +/** + * Represents a function parameter. + * + * @param The type of the function parameter. + */ +public interface Parameter { + + /** + * @return The name of this parameter. + */ + @NotNull String name(); + + /** + * @return The type of this parameter. + */ + @NotNull Class type(); + + /** + * @return All modifiers belonging to this parameter. + */ + @Unmodifiable + @NotNull Set modifiers(); + + /** + * @return Whether this parameter is for single values. + */ + default boolean single() { + return !type().isArray(); + } + + /** + * Represents a modifier that can be applied to a parameter + * when constructing one using {@link Builder#parameter(String, Class, Modifier[])}}. + */ + interface Modifier { + + /** + * @return A new Modifier instance to be used as a custom flag. + */ + static Modifier of() { + return new Modifier() { }; + } + + /** + * The modifier for parameters that are optional. + */ + Modifier OPTIONAL = of(); + + /** + * The modifier for parameters that support optional keyed expressions. + */ + Modifier KEYED = of(); + + } + +} diff --git a/src/main/java/org/skriptlang/skript/lang/function/ScriptParameter.java b/src/main/java/org/skriptlang/skript/lang/function/ScriptParameter.java new file mode 100644 index 00000000000..12c677e0299 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/lang/function/ScriptParameter.java @@ -0,0 +1,28 @@ +package org.skriptlang.skript.lang.function; + +import ch.njol.skript.lang.Expression; + +import java.util.Set; + +/** + * A parameter for a {@link ch.njol.skript.lang.function.DefaultFunction}. + * + * @param name The name. + * @param type The type's class. + * @param modifiers The modifiers. + * @param defaultValue The default value, or null if there is no default value. + * @param The type. + */ +public record ScriptParameter(String name, Class type, Set modifiers, Expression defaultValue) + implements Parameter { + + public ScriptParameter(String name, Class type, Modifier... modifiers) { + this(name, type, Set.of(modifiers), null); + } + + public ScriptParameter(String name, Class type, Expression defaultValue, Modifier... modifiers) { + this(name, type, Set.of(modifiers), defaultValue); + } + +} + diff --git a/src/test/java/ch/njol/skript/lang/function/DefaultFunctionTest.java b/src/test/java/ch/njol/skript/lang/function/DefaultFunctionTest.java new file mode 100644 index 00000000000..3d169d61178 --- /dev/null +++ b/src/test/java/ch/njol/skript/lang/function/DefaultFunctionTest.java @@ -0,0 +1,83 @@ +package ch.njol.skript.lang.function; + +import ch.njol.skript.Skript; +import ch.njol.util.StringUtils; +import org.junit.Test; +import org.skriptlang.skript.addon.SkriptAddon; +import org.skriptlang.skript.lang.function.Parameter.Modifier; + +import static org.junit.Assert.*; + +public class DefaultFunctionTest { + + private static final SkriptAddon SKRIPT = Skript.getAddonInstance(); + + @Test + public void testStrings() { + DefaultFunction built = DefaultFunction.builder(SKRIPT, "test", String.class) + .description() + .since() + .keywords() + .parameter("x", String[].class, Modifier.OPTIONAL) + .build(args -> { + String[] xes = args.getOrDefault("x", new String[]{""}); + + return StringUtils.join(xes, ","); + }); + + assertEquals("test", built.getName()); + assertEquals(String.class, built.getReturnType().getC()); + assertTrue(built.isSingle()); + assertArrayEquals(new String[]{}, built.description()); + assertArrayEquals(new String[]{}, built.since()); + assertArrayEquals(new String[]{}, built.keywords()); + + Parameter[] parameters = built.getParameters(); + + assertEquals(new Parameter<>("x", DefaultFunction.getClassInfo(String[].class), false, null, false, true), parameters[0]); + } + + @Test + public void testObjectArrays() { + DefaultFunction built = DefaultFunction.builder(SKRIPT, "test", Object[].class) + .description("x", "y") + .since("1", "2") + .keywords("x", "y") + .parameter("x", Object[].class, Modifier.OPTIONAL) + .parameter("y", Boolean.class) + .build(args -> new Object[]{true, 1}); + + assertEquals("test", built.getName()); + assertEquals(Object.class, built.getReturnType().getC()); + assertFalse(built.isSingle()); + assertArrayEquals(new String[]{"x", "y"}, built.description()); + assertArrayEquals(new String[]{"1", "2"}, built.since()); + assertArrayEquals(new String[]{"x", "y"}, built.keywords()); + + Parameter[] parameters = built.getParameters(); + + assertEquals(new Parameter<>("x", DefaultFunction.getClassInfo(Object[].class), false, null, false, true), parameters[0]); + assertEquals(new Parameter<>("y", DefaultFunction.getClassInfo(Boolean.class), true, null), parameters[1]); + + Object[] execute = built.execute(consign(new Object[]{1, 2, 3}, new Boolean[]{true})); + + assertArrayEquals(new Object[]{true, 1}, execute); + + execute = built.execute(consign(new Object[]{}, new Boolean[]{true})); + + assertArrayEquals(new Object[]{true, 1}, execute); + } + + static Object[][] consign(Object... arguments) { + Object[][] consigned = new Object[arguments.length][]; + for (int i = 0; i < consigned.length; i++) { + if (arguments[i] instanceof Object[] || arguments[i] == null) { + consigned[i] = (Object[]) arguments[i]; + } else { + consigned[i] = new Object[]{arguments[i]}; + } + } + return consigned; + } + +} diff --git a/src/test/java/ch/njol/skript/lang/function/FunctionRegistryTest.java b/src/test/java/ch/njol/skript/lang/function/FunctionRegistryTest.java index e7b39f27f1a..b0788ff0631 100644 --- a/src/test/java/ch/njol/skript/lang/function/FunctionRegistryTest.java +++ b/src/test/java/ch/njol/skript/lang/function/FunctionRegistryTest.java @@ -7,6 +7,7 @@ import ch.njol.skript.registrations.DefaultClasses; import org.jetbrains.annotations.Nullable; import org.junit.Test; +import org.skriptlang.skript.lang.function.Parameter.Modifier; import static org.junit.Assert.*; diff --git a/src/test/skript/tests/syntaxes/functions/floor.sk b/src/test/skript/tests/syntaxes/functions/floor.sk new file mode 100644 index 00000000000..00838ce03ea --- /dev/null +++ b/src/test/skript/tests/syntaxes/functions/floor.sk @@ -0,0 +1,5 @@ +test "floor function": + assert floor(1) = 1 with "floor(1) should be 1" + assert floor(1.24) = 1 with "floor(1.24) should be 1" + assert floor(1346356276) = 1346356276 with "floor(1346356276) should be 1346356276" + assert floor(1346356276.24) = 1346356276 with "floor(1346356276.24) should be 1346356276"