Skip to content

Fix 'cannot be saved' on emphemeral vars. #8024

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

Merged
merged 4 commits into from
Jul 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 13 additions & 21 deletions src/main/java/ch/njol/skript/effects/EffChange.java
Original file line number Diff line number Diff line change
@@ -1,25 +1,5 @@
package ch.njol.skript.effects;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;

import ch.njol.skript.expressions.ExprParse;
import ch.njol.skript.lang.Effect;
import ch.njol.skript.lang.Expression;
import ch.njol.skript.lang.ExpressionList;
import ch.njol.skript.lang.KeyProviderExpression;
import ch.njol.skript.lang.KeyReceiverExpression;
import ch.njol.skript.lang.SkriptParser;
import ch.njol.skript.lang.SyntaxStringBuilder;
import ch.njol.skript.lang.Variable;
import ch.njol.skript.util.LiteralUtils;
import ch.njol.skript.variables.HintManager;
import org.skriptlang.skript.lang.script.ScriptWarning;
import org.bukkit.event.Event;
import org.jetbrains.annotations.Nullable;

import ch.njol.skript.Skript;
import ch.njol.skript.SkriptConfig;
import ch.njol.skript.classes.Changer;
Expand All @@ -29,13 +9,25 @@
import ch.njol.skript.doc.Examples;
import ch.njol.skript.doc.Name;
import ch.njol.skript.doc.Since;
import ch.njol.skript.expressions.ExprParse;
import ch.njol.skript.lang.*;
import ch.njol.skript.lang.SkriptParser.ParseResult;
import ch.njol.skript.log.CountingLogHandler;
import ch.njol.skript.log.ErrorQuality;
import ch.njol.skript.log.ParseLogHandler;
import ch.njol.skript.registrations.Classes;
import ch.njol.skript.util.LiteralUtils;
import ch.njol.skript.util.Patterns;
import ch.njol.skript.variables.HintManager;
import ch.njol.util.Kleenean;
import org.bukkit.event.Event;
import org.jetbrains.annotations.Nullable;
import org.skriptlang.skript.lang.script.ScriptWarning;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;

