From c1f16c320653e2f3c6ddb59f5ec5141929a7ce4f Mon Sep 17 00:00:00 2001 From: _tud <98935832+UnderscoreTud@users.noreply.github.com> Date: Fri, 17 Oct 2025 17:48:46 +0300 Subject: [PATCH 1/2] Expand the key API to allow for nested keys --- .../njol/skript/expressions/ExprFilter.java | 2 +- .../njol/skript/expressions/ExprIndices.java | 5 + .../ch/njol/skript/expressions/ExprKeyed.java | 23 +- .../skript/expressions/ExprRecursive.java | 109 ++++++++++ .../skript/expressions/ExprTransform.java | 2 +- .../skript/expressions/ExprValueWithin.java | 27 ++- .../expressions/base/WrapperExpression.java | 5 + .../java/ch/njol/skript/lang/Expression.java | 14 ++ .../skript/lang/KeyProviderExpression.java | 2 +- .../skript/lang/KeyReceiverExpression.java | 9 + .../java/ch/njol/skript/lang/KeyedValue.java | 29 +++ .../ch/njol/skript/lang/SkriptParser.java | 4 + .../java/ch/njol/skript/lang/Variable.java | 200 ++++++++++-------- .../njol/skript/lang/function/Function.java | 27 +-- .../lang/function/FunctionReference.java | 68 +++--- .../njol/skript/lang/function/Parameter.java | 51 ++++- .../skript/lang/util/ConvertedExpression.java | 5 + .../util/ConvertedKeyProviderExpression.java | 26 ++- .../syntaxes/expressions/ExprRecursive.sk | 61 ++++++ .../syntaxes/structures/StructFunction.sk | 8 + 20 files changed, 513 insertions(+), 164 deletions(-) create mode 100644 src/main/java/ch/njol/skript/expressions/ExprRecursive.java create mode 100644 src/test/skript/tests/syntaxes/expressions/ExprRecursive.sk diff --git a/src/main/java/ch/njol/skript/expressions/ExprFilter.java b/src/main/java/ch/njol/skript/expressions/ExprFilter.java index f9cc0b1786d..38102e3d2d0 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprFilter.java +++ b/src/main/java/ch/njol/skript/expressions/ExprFilter.java @@ -124,7 +124,7 @@ public boolean canReturnKeys() { @Override public boolean areKeysRecommended() { - return false; + return KeyProviderExpression.areKeysRecommended(unfilteredObjects); } @Override diff --git a/src/main/java/ch/njol/skript/expressions/ExprIndices.java b/src/main/java/ch/njol/skript/expressions/ExprIndices.java index 4f100289899..f0a7a7537fb 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprIndices.java +++ b/src/main/java/ch/njol/skript/expressions/ExprIndices.java @@ -93,6 +93,11 @@ public Class getReturnType() { return String.class; } + @Override + public boolean allowNestedStructures() { + return keyedExpression.allowNestedStructures(); + } + @Override public String toString(@Nullable Event e, boolean debug) { String text = "indices of " + keyedExpression.toString(e, debug); diff --git a/src/main/java/ch/njol/skript/expressions/ExprKeyed.java b/src/main/java/ch/njol/skript/expressions/ExprKeyed.java index cd58db0cd24..f250d6a165d 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprKeyed.java +++ b/src/main/java/ch/njol/skript/expressions/ExprKeyed.java @@ -6,12 +6,15 @@ import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.KeyProviderExpression; +import ch.njol.skript.lang.KeyedValue; import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.util.Kleenean; import org.bukkit.event.Event; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Iterator; + @Name("Keyed") @Description({ "This expression is used to explicitly pass the keys of an expression alongside its values.", @@ -69,12 +72,23 @@ public boolean init(Expression[] expressions, int matchedPattern, Kleenean is @Override public @NotNull String @NotNull [] getArrayKeys(Event event) throws IllegalStateException { - return ((KeyProviderExpression) getExpr()).getArrayKeys(event); + return getExpr().getArrayKeys(event); } @Override public @NotNull String @NotNull [] getAllKeys(Event event) { - return ((KeyProviderExpression) getExpr()).getAllKeys(event); + return getExpr().getAllKeys(event); + } + + @Override + public Iterator> keyedIterator(Event event) { + //noinspection unchecked,rawtypes + return (Iterator) getExpr().keyedIterator(event); + } + + @Override + public boolean isIndexLoop(String input) { + return getExpr().isIndexLoop(input); } @Override @@ -87,4 +101,9 @@ public String toString(@Nullable Event event, boolean debug) { return "keyed " + getExpr().toString(event, debug); } + @Override + public KeyProviderExpression getExpr() { + return (KeyProviderExpression) super.getExpr(); + } + } diff --git a/src/main/java/ch/njol/skript/expressions/ExprRecursive.java b/src/main/java/ch/njol/skript/expressions/ExprRecursive.java new file mode 100644 index 00000000000..d31dbe1511e --- /dev/null +++ b/src/main/java/ch/njol/skript/expressions/ExprRecursive.java @@ -0,0 +1,109 @@ +package ch.njol.skript.expressions; + +import ch.njol.skript.Skript; +import ch.njol.skript.SkriptAPIException; +import ch.njol.skript.doc.*; +import ch.njol.skript.expressions.base.WrapperExpression; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.ExpressionType; +import ch.njol.skript.lang.KeyProviderExpression; +import ch.njol.skript.lang.KeyedValue; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.util.Kleenean; +import org.bukkit.event.Event; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Iterator; + +@Name("Recursive") +@Description("Returns all values of an expression, including those in nested structures such as lists of lists.") +@Example(""" + on load: + set {_data::a::b::c} to "value1" + set {_data::a::b::d} to "value2" + set {_data::a::e} to "value3" + set {_data::f} to "value4" + + broadcast recursive {_data::*} + # broadcasts "value1", "value2", "value3", "value4" + + broadcast recursive indices of {_data::*} + # broadcasts "a::b::c", "a::b::d", "a::e", "f" + """) +@Since("INSERT VERSION") +@Keywords({"deep", "nested"}) +public class ExprRecursive extends WrapperExpression implements KeyProviderExpression { + + static { + Skript.registerExpression(ExprRecursive.class, Object.class, ExpressionType.COMBINED, "recursive %~objects%"); + } + + private boolean returnsKeys; + + @Override + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + if (!expressions[0].allowNestedStructures()) { + Skript.error(expressions[0] + " does not support nested structures."); + return false; + } + setExpr(expressions[0]); + returnsKeys = KeyProviderExpression.canReturnKeys(getExpr()); + return true; + } + + @Override + public Object[] getAll(Event event) { + return getExpr().getAll(event); + } + + @Override + public @NotNull String @NotNull [] getArrayKeys(Event event) throws IllegalStateException { + if (!returnsKeys) + throw new IllegalStateException(); + return ((KeyProviderExpression) getExpr()).getArrayKeys(event); + } + + @Override + public @NotNull String @NotNull [] getAllKeys(Event event) { + if (!returnsKeys) + throw new IllegalStateException(); + return ((KeyProviderExpression) getExpr()).getAllKeys(event); + } + + @Override + public Iterator> keyedIterator(Event event) { + if (!returnsKeys) + throw new IllegalStateException(); + //noinspection unchecked + return ((KeyProviderExpression) getExpr()).keyedIterator(event); + } + + @Override + public boolean canReturnKeys() { + return returnsKeys; + } + + @Override + public boolean areKeysRecommended() { + return KeyProviderExpression.areKeysRecommended(getExpr()); + } + + @Override + public boolean isIndexLoop(String input) { + if (!returnsKeys) + throw new IllegalStateException(); + return ((KeyProviderExpression) getExpr()).isIndexLoop(input); + } + + @Override + public boolean isLoopOf(String input) { + return getExpr().isLoopOf(input); + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return "recursive " + getExpr().toString(event, debug); + } + +} diff --git a/src/main/java/ch/njol/skript/expressions/ExprTransform.java b/src/main/java/ch/njol/skript/expressions/ExprTransform.java index 09e125d3b89..1d131b26f00 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprTransform.java +++ b/src/main/java/ch/njol/skript/expressions/ExprTransform.java @@ -144,7 +144,7 @@ public boolean canReturnKeys() { @Override public boolean areKeysRecommended() { - return false; + return KeyProviderExpression.areKeysRecommended(mappingExpr); } @Override diff --git a/src/main/java/ch/njol/skript/expressions/ExprValueWithin.java b/src/main/java/ch/njol/skript/expressions/ExprValueWithin.java index 35deb918493..7f3280c9cd1 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprValueWithin.java +++ b/src/main/java/ch/njol/skript/expressions/ExprValueWithin.java @@ -10,12 +10,13 @@ import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.registrations.Classes; import ch.njol.skript.util.ClassInfoReference; -import ch.njol.skript.util.Utils; import ch.njol.util.Kleenean; import org.bukkit.event.Event; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Iterator; + @Name("Value Within") @Description( "Gets the value within objects. Usually used with variables to get the value they store rather than the variable itself, " + @@ -81,6 +82,21 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye return ((KeyProviderExpression) getExpr()).getArrayKeys(event); } + @Override + public @NotNull String @NotNull [] getAllKeys(Event event) { + if (!returnsKeys) + throw new IllegalStateException(); + return ((KeyProviderExpression) getExpr()).getAllKeys(event); + } + + @Override + public Iterator> keyedIterator(Event event) { + if (!returnsKeys) + throw new IllegalStateException(); + //noinspection unchecked + return ((KeyProviderExpression) getExpr()).keyedIterator(event); + } + @Override public boolean canReturnKeys() { return returnsKeys; @@ -88,7 +104,7 @@ public boolean canReturnKeys() { @Override public boolean areKeysRecommended() { - return false; + return KeyProviderExpression.areKeysRecommended(getExpr()); } @Override @@ -107,6 +123,13 @@ public void change(Event event, @Nullable Object[] delta, ChangeMode mode) { changer.change(getArray(event), delta, mode); } + @Override + public boolean isIndexLoop(String input) { + if (!returnsKeys) + throw new IllegalStateException(); + return ((KeyProviderExpression) getExpr()).isIndexLoop(input); + } + @Override public boolean isLoopOf(String input) { return getExpr().isLoopOf(input); diff --git a/src/main/java/ch/njol/skript/expressions/base/WrapperExpression.java b/src/main/java/ch/njol/skript/expressions/base/WrapperExpression.java index 1a6173bd3d2..ae130fb13aa 100644 --- a/src/main/java/ch/njol/skript/expressions/base/WrapperExpression.java +++ b/src/main/java/ch/njol/skript/expressions/base/WrapperExpression.java @@ -91,6 +91,11 @@ public int getTime() { return expr.getTime(); } + @Override + public boolean allowNestedStructures() { + return expr.allowNestedStructures(); + } + @Override public boolean isDefault() { return expr.isDefault(); diff --git a/src/main/java/ch/njol/skript/lang/Expression.java b/src/main/java/ch/njol/skript/lang/Expression.java index 72e965d4661..01859f1ccad 100644 --- a/src/main/java/ch/njol/skript/lang/Expression.java +++ b/src/main/java/ch/njol/skript/lang/Expression.java @@ -239,6 +239,20 @@ default boolean canReturn(Class returnType) { */ int getTime(); + /** + * Allows nested structures for this expression, i.e. lists of lists. + *

+ * Note: + * Nested structures must be flattened in {@link #getArray(Event)} and {@link #getAll(Event)}, + * i.e. if this expression returns a list of lists of players, + * {@link #getArray(Event)} must return a single array containing all players of all lists + * + * @return Whether this expression allows nested structures. + */ + default boolean allowNestedStructures() { + return false; + } + /** * Returns whether this value represents the default value of its type for the event, i.e. it can be replaced with a call to event.getXyz() if one knows the event & value type. *

diff --git a/src/main/java/ch/njol/skript/lang/KeyProviderExpression.java b/src/main/java/ch/njol/skript/lang/KeyProviderExpression.java index 237d84025cb..f255cfede4d 100644 --- a/src/main/java/ch/njol/skript/lang/KeyProviderExpression.java +++ b/src/main/java/ch/njol/skript/lang/KeyProviderExpression.java @@ -27,6 +27,7 @@ * {@link #getAllKeys(Event)}. *

  • {@link #getArrayKeys(Event)} might be called after the corresponding {@link #getArray(Event)}
  • *
  • {@link #getAllKeys(Event)} might be called after the corresponding {@link #getAll(Event)}
  • + *
  • {@link #isLoopOf(String)} should be overridden to return {@code KeyProviderExpression.super.isLoopOf(input) || ...}
  • * *
    *

    Advice on Caching

    @@ -163,7 +164,6 @@ default boolean isLoopOf(String input) { return canReturnKeys() && isIndexLoop(input); } - /** * Checks whether the 'loop-...' expression should match this loop's index, * e.g. loop-index matches the index of a loop that iterates over a list variable. diff --git a/src/main/java/ch/njol/skript/lang/KeyReceiverExpression.java b/src/main/java/ch/njol/skript/lang/KeyReceiverExpression.java index e954c2ac5e0..d0cdfb56751 100644 --- a/src/main/java/ch/njol/skript/lang/KeyReceiverExpression.java +++ b/src/main/java/ch/njol/skript/lang/KeyReceiverExpression.java @@ -14,6 +14,15 @@ */ public interface KeyReceiverExpression extends Expression { + /** + * Returns whether this expression's changer supports nested structures. + * + * @return true if nested structures are supported, false otherwise + */ + default boolean acceptsNestedStructures() { + return false; + } + /** * An alternative changer method that provides a set of keys as well as a set of values. * This is only ever called for {@link ChangeMode#supportsKeyedChange()} safe change modes, diff --git a/src/main/java/ch/njol/skript/lang/KeyedValue.java b/src/main/java/ch/njol/skript/lang/KeyedValue.java index a516acf750b..11c71f40189 100644 --- a/src/main/java/ch/njol/skript/lang/KeyedValue.java +++ b/src/main/java/ch/njol/skript/lang/KeyedValue.java @@ -4,6 +4,7 @@ import org.jetbrains.annotations.Nullable; import java.util.*; +import java.util.function.Function; /** * A record that represents a key-value pair @@ -56,6 +57,34 @@ public KeyedValue withValue(@NotNull U newValue) { return new KeyedValue<>(key(), newValue); } + /** + * Maps an array of {@link KeyedValue} objects to a new array by applying a mapping function to each value. + *

    + * For each non-null element in the source array, the provided mapper function is applied to its value. + * If the result of the mapping is non-null, a new {@link KeyedValue} is created with the same key and the mapped value. + * If the mapping result is null, the corresponding element in the result array will be null. + *

    + * Null elements in the source array are skipped and remain null in the result array. + * + * @param source the source array of {@link KeyedValue} objects to map; may be null + * @param mapper a function to apply to each value in the source array + * @return a new array of {@link KeyedValue} objects with mapped values; never null, but may contain null elements + */ + public static KeyedValue[] map(KeyedValue[] source, Function mapper) { + if (source == null) + //noinspection unchecked + return new KeyedValue[0]; + //noinspection unchecked + KeyedValue[] mapped = new KeyedValue[source.length]; + for (int i = 0; i < source.length; i++) { + if (source[i] == null) + continue; + U mappedValue = mapper.apply(source[i].value()); + mapped[i] = mappedValue != null ? source[i].withValue(mappedValue) : null; + } + return mapped; + } + /** * Zips the given values and keys into a {@link KeyedValue} array. * diff --git a/src/main/java/ch/njol/skript/lang/SkriptParser.java b/src/main/java/ch/njol/skript/lang/SkriptParser.java index 713fc52f674..0a5a1d53972 100644 --- a/src/main/java/ch/njol/skript/lang/SkriptParser.java +++ b/src/main/java/ch/njol/skript/lang/SkriptParser.java @@ -1287,6 +1287,10 @@ record SignatureData(ClassInfo classInfo, boolean plural) { } } } + for (Expression param : params) { + if (KeyProviderExpression.areKeysRecommended(param)) + param.allowNestedStructures(); + } FunctionReference functionReference = new FunctionReference<>(functionName, SkriptLogger.getNode(), namespace, types, params); if (!functionReference.validateFunction(true)) { log.printError(); diff --git a/src/main/java/ch/njol/skript/lang/Variable.java b/src/main/java/ch/njol/skript/lang/Variable.java index 9d41e9e2deb..840b788ff5e 100644 --- a/src/main/java/ch/njol/skript/lang/Variable.java +++ b/src/main/java/ch/njol/skript/lang/Variable.java @@ -19,7 +19,6 @@ import ch.njol.util.Pair; import ch.njol.util.StringUtils; import ch.njol.util.coll.CollectionUtils; -import ch.njol.util.coll.iterator.EmptyIterator; import ch.njol.util.coll.iterator.SingleItemIterator; import com.google.common.collect.Iterators; import org.apache.commons.lang3.ArrayUtils; @@ -71,6 +70,8 @@ public class Variable implements Expression, KeyReceiverExpression, Key private final @Nullable Variable source; private final Map cache = new WeakHashMap<>(); + private ListProvider listProvider = new ShallowListProvider(); + @SuppressWarnings("unchecked") private Variable(VariableString name, Class[] types, boolean local, boolean ephemeral, boolean list, @Nullable Variable source) { assert types.length > 0; @@ -365,24 +366,9 @@ public Variable getConvertedExpression(Class... to) { Object rawValue = getRaw(event); if (!list) return rawValue; - if (rawValue == null) - return Array.newInstance(types[0], 0); - List convertedValues = new ArrayList<>(); - String name = StringUtils.substring(this.name.toString(event), 0, -1); - //noinspection unchecked - for (Entry variable : ((Map) rawValue).entrySet()) { - if (variable.getKey() != null && variable.getValue() != null) { - Object value; - if (variable.getValue() instanceof Map) - //noinspection unchecked - value = ((Map) variable.getValue()).get(null); - else - value = variable.getValue(); - if (value != null) - convertedValues.add(convertIfOldPlayer(name + variable.getKey(), local, event, value)); - } - } - return convertedValues.toArray(); + KeyedValue[] values = listProvider.getValues(event); + //noinspection unchecked,rawtypes + return KeyedValue.unzip((KeyedValue[]) values).values().toArray(); } /* @@ -406,14 +392,13 @@ public Variable getConvertedExpression(Class... to) { public Iterator> keyedIterator(Event event) { if (!list) throw new SkriptAPIException("Invalid call to keyedIterator"); - Iterator> transformed = Iterators.transform(variablesIterator(event), pair -> { - Object value = pair.getValue(); - if (value instanceof Map map) - value = map.get(null); - T converted = Converters.convert(value, types); + Iterator> iterator = Iterators.forArray(listProvider.getValues(event)); + Iterator> transformed = Iterators.transform(iterator, value -> { + assert value != null; + T converted = Converters.convert(value.value(), types); if (converted == null) return null; - return new KeyedValue<>(pair.getKey(), converted); + return new KeyedValue<>(value.key(), converted); }); return Iterators.filter(transformed, Objects::nonNull); } @@ -430,51 +415,8 @@ public Iterator> variablesIterator(Event event) { T value = getSingle(event); return value != null ? new SingleItemIterator<>(value) : null; } - String name = StringUtils.substring(this.name.toString(event), 0, -1); - Object value = Variables.getVariable(name + "*", event, local); - if (value == null) - return new EmptyIterator<>(); - assert value instanceof TreeMap; - // temporary list to prevent CMEs - //noinspection unchecked - Iterator keys = new ArrayList<>(((Map) value).keySet()).iterator(); - return new Iterator<>() { - private @Nullable T next = null; - - @Override - public boolean hasNext() { - if (next != null) - return true; - while (keys.hasNext()) { - @Nullable String key = keys.next(); - if (key != null) { - next = Converters.convert(Variables.getVariable(name + key, event, local), types); - - //noinspection unchecked - next = (T) convertIfOldPlayer(name + key, local, event, next); - if (next != null && !(next instanceof TreeMap)) - return true; - } - } - next = null; - return false; - } - - @Override - public T next() { - if (!hasNext()) - throw new NoSuchElementException(); - T n = next; - assert n != null; - next = null; - return n; - } - - @Override - public void remove() { - throw new UnsupportedOperationException(); - } - }; + //noinspection DataFlowIssue + return Iterators.transform(keyedIterator(event), KeyedValue::value); } private @Nullable T getConverted(Event event) { @@ -484,29 +426,16 @@ public void remove() { private T[] getConvertedArray(Event event) { assert list; - Object[] values = (Object[]) get(event); - String[] keys = getKeys(event); - assert values != null; //noinspection unchecked - T[] converted = (T[]) Array.newInstance(superType, values.length); - Converters.convert(values, converted, types); - for (int i = 0; i < converted.length; i++) { - if (converted[i] == null) - keys[i] = null; - } - cache.put(event, ArrayUtils.removeAllOccurrences(keys, null)); - return ArrayUtils.removeAllOccurrences(converted, null); - } + KeyedValue[] values = (KeyedValue[]) listProvider.getValues(event); + KeyedValue[] mappedValues = KeyedValue.map(values, value -> Converters.convert(value, types)); + mappedValues = ArrayUtils.removeAllOccurrences(mappedValues, null); - private String[] getKeys(Event event) { - assert list; - String name = StringUtils.substring(this.name.toString(event), 0, -1); - Object value = Variables.getVariable(name + "*", event, local); - if (value == null) - return new String[0]; - assert value instanceof Map; + KeyedValue.UnzippedKeyValues unzipped = KeyedValue.unzip(mappedValues); + + cache.put(event, unzipped.keys().toArray(new String[0])); //noinspection unchecked - return ((Map) value).keySet().toArray(new String[0]); + return unzipped.values().toArray((T[]) Array.newInstance(superType, 0)); } private void set(Event event, @Nullable Object value) { @@ -768,6 +697,14 @@ public boolean areKeysRecommended() { return false; // We want `set {list::*} to {other::*}` reset numbering! } + @Override + public boolean allowNestedStructures() { + if (!canReturnKeys()) + return false; + listProvider = new DeepListProvider(); + return true; + } + @Override public T[] getArray(Event event) { return getAll(event); @@ -845,4 +782,87 @@ public boolean supportsLoopPeeking() { return true; } + private interface ListProvider { + + KeyedValue[] getValues(Event event); + + } + + class ShallowListProvider implements ListProvider { + + @Override + public KeyedValue[] getValues(Event event) { + if (!list) + throw new SkriptAPIException("Invalid call to getValues on non-list variable"); + + Object rawValue = getRaw(event); + if (rawValue == null) + return new KeyedValue[0]; + + List> keyedValues = new ArrayList<>(); + String name = StringUtils.substring(Variable.this.name.toString(event), 0, -1); + //noinspection unchecked + for (Entry variable : ((Map) rawValue).entrySet()) { + if (variable.getKey() == null || variable.getValue() == null) + continue; + + Object value; + if (variable.getValue() instanceof Map sublist) { + value = sublist.get(null); + } else { + value = variable.getValue(); + } + + value = convertIfOldPlayer(name + variable.getKey(), local, event, value); + if (value != null) + keyedValues.add(new KeyedValue<>(variable.getKey(), value)); + } + + return keyedValues.toArray(new KeyedValue[0]); + } + + } + + class DeepListProvider implements ListProvider { + + @Override + public KeyedValue[] getValues(Event event) { + if (!list) + throw new SkriptAPIException("Invalid call to getValues on non-list variable"); + + Object rawValue = getRaw(event); + if (rawValue == null) + return new KeyedValue[0]; + + List> keyedValues = new ArrayList<>(); + String name = StringUtils.substring(Variable.this.name.toString(event), 0, -1); + getValuesRecursive(event, (Map) rawValue, name, "", keyedValues); + + return keyedValues.toArray(new KeyedValue[0]); + } + + private void getValuesRecursive(Event event, Map variable, String root, String prefix, List> values) { + //noinspection unchecked + for (Entry entry : ((Map) variable).entrySet()) { + if (entry.getKey() == null || entry.getValue() == null) + continue; + + String relativeKey = prefix + entry.getKey(); + String absoluteKey = root + relativeKey; + Object value; + if (entry.getValue() instanceof Map sublist) { + getValuesRecursive(event, (Map) entry.getValue(), root, relativeKey + SEPARATOR, values); + value = sublist.get(null); + } else { + value = entry.getValue(); + } + + value = convertIfOldPlayer(absoluteKey, local, event, value); + if (value != null) + values.add(new KeyedValue<>(relativeKey, value)); + } + } + + } + } 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 3734f1e925e..a8a695419a1 100644 --- a/src/main/java/ch/njol/skript/lang/function/Function.java +++ b/src/main/java/ch/njol/skript/lang/function/Function.java @@ -2,7 +2,6 @@ import ch.njol.skript.SkriptConfig; import ch.njol.skript.classes.ClassInfo; -import ch.njol.skript.lang.KeyProviderExpression; import ch.njol.skript.lang.KeyedValue; import ch.njol.util.coll.CollectionUtils; import org.bukkit.Bukkit; @@ -98,26 +97,9 @@ public boolean isSingle() { Parameter parameter = parameters[i]; Object[] parameterValue = parameter.hasModifier(Modifier.KEYED) ? convertToKeyed(parameterValues[i]) : parameterValues[i]; - // see https://github.com/SkriptLang/Skript/pull/8135 - if ((parameterValues[i] == null || parameterValues[i].length == 0) - && parameter.keyed - && parameter.def != null - ) { - Object[] defaultValue = parameter.def.getArray(event); - if (defaultValue.length == 1) { - parameterValue = KeyedValue.zip(defaultValue, null); - } else { - parameterValue = defaultValue; - } - } else if (!(this instanceof DefaultFunction) && parameterValue == null) { // Go for default value + if (parameterValue == null && !(this instanceof DefaultFunction)) { // Go for default value assert parameter.def != null; // Should've been parse error - Object[] defaultValue = parameter.def.getArray(event); - if (parameter.hasModifier(Modifier.KEYED) && KeyProviderExpression.areKeysRecommended(parameter.def)) { - String[] keys = ((KeyProviderExpression) parameter.def).getArrayKeys(event); - parameterValue = KeyedValue.zip(defaultValue, keys); - } else { - parameterValue = defaultValue; - } + parameterValue = parameter.evaluateDefault(event); } /* @@ -145,7 +127,10 @@ public boolean isSingle() { } private KeyedValue @Nullable [] convertToKeyed(Object[] values) { - if (values == null || values.length == 0) + if (values == null) + return null; + + if (values.length == 0) //noinspection unchecked return new KeyedValue[0]; diff --git a/src/main/java/ch/njol/skript/lang/function/FunctionReference.java b/src/main/java/ch/njol/skript/lang/function/FunctionReference.java index 478d2cd0bac..52e5c1eff51 100644 --- a/src/main/java/ch/njol/skript/lang/function/FunctionReference.java +++ b/src/main/java/ch/njol/skript/lang/function/FunctionReference.java @@ -386,68 +386,58 @@ 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).hasModifier(Modifier.KEYED)); + if (singleListParam) { // All parameters to one list + params[0] = evaluateSingleListParameter(function.getParameter(0), parameters, event); } else { // Use parameters in normal way for (int i = 0; i < parameters.length; i++) - params[i] = evaluateParameter(parameters[i], event, function.getParameter(i).hasModifier(Modifier.KEYED)); + //noinspection unchecked,rawtypes + params[i] = function.getParameter(i).evaluate((Expression) parameters[i], event); } // Execute the function return function.execute(params); } - private Object[] evaluateSingleListParameter(Expression[] parameters, Event event, boolean keyed) { - if (!keyed) { + private Object[] evaluateSingleListParameter(Parameter parameter, Expression[] arguments, Event event) { + if (arguments.length == 0) + return null; + + if (arguments.length == 1) + //noinspection unchecked,rawtypes + return parameter.evaluate((Expression) arguments[0], event); + + if (!parameter.hasModifier(Modifier.KEYED)) { List list = new ArrayList<>(); - for (Expression parameter : parameters) - list.addAll(Arrays.asList(evaluateParameter(parameter, event, false))); + for (Expression argument : arguments) + //noinspection unchecked,rawtypes + list.addAll(Arrays.asList(parameter.evaluate((Expression) argument, event))); return list.toArray(); } List values = new ArrayList<>(); Set keys = new LinkedHashSet<>(); int keyIndex = 1; - for (Expression parameter : parameters) { - Object[] valuesArray = parameter.getArray(event); - String[] keysArray = KeyProviderExpression.areKeysRecommended(parameter) - ? ((KeyProviderExpression) parameter).getArrayKeys(event) + for (Expression argument : arguments) { + Object[] valuesArray = argument.getArray(event); + String[] keysArray = KeyProviderExpression.areKeysRecommended(argument) + ? ((KeyProviderExpression) argument).getArrayKeys(event) : null; - // Don't allow mutating across function boundary; same hack is applied to variables - for (Object value : valuesArray) - values.add(Classes.clone(value)); - - if (keysArray != null) { - keys.addAll(Arrays.asList(keysArray)); - continue; - } - for (int i = 0; i < valuesArray.length; i++) { - while (keys.contains(String.valueOf(keyIndex))) - keyIndex++; - keys.add(String.valueOf(keyIndex++)); + if (keysArray == null) { + while (keys.contains(String.valueOf(keyIndex))) + keyIndex++; + keys.add(String.valueOf(keyIndex++)); + } else if (!keys.add(keysArray[i])) { + continue; + } + // Don't allow mutating across function boundary; same hack is applied to variables + values.add(Classes.clone(valuesArray[i])); } } return KeyedValue.zip(values.toArray(), keys.toArray(new String[0])); } - private Object[] evaluateParameter(Expression parameter, Event event, boolean keyed) { - Object[] values = parameter.getArray(event); - - // Don't allow mutating across function boundary; same hack is applied to variables - for (int i = 0; i < values.length; i++) - values[i] = Classes.clone(values[i]); - - if (!keyed) - return values; - - String[] keys = KeyProviderExpression.areKeysRecommended(parameter) - ? ((KeyProviderExpression) parameter).getArrayKeys(event) - : null; - return KeyedValue.zip(values, keys); - } - public boolean isSingle() { return contract.isSingle(parameters); } diff --git a/src/main/java/ch/njol/skript/lang/function/Parameter.java b/src/main/java/ch/njol/skript/lang/function/Parameter.java index 4b3f9abed54..66efe0f775e 100644 --- a/src/main/java/ch/njol/skript/lang/function/Parameter.java +++ b/src/main/java/ch/njol/skript/lang/function/Parameter.java @@ -3,10 +3,7 @@ import ch.njol.skript.Skript; import ch.njol.skript.SkriptConfig; import ch.njol.skript.classes.ClassInfo; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ParseContext; -import ch.njol.skript.lang.SkriptParser; -import ch.njol.skript.lang.Variable; +import ch.njol.skript.lang.*; import ch.njol.skript.log.RetainingLogHandler; import ch.njol.skript.log.SkriptLogger; import ch.njol.skript.registrations.Classes; @@ -14,6 +11,7 @@ import ch.njol.skript.util.Utils; import ch.njol.util.NonNullPair; import ch.njol.util.StringUtils; +import org.bukkit.event.Event; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; @@ -238,6 +236,51 @@ public ClassInfo getType() { return params; } + /** + * Evaluates this parameter's default expression. + * + * @return an object array containing either value-only elements or {@code KeyedValue[]} when keyed + * @throws IllegalStateException if this parameter does not have a default value + * @see #evaluate(Expression, Event) + */ + public Object[] evaluateDefault(Event event) { + if (def == null) + throw new IllegalStateException("This parameter does not have a default value"); + return evaluate(def, event); + } + + /** + * Evaluates the provided argument expression and returns the resulting values, taking into account this parameter's modifiers. + * + *

    If this parameter has the {@link org.skriptlang.skript.common.function.Parameter.Modifier#KEYED} modifier, + * the returned array will contain {@link ch.njol.skript.lang.KeyedValue} objects, pairing each value with its corresponding key. + * If the argument expression does not provide keys, numerical indices (1, 2, 3, ...) will be used as keys; + * otherwise, the returned array will contain only the values.

    + * + * @return an object array containing either value-only elements or {@code KeyedValue[]} when keyed + */ + public Object[] evaluate(@Nullable Expression argument, Event event) { + if (argument == null) { + if (!modifiers.contains(Modifier.OPTIONAL)) + throw new IllegalStateException("This parameter is required, but no argument was provided"); + return evaluateDefault(event); + } + + Object[] values = argument.getArray(event); + + // Don't allow mutating across function boundary; same hack is applied to variables + for (int i = 0; i < values.length; i++) + values[i] = Classes.clone(values[i]); + + if (!hasModifier(Modifier.KEYED)) + return values; + + String[] keys = KeyProviderExpression.areKeysRecommended(argument) + ? ((KeyProviderExpression) argument).getArrayKeys(event) + : null; + return KeyedValue.zip(values, keys); + } + /** * @deprecated Use {@link #name()} instead. */ diff --git a/src/main/java/ch/njol/skript/lang/util/ConvertedExpression.java b/src/main/java/ch/njol/skript/lang/util/ConvertedExpression.java index f6a54feacd3..f847e9894a5 100644 --- a/src/main/java/ch/njol/skript/lang/util/ConvertedExpression.java +++ b/src/main/java/ch/njol/skript/lang/util/ConvertedExpression.java @@ -249,6 +249,11 @@ public int getTime() { return source.getTime(); } + @Override + public boolean allowNestedStructures() { + return source.allowNestedStructures(); + } + @Override public boolean isDefault() { return source.isDefault(); diff --git a/src/main/java/ch/njol/skript/lang/util/ConvertedKeyProviderExpression.java b/src/main/java/ch/njol/skript/lang/util/ConvertedKeyProviderExpression.java index 917dbbb8bf3..7f710b7fbc6 100644 --- a/src/main/java/ch/njol/skript/lang/util/ConvertedKeyProviderExpression.java +++ b/src/main/java/ch/njol/skript/lang/util/ConvertedKeyProviderExpression.java @@ -3,15 +3,17 @@ import ch.njol.skript.classes.Changer.ChangeMode; import ch.njol.skript.lang.KeyProviderExpression; import ch.njol.skript.lang.KeyReceiverExpression; +import ch.njol.skript.lang.KeyedValue; +import com.google.common.collect.Iterators; import org.apache.commons.lang3.ArrayUtils; import org.bukkit.event.Event; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.lang.converter.ConverterInfo; import org.skriptlang.skript.lang.converter.Converters; import java.lang.reflect.Array; -import java.util.Collection; -import java.util.WeakHashMap; +import java.util.*; import java.util.function.Consumer; /** @@ -101,9 +103,27 @@ public void change(Event event, Object @NotNull [] delta, ChangeMode mode, @NotN } } + @Override + public boolean isIndexLoop(String input) { + return getSource().isIndexLoop(input); + } + @Override public boolean isLoopOf(String input) { - return getSource().isLoopOf(input); + return KeyProviderExpression.super.isLoopOf(input); + } + + @Override + public Iterator> keyedIterator(Event event) { + Iterator> source = getSource().keyedIterator(event); + return Iterators.filter( + Iterators.transform(source, keyedValue -> { + assert keyedValue != null; + T convertedValue = converter.convert(keyedValue.value()); + return convertedValue != null ? keyedValue.withValue(convertedValue) : null; + }), + Objects::nonNull + ); } } diff --git a/src/test/skript/tests/syntaxes/expressions/ExprRecursive.sk b/src/test/skript/tests/syntaxes/expressions/ExprRecursive.sk new file mode 100644 index 00000000000..cd328947b53 --- /dev/null +++ b/src/test/skript/tests/syntaxes/expressions/ExprRecursive.sk @@ -0,0 +1,61 @@ +local function objects(objects: objects) returns objects: + return recursive {_objects::*} + +test "recursive list": + set {_data::a::b} to "ab" + set {_data::a::b::c} to "abc" + set {_data::a::b::d} to "abd" + set {_data::a::e} to "ae" + set {_data::f} to "f" + + set {_copy::*} to recursive {_data::*} + assert size of {_copy::*} is 5 + assert {_copy} is not set + assert {_copy::1} is "ab" + assert {_copy::2} is "abc" + assert {_copy::3} is "abd" + assert {_copy::4} is "ae" + assert {_copy::5} is "f" + delete {_copy::*} + + set {_copy::*} to recursive keyed {_data::*} + assert size of {_copy::*} is 5 + assert {_copy} is not set + assert {_copy::a::b} is "ab" + assert {_copy::a::b::c} is "abc" + assert {_copy::a::b::d} is "abd" + assert {_copy::a::e} is "ae" + assert {_copy::f} is "f" + +test "recursive function": + set {_data::a::b} to "ab" + set {_data::a::b::c} to "abc" + set {_data::a::b::d} to "abd" + set {_data::a::e} to "ae" + set {_data::f} to "f" + + set {_copy::*} to objects({_data::*}) + assert size of {_copy::*} is 1 + assert {_copy} is not set + assert {_copy::1} is "f" + delete {_copy::*} + + set {_copy::*} to objects(recursive {_data::*}) + assert size of {_copy::*} is 5 + assert {_copy} is not set + assert {_copy::1} is "ab" + assert {_copy::2} is "abc" + assert {_copy::3} is "abd" + assert {_copy::4} is "ae" + assert {_copy::5} is "f" + delete {_copy::*} + + set {_copy::*} to keyed objects(recursive {_data::*}) + assert size of {_copy::*} is 5 + assert {_copy} is not set + assert {_copy::a::b} is "ab" + assert {_copy::a::b::c} is "abc" + assert {_copy::a::b::d} is "abd" + assert {_copy::a::e} is "ae" + assert {_copy::f} is "f" + delete {_copy::*} diff --git a/src/test/skript/tests/syntaxes/structures/StructFunction.sk b/src/test/skript/tests/syntaxes/structures/StructFunction.sk index 1fa8f21a682..ff9f740b21e 100644 --- a/src/test/skript/tests/syntaxes/structures/StructFunction.sk +++ b/src/test/skript/tests/syntaxes/structures/StructFunction.sk @@ -69,3 +69,11 @@ test "literal type parsing": literal_test_projectile(projectile) assert last parse logs are not set clear entity within {_entity} + +local function duplicate_keys_test(x: strings) :: strings: + return {_x::*} + +test "duplicate keys in function arguments": + set {_a::foo} to "first" + set {_b::foo} to "second" + assert duplicate_keys_test(keyed {_a::foo}, keyed {_b::foo}) is "first" From e3d31aae96bec1223ef4b3a35fafd6f8efdafb73 Mon Sep 17 00:00:00 2001 From: _tud <98935832+UnderscoreTud@users.noreply.github.com> Date: Fri, 17 Oct 2025 20:53:52 +0300 Subject: [PATCH 2/2] Fix tests --- .../syntaxes/expressions/ExprRecursive.sk | 28 ++++++++++--------- .../syntaxes/structures/StructFunction.sk | 2 +- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/test/skript/tests/syntaxes/expressions/ExprRecursive.sk b/src/test/skript/tests/syntaxes/expressions/ExprRecursive.sk index cd328947b53..061599670be 100644 --- a/src/test/skript/tests/syntaxes/expressions/ExprRecursive.sk +++ b/src/test/skript/tests/syntaxes/expressions/ExprRecursive.sk @@ -2,35 +2,36 @@ local function objects(objects: objects) returns objects: return recursive {_objects::*} test "recursive list": - set {_data::a::b} to "ab" set {_data::a::b::c} to "abc" set {_data::a::b::d} to "abd" + set {_data::a::b} to "ab" set {_data::a::e} to "ae" set {_data::f} to "f" set {_copy::*} to recursive {_data::*} assert size of {_copy::*} is 5 assert {_copy} is not set - assert {_copy::1} is "ab" - assert {_copy::2} is "abc" - assert {_copy::3} is "abd" + assert {_copy::1} is "abc" + assert {_copy::2} is "abd" + assert {_copy::3} is "ab" assert {_copy::4} is "ae" assert {_copy::5} is "f" delete {_copy::*} set {_copy::*} to recursive keyed {_data::*} - assert size of {_copy::*} is 5 + assert size of {_copy::*} is 1 + assert recursive size of {_copy::*} is 5 assert {_copy} is not set - assert {_copy::a::b} is "ab" assert {_copy::a::b::c} is "abc" assert {_copy::a::b::d} is "abd" + assert {_copy::a::b} is "ab" assert {_copy::a::e} is "ae" assert {_copy::f} is "f" test "recursive function": - set {_data::a::b} to "ab" set {_data::a::b::c} to "abc" set {_data::a::b::d} to "abd" + set {_data::a::b} to "ab" set {_data::a::e} to "ae" set {_data::f} to "f" @@ -43,19 +44,20 @@ test "recursive function": set {_copy::*} to objects(recursive {_data::*}) assert size of {_copy::*} is 5 assert {_copy} is not set - assert {_copy::1} is "ab" - assert {_copy::2} is "abc" - assert {_copy::3} is "abd" + assert {_copy::1} is "abc" + assert {_copy::2} is "abd" + assert {_copy::3} is "ab" assert {_copy::4} is "ae" assert {_copy::5} is "f" delete {_copy::*} - set {_copy::*} to keyed objects(recursive {_data::*}) - assert size of {_copy::*} is 5 + set {_copy::*} to keyed objects(keyed {_data::*}) + assert size of {_copy::*} is 1 + assert recursive size of {_copy::*} is 5 assert {_copy} is not set - assert {_copy::a::b} is "ab" assert {_copy::a::b::c} is "abc" assert {_copy::a::b::d} is "abd" + assert {_copy::a::b} is "ab" assert {_copy::a::e} is "ae" assert {_copy::f} is "f" delete {_copy::*} diff --git a/src/test/skript/tests/syntaxes/structures/StructFunction.sk b/src/test/skript/tests/syntaxes/structures/StructFunction.sk index ff9f740b21e..36a2e834270 100644 --- a/src/test/skript/tests/syntaxes/structures/StructFunction.sk +++ b/src/test/skript/tests/syntaxes/structures/StructFunction.sk @@ -76,4 +76,4 @@ local function duplicate_keys_test(x: strings) :: strings: test "duplicate keys in function arguments": set {_a::foo} to "first" set {_b::foo} to "second" - assert duplicate_keys_test(keyed {_a::foo}, keyed {_b::foo}) is "first" + assert duplicate_keys_test(keyed {_a::*}, keyed {_b::*}) is "first"