Skip to content

Java function rework #7969

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 37 commits into
base: dev/feature
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
a11d4cd
init commit
Efnilite Jun 24, 2025
948ce19
deprecate java function
Efnilite Jun 24, 2025
15ff77c
add tests
Efnilite Jun 24, 2025
d51a56c
review comments
Efnilite Jun 25, 2025
3b45fc8
forgot one
Efnilite Jun 25, 2025
ab30936
fix tests
Efnilite Jun 25, 2025
6bb80ae
reformat
Efnilite Jun 26, 2025
d09c311
Apply suggestions from code review
Efnilite Jun 26, 2025
7ac7e78
review comments
Efnilite Jun 26, 2025
fcc4612
Merge remote-tracking branch 'origin/feature/java-function-rework' in…
Efnilite Jun 26, 2025
1089694
fix tests
Efnilite Jun 26, 2025
c280bba
forgot one :(
Efnilite Jun 26, 2025
8a600cd
Update src/main/java/ch/njol/skript/doc/Documentation.java
Efnilite Jun 26, 2025
5eee30b
review comments
Efnilite Jun 26, 2025
c89ef3d
add DefaultFunction#register
Efnilite Jun 28, 2025
4d18535
update return type
Efnilite Jun 28, 2025
baa7216
review comments
Efnilite Jun 30, 2025
e6429a2
Merge branch 'dev/feature' into feature/java-function-rework
Efnilite Jun 30, 2025
4142966
Merge branch 'dev/feature' into feature/java-function-rework
Efnilite Jul 1, 2025
7f16c59
fix tud's mistake
Efnilite Jul 1, 2025
9566fb0
Merge branch 'dev/feature' into feature/java-function-rework
Efnilite Jul 1, 2025
0419038
randomly deleted registry#register?
Efnilite Jul 1, 2025
e6fe8ba
update Parameter
Efnilite Jul 1, 2025
151ff17
fix tests
Efnilite Jul 1, 2025
7025f46
switch to new parameter
Efnilite Jul 3, 2025
71a0d17
new interface, prefer modifiers over variables
Efnilite Jul 3, 2025
65bbe62
fix tests
Efnilite Jul 3, 2025
6b27213
new parameter type!!
Efnilite Jul 3, 2025
bc660b7
fix
Efnilite Jul 3, 2025
b32f2c2
Update .github/CODEOWNERS
Efnilite Jul 6, 2025
5ca1086
Merge branch 'dev/feature' into feature/java-function-rework
Efnilite Jul 11, 2025
ffc1e77
add SkriptAddon to builder
Efnilite Jul 13, 2025
df925ad
seal!!
Efnilite Jul 13, 2025
4b64a36
unseal the box
Efnilite Jul 14, 2025
85701e0
Merge branch 'dev/feature' into feature/java-function-rework
Efnilite Jul 14, 2025
bf9dbd4
Merge branch 'dev/feature' into feature/java-function-rework
Efnilite Jul 18, 2025
6d11d81
fix test
Efnilite Jul 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -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
223 changes: 118 additions & 105 deletions src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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);
}
Expand All @@ -45,16 +51,20 @@ private static String str(double n) {

// basic math functions

Functions.registerFunction(new SimpleJavaFunction<Long>("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<Number>("round", new Parameter[] {new Parameter<>("n", DefaultClasses.NUMBER, true, null), new Parameter<>("d", DefaultClasses.NUMBER, true, new SimpleLiteral<Number>(0, false))}, DefaultClasses.NUMBER, true) {
@Override
Expand Down Expand Up @@ -353,53 +363,54 @@ public World[] executeSimple(Object[][] params) {
.examples("set {_nether} to world(\"%{_world}%_nether\")")
.since("2.2");

Functions.registerFunction(new JavaFunction<Location>("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<Number>(0, true)),
new Parameter<>("pitch", DefaultClasses.NUMBER, true, new SimpleLiteral<Number>(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.<Number>get("x").doubleValue(), args.<Number>get("y").doubleValue(), args.<Number>get("z").doubleValue(),
args.getOrDefault("yaw", 0f), args.getOrDefault("pitch", 0f));
})
.register();

Functions.registerFunction(new SimpleJavaFunction<Date>("date", new Parameter[] {
new Parameter<>("year", DefaultClasses.NUMBER, true, null),
Expand Down Expand Up @@ -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>("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.<Number>get("x").doubleValue(),
args.<Number>get("y").doubleValue(),
args.<Number>get("z").doubleValue()
))
.register();

Functions.registerFunction(new SimpleJavaFunction<Long>("calcExperience", new Parameter[] {
new Parameter<>("level", DefaultClasses.LONG, true, null)
Expand Down Expand Up @@ -529,27 +536,32 @@ public ColorRGB[] executeSimple(Object[][] params) {
)
.since("2.5, 2.10 (alpha)");

Functions.registerFunction(new SimpleJavaFunction<Player>("player", new Parameter[] {
new Parameter<>("nameOrUUID", DefaultClasses.STRING, true, null),
new Parameter<>("getExactPlayer", DefaultClasses.BOOLEAN, true, new SimpleLiteral<Boolean>(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\") # <none> 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\") # <none> 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
Expand Down Expand Up @@ -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<String>("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
{
Expand Down
36 changes: 23 additions & 13 deletions src/main/java/ch/njol/skript/doc/Documentation.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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(), "<br/>"), "functions");
final String since = validateHTML(func.getSince(), "functions");
String desc = validateHTML(StringUtils.join(typeDescription, "<br/>"), "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;
Expand All @@ -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);
}

Expand Down
Loading