@Name("Change: Set/Add/Remove/Remove All/Delete/Reset")
@Description({
Expand Down Expand Up @@ -288,7 +280,7 @@ public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelaye
hintManager.add(variable, hints);
}
}
if (!variable.isLocal()) {
if (!variable.isLocal() && !variable.isEphemeral()) {
ClassInfo<?> changerInfo = Classes.getSuperClassInfo(changer.getReturnType());
if (changerInfo.getC() != Object.class && changerInfo.getSerializer() == null && changerInfo.getSerializeAs() == null
&& !SkriptConfig.disableObjectCannotBeSavedWarnings.value()
Expand Down
80 changes: 54 additions & 26 deletions src/main/java/ch/njol/skript/lang/Variable.java
Original file line number Diff line number Diff line change
@@ -1,25 +1,12 @@
package ch.njol.skript.lang;

import java.lang.reflect.Array;
import java.util.*;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.TreeMap;
import java.util.function.Predicate;
import java.util.function.Function;

import ch.njol.skript.Skript;
import ch.njol.skript.SkriptAPIException;
import ch.njol.skript.SkriptConfig;
import ch.njol.skript.classes.Changer;
import ch.njol.skript.classes.Changer.ChangeMode;
import ch.njol.skript.classes.Changer.ChangerUtils;
import ch.njol.skript.classes.ClassInfo;
import com.google.common.collect.Iterators;
import org.apache.commons.lang3.ArrayUtils;
import org.skriptlang.skript.lang.arithmetic.Arithmetics;
import org.skriptlang.skript.lang.arithmetic.OperationInfo;
import org.skriptlang.skript.lang.arithmetic.Operator;
import ch.njol.skript.lang.SkriptParser.ParseResult;
import ch.njol.skript.lang.parser.ParserInstance;
import ch.njol.skript.lang.util.SimpleExpression;
Expand All @@ -34,23 +21,34 @@
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;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.skriptlang.skript.lang.arithmetic.Arithmetics;
import org.skriptlang.skript.lang.arithmetic.OperationInfo;
import org.skriptlang.skript.lang.arithmetic.Operator;
import org.skriptlang.skript.lang.comparator.Comparators;
import org.skriptlang.skript.lang.comparator.Relation;
import org.skriptlang.skript.lang.converter.Converters;
import org.skriptlang.skript.lang.script.Script;
import org.skriptlang.skript.lang.script.ScriptWarning;

import java.lang.reflect.Array;
import java.util.*;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.function.Predicate;

public class Variable<T> implements Expression<T>, KeyReceiverExpression<T>, KeyProviderExpression<T> {

private final static String SINGLE_SEPARATOR_CHAR = ":";
public final static String SEPARATOR = SINGLE_SEPARATOR_CHAR + SINGLE_SEPARATOR_CHAR;
public final static String LOCAL_VARIABLE_TOKEN = "_";
public static final String EPHEMERAL_VARIABLE_TOKEN = "-";
private static final char[] reservedTokens = {'~', '.', '+', '$', '!', '&', '^', '*'};

/**
Expand All @@ -67,13 +65,14 @@ public class Variable<T> implements Expression<T>, KeyReceiverExpression<T>, Key
private final Class<? extends T>[] types;

private final boolean local;
private final boolean ephemeral;
private final boolean list;

private final @Nullable Variable<?> source;
private final Map<Event, String[]> cache = new WeakHashMap<>();

@SuppressWarnings("unchecked")
private Variable(VariableString name, Class<? extends T>[] types, boolean local, boolean list, @Nullable Variable<?> source) {
private Variable(VariableString name, Class<? extends T>[] types, boolean local, boolean ephemeral, boolean list, @Nullable Variable<?> source) {
assert types.length > 0;

assert name.isSimple() || name.getMode() == StringMode.VARIABLE_NAME;
Expand All @@ -83,6 +82,7 @@ private Variable(VariableString name, Class<? extends T>[] types, boolean local,
this.script = parser.isActive() ? parser.getCurrentScript() : null;

this.local = local;
this.ephemeral = ephemeral;
this.list = list;

this.name = name;
Expand Down Expand Up @@ -160,10 +160,14 @@ else if (character == '%')
}

/**
* Prints errors
* Creates a new variable instance with the given name and types. Prints errors.
* @param name The raw name of the variable.
* @param types The types this variable is expected to be.
* @return A new variable instance, or null if the name is invalid or the variable could not be created.
* @param <T> The supertype the variable is expected to be.
*/
public static <T> @Nullable Variable<T> newInstance(String name, Class<? extends T>[] types) {
name = "" + name.trim();
name = name.trim();
if (!isValidVariableName(name, true, true))
return null;
VariableString variableString = VariableString.newInstance(
Expand All @@ -172,17 +176,28 @@ else if (character == '%')
return null;

boolean isLocal = name.startsWith(LOCAL_VARIABLE_TOKEN);
boolean isEphemeral = name.startsWith(EPHEMERAL_VARIABLE_TOKEN);
boolean isPlural = name.endsWith(SEPARATOR + "*");

ParserInstance parser = ParserInstance.get();
Script currentScript = parser.isActive() ? parser.getCurrentScript() : null;

// check for 'starting with expression' warning
if (currentScript != null
&& !SkriptConfig.disableVariableStartingWithExpressionWarnings.value()
&& !currentScript.suppressesWarning(ScriptWarning.VARIABLE_STARTS_WITH_EXPRESSION)
&& (isLocal ? name.substring(LOCAL_VARIABLE_TOKEN.length()) : name).startsWith("%")) {
Skript.warning("Starting a variable's name with an expression is discouraged ({" + name + "}). " +
"You could prefix it with the script's name: " +
"{" + StringUtils.substring(currentScript.getConfig().getFileName(), 0, -3) + SEPARATOR + name + "}");
&& !currentScript.suppressesWarning(ScriptWarning.VARIABLE_STARTS_WITH_EXPRESSION)) {

String strippedName = name;
if (isLocal) {
strippedName = strippedName.substring(LOCAL_VARIABLE_TOKEN.length());
} else if (isEphemeral) {
strippedName = strippedName.substring(EPHEMERAL_VARIABLE_TOKEN.length());
}
if (strippedName.startsWith("%")) {
Skript.warning("Starting a variable's name with an expression is discouraged ({" + name + "}). " +
"You could prefix it with the script's name: " +
"{" + StringUtils.substring(currentScript.getConfig().getFileName(), 0, -3) + SEPARATOR + name + "}");
}
}

// Check for local variable type hints
Expand All @@ -191,7 +206,7 @@ else if (character == '%')
if (!hints.isEmpty()) { // Type hint(s) available
if (types[0] == Object.class) { // Object is generic, so we initialize with the hints instead
//noinspection unchecked
return new Variable<>(variableString, hints.toArray(new Class[0]), true, isPlural, null);
return new Variable<>(variableString, hints.toArray(new Class[0]), true, isEphemeral, isPlural, null);
}

List<Class<? extends T>> potentialTypes = new ArrayList<>();
Expand All @@ -205,7 +220,7 @@ else if (character == '%')
}
if (!potentialTypes.isEmpty()) { // Hint matches, use variable with exactly correct type
//noinspection unchecked
return new Variable<>(variableString, potentialTypes.toArray(Class[]::new), true, isPlural, null);
return new Variable<>(variableString, potentialTypes.toArray(Class[]::new), true, isEphemeral, isPlural, null);
}

// Hint exists and does NOT match any types requested
Expand All @@ -223,18 +238,31 @@ else if (character == '%')
}
}

return new Variable<>(variableString, types, isLocal, isPlural, null);
return new Variable<>(variableString, types, isLocal, isEphemeral, isPlural, null);
}

@Override
public boolean init(Expression<?>[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
throw new UnsupportedOperationException();
}

/**
* @return Whether this variable is a local variable, i.e. starts with {@link #LOCAL_VARIABLE_TOKEN}.
*/
public boolean isLocal() {
return local;
}

/**
* @return Whether this variable is an ephemeral variable, i.e. starts with {@link #EPHEMERAL_VARIABLE_TOKEN}.
*/
public boolean isEphemeral() {
return ephemeral;
}

/**
* @return Whether this variable is a list variable, i.e. ends with {@link #SEPARATOR + "*"}.
*/
public boolean isList() {
return list;
}
Expand Down Expand Up @@ -296,7 +324,7 @@ public <R> Variable<R> getConvertedExpression(Class<R>... to) {
if (!converterExists) {
return null;
}
return new Variable<>(name, to, local, list, this);
return new Variable<>(name, to, local, ephemeral, list, this);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/ch/njol/skript/variables/FlatFileStorage.java
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ public final void saveVariables(boolean finalSave) {
*/
@SuppressWarnings("unchecked")
private void save(PrintWriter pw, String parent, TreeMap<String, Object> map) {
if (parent.startsWith(Variables.EPHEMERAL_VARIABLE_PREFIX))
if (parent.startsWith(Variable.EPHEMERAL_VARIABLE_TOKEN))
// Skip ephemeral variables
return;

Expand All @@ -475,7 +475,7 @@ private void save(PrintWriter pw, String parent, TreeMap<String, Object> map) {
// Remove variable separator if needed
String name = childKey == null ? parent.substring(0, parent.length() - Variable.SEPARATOR.length()) : parent + childKey;

if (name.startsWith(Variables.EPHEMERAL_VARIABLE_PREFIX))
if (name.startsWith(Variable.EPHEMERAL_VARIABLE_TOKEN))
// Skip ephemeral variables
continue;

Expand Down
4 changes: 1 addition & 3 deletions src/main/java/ch/njol/skript/variables/Variables.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,6 @@ public class Variables {
*/
private static final String CONFIGURATION_SERIALIZABLE_PREFIX = "ConfigurationSerializable_";

public static final String EPHEMERAL_VARIABLE_PREFIX = "-";

private final static Multimap<Class<? extends VariablesStorage>, String> TYPES = HashMultimap.create();

// Register some things with Yggdrasil
Expand Down Expand Up @@ -889,7 +887,7 @@ public static SerializedVariable serialize(String name, @Nullable Object value)
* @param value the value of the variable.
*/
private static void saveVariableChange(String name, @Nullable Object value) {
if (name.startsWith(Variables.EPHEMERAL_VARIABLE_PREFIX))
if (name.startsWith(Variable.EPHEMERAL_VARIABLE_TOKEN))
return;
saveQueue.add(serialize(name, value));
}
Expand Down