diff --git a/src/main/java/ch/njol/skript/effects/EffSort.java b/src/main/java/ch/njol/skript/effects/EffSort.java
new file mode 100644
index 00000000000..40061777d50
--- /dev/null
+++ b/src/main/java/ch/njol/skript/effects/EffSort.java
@@ -0,0 +1,167 @@
+/**
+ * This file is part of Skript.
+ *
+ * Skript is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Skript is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Skript. If not, see .
+ *
+ * Copyright Peter Güttinger, SkriptLang team and contributors
+ */
+package ch.njol.skript.effects;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.classes.Changer.ChangeMode;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Keywords;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.expressions.ExprInput;
+import ch.njol.skript.expressions.ExprSortedList;
+import ch.njol.skript.lang.Effect;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.InputSource;
+import ch.njol.skript.lang.ParseContext;
+import ch.njol.skript.lang.SkriptParser;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.skript.lang.Variable;
+import ch.njol.skript.lang.parser.ParserInstance;
+import ch.njol.util.Kleenean;
+import ch.njol.util.Pair;
+import org.bukkit.event.Event;
+import org.eclipse.jdt.annotation.Nullable;
+import org.jetbrains.annotations.UnknownNullability;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+@Name("Sort")
+@Description({
+ "Sorts a list variable using either the natural ordering of the contents or the results of the given expression.",
+ "Be warned, this will overwrite the indices of the list variable."
+})
+@Examples({
+ "set {_words::*} to \"pineapple\", \"banana\", \"yoghurt\", and \"apple\"",
+ "sort {_words::*} # alphabetical sort",
+ "sort {_words::*} by length of input # shortest to longest",
+ "sort {_words::*} based on {tastiness::%input%} # sort based on custom value"
+})
+@Since("INSERT VERSION")
+@Keywords("input")
+public class EffSort extends Effect implements InputSource {
+
+ static {
+ Skript.registerEffect(EffSort.class, "sort %~objects% [(by|based on) <.+>]");
+ if (!ParserInstance.isRegistered(InputData.class))
+ ParserInstance.registerData(InputData.class, InputData::new);
+ }
+
+ @Nullable
+ private Expression> mappingExpr;
+ @Nullable
+ private String unparsedExpression;
+ private Variable> unsortedObjects;
+
+ private Set> dependentInputs = new HashSet<>();
+
+ @Nullable
+ private Object currentValue;
+ @UnknownNullability
+ private String currentIndex;
+
+ @Override
+ public boolean init(Expression>[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+ if (expressions[0].isSingle() || !(expressions[0] instanceof Variable)) {
+ Skript.error("You can only sort list variables!");
+ return false;
+ }
+ unsortedObjects = (Variable>) expressions[0];
+
+ if (!parseResult.regexes.isEmpty()) {
+ unparsedExpression = parseResult.regexes.get(0).group();
+ assert unparsedExpression != null;
+ InputData inputData = getParser().getData(InputData.class);
+ InputSource originalSource = inputData.getSource();
+ inputData.setSource(this);
+ mappingExpr = new SkriptParser(unparsedExpression, SkriptParser.PARSE_EXPRESSIONS, ParseContext.DEFAULT)
+ .parseExpression(Object.class);
+ inputData.setSource(originalSource);
+ return mappingExpr != null && mappingExpr.isSingle();
+ }
+ return true;
+ }
+
+ @Override
+ protected void execute(Event event) {
+ Object[] sorted;
+ if (mappingExpr == null) {
+ try {
+ sorted = unsortedObjects.stream(event)
+ .sorted(ExprSortedList::compare)
+ .toArray();
+ } catch (IllegalArgumentException | ClassCastException e) {
+ return;
+ }
+ } else {
+ Map valueToMappedValue = new LinkedHashMap<>();
+ for (Iterator> it = unsortedObjects.variablesIterator(event); it.hasNext(); ) {
+ Pair pair = it.next();
+ currentIndex = pair.getKey();
+ currentValue = pair.getValue();
+ Object mappedValue = mappingExpr.getSingle(event);
+ if (mappedValue == null)
+ return;
+ valueToMappedValue.put(currentValue, mappedValue);
+ }
+ try {
+ sorted = valueToMappedValue.entrySet().stream()
+ .sorted(Map.Entry.comparingByValue(ExprSortedList::compare))
+ .map(Map.Entry::getKey)
+ .toArray();
+ } catch (IllegalArgumentException | ClassCastException e) {
+ return;
+ }
+ }
+
+ unsortedObjects.change(event, sorted, ChangeMode.SET);
+ }
+
+ @Override
+ public Set> getDependentInputs() {
+ return dependentInputs;
+ }
+
+ @Override
+ public @Nullable Object getCurrentValue() {
+ return currentValue;
+ }
+
+ @Override
+ public boolean hasIndices() {
+ return true;
+ }
+
+ @Override
+ public @UnknownNullability String getCurrentIndex() {
+ return currentIndex;
+ }
+
+ @Override
+ public String toString(@Nullable Event event, boolean debug) {
+ return "sort" + unsortedObjects.toString(event, debug)
+ + (mappingExpr == null ? "" : " by " + mappingExpr.toString(event, debug));
+ }
+
+}
diff --git a/src/main/java/ch/njol/skript/expressions/ExprFilter.java b/src/main/java/ch/njol/skript/expressions/ExprFilter.java
index 239b720e418..a9747bbc3fc 100644
--- a/src/main/java/ch/njol/skript/expressions/ExprFilter.java
+++ b/src/main/java/ch/njol/skript/expressions/ExprFilter.java
@@ -27,83 +27,97 @@
import ch.njol.skript.lang.Condition;
import ch.njol.skript.lang.Expression;
import ch.njol.skript.lang.ExpressionType;
-import ch.njol.skript.lang.Literal;
+import ch.njol.skript.lang.InputSource;
import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.skript.lang.Variable;
+import ch.njol.skript.lang.parser.ParserInstance;
import ch.njol.skript.lang.util.SimpleExpression;
+import ch.njol.util.Pair;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.UnknownNullability;
import org.skriptlang.skript.lang.converter.Converters;
import ch.njol.skript.util.LiteralUtils;
-import ch.njol.skript.util.Utils;
import ch.njol.util.Kleenean;
-import ch.njol.util.coll.iterator.ArrayIterator;
import com.google.common.collect.Iterators;
import org.bukkit.event.Event;
-import org.eclipse.jdt.annotation.NonNull;
-import org.eclipse.jdt.annotation.Nullable;
-import java.lang.reflect.Array;
-import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashSet;
import java.util.Iterator;
-import java.util.List;
+import java.util.Set;
+import java.util.Spliterator;
+import java.util.Spliterators;
import java.util.regex.Pattern;
+import java.util.stream.StreamSupport;
@Name("Filter")
-@Description("Filters a list based on a condition. " +
- "For example, if you ran 'broadcast \"something\" and \"something else\" where [string input is \"something\"]', " +
- "only \"something\" would be broadcast as it is the only string that matched the condition.")
+@Description({
+ "Filters a list based on a condition. ",
+ "For example, if you ran 'broadcast \"something\" and \"something else\" where [string input is \"something\"]', ",
+ "only \"something\" would be broadcast as it is the only string that matched the condition."
+})
@Examples("send \"congrats on being staff!\" to all players where [player input has permission \"staff\"]")
@Since("2.2-dev36")
@SuppressWarnings({"null", "unchecked"})
-public class ExprFilter extends SimpleExpression {
-
- @Nullable
- private static ExprFilter parsing;
+public class ExprFilter extends SimpleExpression implements InputSource {
static {
Skript.registerExpression(ExprFilter.class, Object.class, ExpressionType.COMBINED,
"%objects% (where|that match) \\[<.+>\\]");
+ if (!ParserInstance.isRegistered(InputData.class))
+ ParserInstance.registerData(InputData.class, InputData::new);
}
- private Object current;
- private List> children = new ArrayList<>();
- private Condition condition;
- private String rawCond;
- private Expression objects;
+ private Condition filterCondition;
+ private String unparsedCondition;
+ private Expression> unfilteredObjects;
+ private Set> dependentInputs = new HashSet<>();
@Nullable
- public static ExprFilter getParsing() {
- return parsing;
- }
+ private Object currentFilterValue;
+ @Nullable
+ private String currentFilterIndex;
@Override
public boolean init(Expression>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
- try {
- parsing = this;
- objects = LiteralUtils.defendExpression(exprs[0]);
- if (objects.isSingle())
- return false;
- rawCond = parseResult.regexes.get(0).group();
- condition = Condition.parse(rawCond, "Can't understand this condition: " + rawCond);
- } finally {
- parsing = null;
- }
- return condition != null && LiteralUtils.canInitSafely(objects);
+ unfilteredObjects = LiteralUtils.defendExpression(exprs[0]);
+ if (unfilteredObjects.isSingle() || !LiteralUtils.canInitSafely(unfilteredObjects))
+ return false;
+ unparsedCondition = parseResult.regexes.get(0).group();
+ InputData inputData = getParser().getData(InputData.class);
+ InputSource originalSource = inputData.getSource();
+ inputData.setSource(this);
+ filterCondition = Condition.parse(unparsedCondition, "Can't understand this condition: " + unparsedCondition);
+ inputData.setSource(originalSource);
+ return filterCondition != null;
}
- @NonNull
+ @NotNull
@Override
public Iterator> iterator(Event event) {
- Iterator> objIterator = this.objects.iterator(event);
- if (objIterator == null)
+ if (unfilteredObjects instanceof Variable>) {
+ Iterator> variableIterator = ((Variable>) unfilteredObjects).variablesIterator(event);
+ return StreamSupport.stream(Spliterators.spliteratorUnknownSize(variableIterator, Spliterator.ORDERED), false)
+ .filter(pair -> {
+ currentFilterValue = pair.getValue();
+ currentFilterIndex = pair.getKey();
+ return filterCondition.check(event);
+ })
+ .map(Pair::getValue)
+ .iterator();
+ }
+
+ // clear current index just to be safe
+ currentFilterIndex = null;
+
+ Iterator> unfilteredObjectIterator = unfilteredObjects.iterator(event);
+ if (unfilteredObjectIterator == null)
return Collections.emptyIterator();
- try {
- return Iterators.filter(objIterator, object -> {
- current = object;
- return condition.check(event);
- });
- } finally {
- current = null;
- }
+ return Iterators.filter(unfilteredObjectIterator, candidateObject -> {
+ currentFilterValue = candidateObject;
+ return filterCondition.check(event);
+ });
}
@Override
@@ -115,148 +129,64 @@ protected Object[] get(Event event) {
}
}
- public Object getCurrent() {
- return current;
- }
-
- private void addChild(ExprInput> child) {
- children.add(child);
- }
-
- private void removeChild(ExprInput> child) {
- children.remove(child);
+ @Override
+ public boolean isSingle() {
+ return false;
}
@Override
public Class> getReturnType() {
- return objects.getReturnType();
+ return unfilteredObjects.getReturnType();
}
- @Override
- public boolean isSingle() {
- return objects.isSingle();
- }
@Override
public String toString(Event event, boolean debug) {
- return String.format("%s where [%s]", objects.toString(event, debug), rawCond);
+ return unfilteredObjects.toString(event, debug) + " that match [" + unparsedCondition + "]";
}
- @Override
- public boolean isLoopOf(String s) {
- for (ExprInput> child : children) { // if they used player input, let's assume loop-player is valid
- if (child.getClassInfo() == null || child.getClassInfo().getUserInputPatterns() == null)
- continue;
+ private boolean matchesAnySpecifiedTypes(String candidateString) {
+ for (ExprInput> dependentInput : dependentInputs) {
+ ClassInfo> specifiedType = dependentInput.getSpecifiedType();
+ if (specifiedType == null)
+ return false;
+ Pattern[] specifiedTypePatterns = specifiedType.getUserInputPatterns();
+ if (specifiedTypePatterns == null)
+ return false;
- for (Pattern pattern : child.getClassInfo().getUserInputPatterns()) {
- if (pattern.matcher(s).matches())
+ for (Pattern typePattern : specifiedTypePatterns) {
+ if (typePattern.matcher(candidateString).matches()) {
return true;
+ }
}
}
- return objects.isLoopOf(s); // nothing matched, so we'll rely on the object expression's logic
+ return false;
}
- @Name("Filter Input")
- @Description("Represents the input in a filter expression. " +
- "For example, if you ran 'broadcast \"something\" and \"something else\" where [input is \"something\"]" +
- "the condition would be checked twice, using \"something\" and \"something else\" as the inputs.")
- @Examples("send \"congrats on being staff!\" to all players where [input has permission \"staff\"]")
- @Since("2.2-dev36")
- public static class ExprInput extends SimpleExpression {
-
- static {
- Skript.registerExpression(ExprInput.class, Object.class, ExpressionType.COMBINED,
- "input",
- "%*classinfo% input"
- );
- }
-
- @Nullable
- private final ExprInput> source;
- private final Class extends T>[] types;
- private final Class superType;
- @SuppressWarnings("NotNullFieldNotInitialized")
- private ExprFilter parent;
- @Nullable
- private ClassInfo> inputType;
-
- public ExprInput() {
- this(null, (Class extends T>) Object.class);
- }
-
- public ExprInput(@Nullable ExprInput> source, Class extends T>... types) {
- this.source = source;
- if (source != null) {
- this.parent = source.parent;
- this.inputType = source.inputType;
- parent.removeChild(source);
- parent.addChild(this);
- }
-
- this.types = types;
- this.superType = (Class) Utils.getSuperType(types);
- }
-
- @Override
- public boolean init(Expression>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
- parent = ExprFilter.getParsing();
-
- if (parent == null)
- return false;
-
- parent.addChild(this);
- inputType = matchedPattern == 0 ? null : ((Literal>) exprs[0]).getSingle();
- return true;
- }
-
- @Override
- protected T[] get(Event event) {
- Object current = parent.getCurrent();
- if (inputType != null && !inputType.getC().isInstance(current)) {
- return null;
- }
-
- try {
- return Converters.convert(new Object[]{current}, types, superType);
- } catch (ClassCastException e1) {
- return (T[]) Array.newInstance(superType, 0);
- }
- }
-
- public void setParent(ExprFilter parent) {
- this.parent = parent;
- }
-
- @Override
- public Expression extends R> getConvertedExpression(Class... to) {
- return new ExprInput<>(this, to);
- }
- @Override
- public Expression> getSource() {
- return source == null ? this : source;
- }
-
- @Override
- public Class extends T> getReturnType() {
- return superType;
- }
+ @Override
+ public boolean isLoopOf(String candidateString) {
+ return unfilteredObjects.isLoopOf(candidateString) || matchesAnySpecifiedTypes(candidateString);
+ }
- @Nullable
- private ClassInfo> getClassInfo() {
- return inputType;
- }
+ public Set> getDependentInputs() {
+ return dependentInputs;
+ }
- @Override
- public boolean isSingle() {
- return true;
- }
+ @Nullable
+ public Object getCurrentValue() {
+ return currentFilterValue;
+ }
- @Override
- public String toString(Event event, boolean debug) {
- return inputType == null ? "input" : inputType.getCodeName() + " input";
- }
+ @Override
+ public boolean hasIndices() {
+ return unfilteredObjects instanceof Variable>;
+ }
+ @Override
+ @UnknownNullability
+ public String getCurrentIndex() {
+ return currentFilterIndex;
}
}
diff --git a/src/main/java/ch/njol/skript/expressions/ExprInput.java b/src/main/java/ch/njol/skript/expressions/ExprInput.java
new file mode 100644
index 00000000000..334fc79cd93
--- /dev/null
+++ b/src/main/java/ch/njol/skript/expressions/ExprInput.java
@@ -0,0 +1,170 @@
+/**
+ * This file is part of Skript.
+ *
+ * Skript is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Skript is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Skript. If not, see .
+ *
+ * Copyright Peter Güttinger, SkriptLang team and contributors
+ */
+package ch.njol.skript.expressions;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.classes.ClassInfo;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.ExpressionType;
+import ch.njol.skript.lang.InputSource;
+import ch.njol.skript.lang.InputSource.InputData;
+import ch.njol.skript.lang.Literal;
+import ch.njol.skript.lang.SkriptParser;
+import ch.njol.skript.lang.util.SimpleExpression;
+import ch.njol.skript.registrations.DefaultClasses;
+import ch.njol.skript.util.ClassInfoReference;
+import ch.njol.skript.util.Utils;
+import ch.njol.util.Kleenean;
+import org.bukkit.event.Event;
+import org.eclipse.jdt.annotation.Nullable;
+import org.skriptlang.skript.lang.converter.Converters;
+
+import java.lang.reflect.Array;
+import java.util.Set;
+
+@Name("Input")
+@Description({
+ "Represents the input in a filter expression or sort effect.",
+ "For example, if you ran 'broadcast \"something\" and \"something else\" where [input is \"something\"]",
+ "the condition would be checked twice, using \"something\" and \"something else\" as the inputs.",
+ "The 'input index' pattern can be used when acting on a variable to access the index of the input."
+})
+@Examples({
+ "send \"congrats on being staff!\" to all players where [input has permission \"staff\"]",
+ "sort {_list::*} based on length of input index"
+})
+@Since("2.2-dev36, INSERT_VERSION (input index)")
+public class ExprInput extends SimpleExpression {
+
+ static {
+ Skript.registerExpression(ExprInput.class, Object.class, ExpressionType.COMBINED,
+ "input",
+ "%*classinfo% input",
+ "input index"
+ );
+ }
+
+ @Nullable
+ private final ExprInput> source;
+ private final Class extends T>[] types;
+ private final Class superType;
+
+ private InputSource inputSource;
+
+ @Nullable
+ private ClassInfo> specifiedType;
+ private boolean isIndex = false;
+
+ public ExprInput() {
+ this(null, (Class extends T>) Object.class);
+ }
+
+ public ExprInput(@Nullable ExprInput> source, Class extends T>... types) {
+ this.source = source;
+ if (source != null) {
+ isIndex = source.isIndex;
+ specifiedType = source.specifiedType;
+ inputSource = source.inputSource;
+ Set> dependentInputs = inputSource.getDependentInputs();
+ dependentInputs.remove(this.source);
+ dependentInputs.add(this);
+ }
+ this.types = types;
+ this.superType = (Class) Utils.getSuperType(types);
+ }
+
+ @Override
+ public boolean init(Expression>[] exprs, int matchedPattern, Kleenean isDelayed, SkriptParser.ParseResult parseResult) {
+ inputSource = getParser().getData(InputData.class).getSource();
+ if (inputSource == null)
+ return false;
+ switch (matchedPattern) {
+ case 1:
+ ClassInfoReference classInfoReference = ((Literal) ClassInfoReference.wrap((Expression>) exprs[0])).getSingle();
+ if (classInfoReference.isPlural().isTrue()) {
+ Skript.error("An input can only be a single value! Please use a singular type (for example: players input -> player input).");
+ return false;
+ }
+ specifiedType = classInfoReference.getClassInfo();
+ break;
+ case 2:
+ if (!inputSource.hasIndices()) {
+ Skript.error("You cannot use 'input index' on lists without indices!");
+ return false;
+ }
+ specifiedType = DefaultClasses.STRING;
+ isIndex = true;
+ break;
+ default:
+ specifiedType = null;
+ }
+ return true;
+ }
+
+ @Override
+ protected T[] get(Event event) {
+ Object currentValue = isIndex ? inputSource.getCurrentIndex() : inputSource.getCurrentValue();
+ if (currentValue == null || (specifiedType != null && !specifiedType.getC().isInstance(currentValue)))
+ return (T[]) Array.newInstance(superType, 0);
+
+ try {
+ return Converters.convert(new Object[]{currentValue}, types, superType);
+ } catch (ClassCastException exception) {
+ return (T[]) Array.newInstance(superType, 0);
+ }
+ }
+
+ @Override
+ public Expression extends R> getConvertedExpression(Class... to) {
+ return new ExprInput<>(this, to);
+ }
+
+ @Override
+ public Expression> getSource() {
+ return source == null ? this : source;
+ }
+
+ @Override
+ public boolean isSingle() {
+ return true;
+ }
+
+ @Override
+ public Class extends T> getReturnType() {
+ return superType;
+ }
+
+ @Nullable
+ public ClassInfo> getSpecifiedType() {
+ return specifiedType;
+ }
+
+
+ @Override
+ public String toString(Event event, boolean debug) {
+ if (isIndex)
+ return "input index";
+ return specifiedType == null ? "input" : specifiedType.getCodeName() + " input";
+ }
+
+}
diff --git a/src/main/java/ch/njol/skript/lang/InputSource.java b/src/main/java/ch/njol/skript/lang/InputSource.java
new file mode 100644
index 00000000000..df541a0e40f
--- /dev/null
+++ b/src/main/java/ch/njol/skript/lang/InputSource.java
@@ -0,0 +1,101 @@
+/**
+ * This file is part of Skript.
+ *
+ * Skript is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Skript is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Skript. If not, see .
+ *
+ * Copyright Peter Güttinger, SkriptLang team and contributors
+ */
+package ch.njol.skript.lang;
+
+import ch.njol.skript.expressions.ExprInput;
+import ch.njol.skript.lang.parser.ParserInstance;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.UnknownNullability;
+
+import java.util.Set;
+
+/**
+ * An InputSource represents a syntax that can provide a
+ * value for {@link ExprInput} to use.
+ *
+ * @see ch.njol.skript.expressions.ExprFilter
+ * @see ch.njol.skript.effects.EffSort
+ */
+public interface InputSource {
+
+ /**
+ * @return A mutable {@link Set} of {@link ExprInput}s that depend on this source.
+ */
+ Set> getDependentInputs();
+
+ /**
+ * @return The current value that {@link ExprInput} should use.
+ */
+ @Nullable Object getCurrentValue();
+
+ /**
+ * {@link InputSource}s that can supply indices along with values should override this
+ * method to indicate their ability.
+ *
+ * @return Whether this source can return indices.
+ */
+ default boolean hasIndices() {
+ return false;
+ }
+
+ /**
+ * This should only be used by {@link InputSource}s that return true for {@link InputSource#hasIndices()}.
+ *
+ * @return The current value's index.
+ */
+ default @UnknownNullability String getCurrentIndex() {
+ return null;
+ }
+
+ /**
+ * A {@link ch.njol.skript.lang.parser.ParserInstance.Data} used for
+ * linking {@link InputSource}s and {@link ExprInput}s.
+ */
+ class InputData extends ParserInstance.Data {
+
+ @Nullable
+ private InputSource source;
+
+ public InputData(ParserInstance parserInstance) {
+ super(parserInstance);
+ }
+
+ /**
+ * {@link InputSource} should call this during init() to declare that they are the current source for future
+ * {@link ExprInput}s, and then reset it to its previous value once out of scope.
+ *
+ * @param source the source of information.
+ */
+ public void setSource(@Nullable InputSource source) {
+ this.source = source;
+ }
+
+ /**
+ * ExprInput should use this to get the information source, and then call
+ * {@link InputSource#getCurrentValue()} to get the current value of the source.
+ *
+ * @return the source of information.
+ */
+ @Nullable
+ public InputSource getSource() {
+ return source;
+ }
+
+ }
+}
diff --git a/src/test/skript/tests/syntaxes/effects/EffSort.sk b/src/test/skript/tests/syntaxes/effects/EffSort.sk
new file mode 100644
index 00000000000..4d832cd61fa
--- /dev/null
+++ b/src/test/skript/tests/syntaxes/effects/EffSort.sk
@@ -0,0 +1,47 @@
+test "sorting":
+ set {_numbers::*} to shuffled integers from 1 to 50
+ sort {_numbers::*}
+ assert {_numbers::*} is integers from 1 to 50 with "improper sorting of numbers"
+
+ set {_numbers::*} to shuffled integers from 1 to 5
+ sort {_numbers::*} by input * 20 + 4 - 3 # linear transformations don't affect order
+ assert {_numbers::*} is integers from 1 to 5 with "improper custom sorting of numbers"
+
+ set {_numbers::*} to shuffled integers from 1 to 5
+ set {_pre-sort-numbers::*} to {_numbers::*}
+ sort {_numbers::*} by "%input%" parsed as time # map expression returns null
+ assert {_numbers::*} is {_pre-sort-numbers::*} with "Invalid sorting expression adjusted list"
+
+ set {_numbers::*} to shuffled integers from 1 to 5
+ set {_pre-sort-numbers::*} to {_numbers::*}
+ sort {_numbers::*} by {_}
+ assert {_numbers::*} is {_pre-sort-numbers::*} with "Invalid sorting expression adjusted list"
+
+ set {_numbers::*} to {_}
+ sort {_numbers::*} by input + 3
+ assert {_numbers::*} is not set with "Invalid sorting of unset list"
+
+ set {_chars::*} to shuffled characters between "a" and "f"
+ sort {_chars::*}
+ assert {_chars::*} is characters between "a" and "f" with "improper sorting of chars"
+
+ set {_chars::*} to shuffled characters between "a" and "f"
+ sort {_chars::*} based on codepoint of input
+ assert {_chars::*} is characters between "a" and "f" with "improper custom sorting of chars"
+
+ set {_mixed::*} to shuffled (characters between "a" and "f", integers from 1 to 5)
+ set {_pre-sort-mixed::*} to {_mixed::*}
+ sort {_mixed::*}
+ assert {_mixed::*} is {_pre-sort-mixed::*} with "incomparable mixed list was adjusted"
+
+ set {_mixed::*} to shuffled (characters between "a" and "f", integers from 1 to 5)
+ sort {_mixed::*} by "%input%"
+ assert {_mixed::*} is 1, 2, 3, 4, 5, and characters between "a" and "f" with "improper custom sorting of mixed list"
+
+ set {_list::x} to 1
+ set {_list::aa} to 2
+ set {_list::bxs} to 3
+ set {_list::zysa} to 4
+ set {_list::aaaaa} to 5
+ sort {_list::*} by length of input index
+ assert {_list::*} is integers from 1 to 5 with "improper custom sorting based on index"
diff --git a/src/test/skript/tests/syntaxes/expressions/ExprFilter.sk b/src/test/skript/tests/syntaxes/expressions/ExprFilter.sk
index b1f1738f20d..bc195b894c8 100644
--- a/src/test/skript/tests/syntaxes/expressions/ExprFilter.sk
+++ b/src/test/skript/tests/syntaxes/expressions/ExprFilter.sk
@@ -3,4 +3,15 @@ test "where filter":
assert first element of ({_list::*} where [string input is "foo"]) is "foo" with "ExprFilter filtered incorrectly"
assert {_list::*} where [number input is set] is not set with "ExprFilter provided input value when classinfo did not match"
assert first element of ({_list::*} where [input is "foo"]) is "foo" with "ExprFilter filtered object input incorrectly"
+ assert first element of ({_list::*} where [input is "bar"]) is "bar" with "ExprFilter filtered object input incorrectly"
+ assert size of ({_list::*} where [input is "bar"]) is 1 with "ExprFilter filtered object input incorrectly"
+ assert first element of ({_list::*} where [input is "bar"]) is "bar" with "ExprFilter filtered object input incorrectly"
+ assert size of ({_list::*} where [input is "bar"]) is 1 with "ExprFilter filtered object input incorrectly"
+ assert first element of ({_list::*} where [input is "foobar"]) is "foobar" with "ExprFilter filtered object input incorrectly"
+ assert size of ({_list::*} where [input is "foobar"]) is 1 with "ExprFilter filtered object input incorrectly"
+ assert size of ({_list::*} where [input is "foo" or "bar"]) is 2 with "ExprFilter filtered object input incorrectly"
+ assert size of ({_list::*} where [input is set]) is 3 with "ExprFilter filtered object input incorrectly"
assert {_list::*} where [false is true] is not set with "ExprFilter returned objects with false condition"
+ assert ({_list::*} where [input is (("foo" and "bar") where [input is "bar"])]) is "bar" with "Failed filter with filter within condition"
+ assert (({_list::*} where [input is "foo"]) where [input is "foo"]) is "foo" with "Failed chained filters"
+ assert {_list::*} where [input index is "2" or "3"] is "bar" and "foobar" with "Failed input index filter"