Merge branch 'master' into 1.17
This commit is contained in:
commit
84dd972758
24 files changed
+2438
-16
No files matched your search
@@ -82,6 +82,7 @@ public class CarpetServer implements ClientModInitializer,DedicatedServerModInit
|
||||
settingsManager.parseSettingsClass(CarpetSettings.class);
|
||||
extensions.forEach(CarpetExtension::onGameStarted);
|
||||
FabricAPIHooks.initialize();
|
||||
CarpetScriptServer.parseFunctionClasses();
|
||||
}
|
||||
|
||||
public static void onServerLoaded(MinecraftServer server)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package carpet;
|
||||
|
||||
import carpet.script.utils.ScriptDownloader;
|
||||
import carpet.settings.ParsedRule;
|
||||
import carpet.settings.Rule;
|
||||
import carpet.settings.SettingsManager;
|
||||
@@ -456,6 +457,18 @@ public class CarpetSettings
|
||||
)
|
||||
public static boolean scriptsOptimization = true;
|
||||
|
||||
@Rule(
|
||||
desc = "Location of the online repository of scarpet apps",
|
||||
extra = {
|
||||
"set to 'none' to disable.",
|
||||
"Point to any github repo with scarpet apps",
|
||||
"using <user>/<repo>/contents/<path...>"
|
||||
},
|
||||
category = SCARPET,
|
||||
validate= ScriptDownloader.ScarpetAppStoreValidator.class
|
||||
)
|
||||
public static String scriptsAppStore = "gnembon/scarpet/contents/programs";
|
||||
|
||||
|
||||
@Rule(desc = "Enables /player command to control/spawn players", category = COMMAND)
|
||||
public static String commandPlayer = "ops";
|
||||
|
||||
@@ -2,6 +2,8 @@ package carpet.commands;
|
||||
|
||||
import carpet.CarpetServer;
|
||||
import carpet.CarpetSettings;
|
||||
import carpet.script.CarpetScriptServer;
|
||||
import carpet.script.utils.ScriptDownloader;
|
||||
import carpet.script.CarpetEventServer;
|
||||
import carpet.script.CarpetExpression;
|
||||
import carpet.script.CarpetScriptHost;
|
||||
@@ -20,6 +22,7 @@ import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import net.minecraft.block.Block;
|
||||
@@ -36,6 +39,7 @@ import net.minecraft.server.world.ServerWorld;
|
||||
import net.minecraft.util.math.BlockBox;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
@@ -105,6 +109,27 @@ public class ScriptCommand
|
||||
return suggestionsBuilder.buildFuture();
|
||||
}
|
||||
|
||||
/**
|
||||
* A method to suggest the available scarpet scripts based off of the current player input and {@link ScriptDownloader#localScarpetRepoStructure}
|
||||
* variable.
|
||||
*/
|
||||
private static CompletableFuture<Suggestions> suggestDownloadableApps(
|
||||
CommandContext<ServerCommandSource> context,
|
||||
SuggestionsBuilder suggestionsBuilder
|
||||
) throws CommandSyntaxException {
|
||||
try {
|
||||
String previous = suggestionsBuilder.getRemaining();
|
||||
List<String> suggestions = ScriptDownloader.suggestionsFromPath(previous);
|
||||
CarpetScriptServer.LOG.error(String.join(":", suggestions));
|
||||
ScriptDownloader.suggestionsFromPath(previous).forEach(suggestionsBuilder::suggest);
|
||||
return suggestionsBuilder.buildFuture();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new SimpleCommandExceptionType(Messenger.c("rb "+e.getMessage())).create();
|
||||
}
|
||||
}
|
||||
|
||||
public static void register(CommandDispatcher<ServerCommandSource> dispatcher)
|
||||
{
|
||||
LiteralArgumentBuilder<ServerCommandSource> b = literal("globals").
|
||||
@@ -128,7 +153,7 @@ public class ScriptCommand
|
||||
null,
|
||||
null,
|
||||
""
|
||||
)).
|
||||
)) .
|
||||
then(argument("arguments", StringArgumentType.greedyString()).
|
||||
executes( (cc) -> invoke(
|
||||
cc,
|
||||
@@ -302,10 +327,13 @@ public class ScriptCommand
|
||||
StringArgumentType.getString(cc, "call")
|
||||
)?1:0))));
|
||||
|
||||
LiteralArgumentBuilder<ServerCommandSource> d = literal("download").requires((player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandScriptACE)).
|
||||
then(literal("global").then(argument("path", StringArgumentType.greedyString()).suggests(ScriptCommand::suggestDownloadableApps).executes(cc->ScriptDownloader.downloadScript(cc, StringArgumentType.getString(cc,"path"), true)))).
|
||||
then(literal("local").then(argument("path", StringArgumentType.greedyString()).suggests(ScriptCommand::suggestDownloadableApps).executes(cc->ScriptDownloader.downloadScript(cc, StringArgumentType.getString(cc,"path"), false))));
|
||||
|
||||
dispatcher.register(literal("script").
|
||||
requires((player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandScript)).
|
||||
then(b).then(u).then(o).then(l).then(s).then(c).then(h).then(i).then(e).then(t).then(a).then(f).then(q));
|
||||
then(b).then(u).then(o).then(l).then(s).then(c).then(h).then(i).then(e).then(t).then(a).then(f).then(q).then(d));
|
||||
dispatcher.register(literal("script").
|
||||
requires((player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandScript)).
|
||||
then(literal("in").
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package carpet.fakes;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface BiomeEffectsInterface
|
||||
{
|
||||
int getCMFogColor();
|
||||
Optional<Integer> getCMFoliageColor();
|
||||
int getCMSkyColor();
|
||||
int getCMWaterColor();
|
||||
int getCMWaterFogColor();
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package carpet.mixins;
|
||||
|
||||
import carpet.fakes.BiomeEffectsInterface;
|
||||
import net.minecraft.world.biome.BiomeEffects;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Mixin(BiomeEffects.class)
|
||||
public class BiomeEffects_scarpetMixin implements BiomeEffectsInterface
|
||||
{
|
||||
@Shadow @Final private int fogColor;
|
||||
|
||||
@Shadow @Final private Optional<Integer> foliageColor;
|
||||
|
||||
@Shadow @Final private int skyColor;
|
||||
|
||||
@Shadow @Final private int waterColor;
|
||||
|
||||
@Shadow @Final private int waterFogColor;
|
||||
|
||||
@Override
|
||||
public int getCMFogColor()
|
||||
{
|
||||
return fogColor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Integer> getCMFoliageColor()
|
||||
{
|
||||
return foliageColor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCMSkyColor()
|
||||
{
|
||||
return skyColor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCMWaterColor()
|
||||
{
|
||||
return waterColor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCMWaterFogColor()
|
||||
{
|
||||
return waterFogColor;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package carpet.script;
|
||||
|
||||
import carpet.CarpetServer;
|
||||
import carpet.script.annotation.AnnotationParser;
|
||||
import carpet.script.api.Auxiliary;
|
||||
import carpet.script.api.BlockIterators;
|
||||
import carpet.script.api.Entities;
|
||||
@@ -45,6 +46,7 @@ public class CarpetExpression
|
||||
Threading.apply(this.expr);
|
||||
Scoreboards.apply(this.expr);
|
||||
Monitoring.apply(this.expr);
|
||||
AnnotationParser.apply(this.expr);
|
||||
CarpetServer.extensions.forEach(e -> e.scarpetApi(this));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
package carpet.script;
|
||||
|
||||
import carpet.CarpetSettings;
|
||||
import carpet.script.annotation.AnnotationParser;
|
||||
import carpet.script.api.Auxiliary;
|
||||
import carpet.script.api.BlockIterators;
|
||||
import carpet.script.api.Entities;
|
||||
import carpet.script.api.Inventories;
|
||||
import carpet.script.api.Scoreboards;
|
||||
import carpet.script.api.WorldAccess;
|
||||
import carpet.script.bundled.BundledModule;
|
||||
import carpet.CarpetServer;
|
||||
import carpet.script.bundled.FileModule;
|
||||
import carpet.script.bundled.Module;
|
||||
import carpet.script.exception.IntegrityException;
|
||||
import carpet.script.exception.InvalidCallbackException;
|
||||
import carpet.script.language.Arithmetic;
|
||||
import carpet.script.language.ControlFlow;
|
||||
import carpet.script.language.DataStructures;
|
||||
import carpet.script.language.Functions;
|
||||
import carpet.script.language.Loops;
|
||||
import carpet.script.language.Sys;
|
||||
import carpet.script.language.Threading;
|
||||
import carpet.script.value.FunctionValue;
|
||||
import carpet.script.value.Value;
|
||||
import carpet.utils.CarpetProfiler;
|
||||
@@ -481,4 +495,25 @@ public class CarpetScriptServer
|
||||
{
|
||||
modules.values().forEach(host -> host.addAppCommands(s -> {}));
|
||||
}
|
||||
|
||||
public static void parseFunctionClasses()
|
||||
{
|
||||
// Language
|
||||
AnnotationParser.parseFunctionClass(Arithmetic.class);
|
||||
AnnotationParser.parseFunctionClass(ControlFlow.class);
|
||||
AnnotationParser.parseFunctionClass(DataStructures.class);
|
||||
AnnotationParser.parseFunctionClass(Functions.class);
|
||||
AnnotationParser.parseFunctionClass(Loops.class);
|
||||
AnnotationParser.parseFunctionClass(Sys.class);
|
||||
AnnotationParser.parseFunctionClass(Threading.class);
|
||||
|
||||
// API
|
||||
AnnotationParser.parseFunctionClass(Auxiliary.class);
|
||||
AnnotationParser.parseFunctionClass(BlockIterators.class);
|
||||
AnnotationParser.parseFunctionClass(Entities.class);
|
||||
AnnotationParser.parseFunctionClass(Inventories.class);
|
||||
AnnotationParser.parseFunctionClass(Scoreboards.class);
|
||||
AnnotationParser.parseFunctionClass(carpet.script.language.Threading.class);
|
||||
AnnotationParser.parseFunctionClass(WorldAccess.class);
|
||||
}
|
||||
}
|
||||
@@ -125,15 +125,7 @@ public abstract class Fluff
|
||||
|
||||
public List<Value> unpackArgs(List<LazyValue> lzargs, Context c, Context.Type contextType)
|
||||
{
|
||||
List<Value> args = new ArrayList<>();
|
||||
for (LazyValue lv : lzargs)
|
||||
{
|
||||
Value arg = lv.evalValue(c, contextType);
|
||||
if (arg instanceof FunctionUnpackedArgumentsValue)
|
||||
args.addAll(((ListValue) arg).getItems());
|
||||
else
|
||||
args.add(arg);
|
||||
}
|
||||
List<Value> args = unpackLazy(lzargs, c, contextType);
|
||||
if (!numParamsVaries() && getNumParams() != args.size())
|
||||
throw new InternalExpressionException("Function " + getName() + " expected " + getNumParams() + " parameters, got " + args.size());
|
||||
return args;
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
package carpet.script.annotation;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.ClassUtils;
|
||||
|
||||
import carpet.CarpetExtension;
|
||||
import carpet.script.Context;
|
||||
import carpet.script.Expression;
|
||||
import carpet.script.Fluff.AbstractLazyFunction;
|
||||
import carpet.script.Fluff.TriFunction;
|
||||
import carpet.script.exception.InternalExpressionException;
|
||||
import carpet.script.LazyValue;
|
||||
import carpet.script.value.Value;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
|
||||
/**
|
||||
* <p>This class parses methods annotated with the {@link ScarpetFunction} annotation in a given {@link Class}, generating
|
||||
* fully-featured, automatically parsed and converted functions to be used in the Scarpet language.</p>
|
||||
*
|
||||
* <p>This class and the rest in this package will try to ensure that the annotated method receives the proper parameters
|
||||
* directly, without all the always-repeated code of evaluating {@link Value}s, checking and casting them to their respective
|
||||
* types, and converting them to the final needed object.</p>
|
||||
*
|
||||
* <p>To do that, functions will save a list of {@link ValueConverter}s to convert all their parameters. {@link ValueConverter}s
|
||||
* are able to convert from any compatible {@link Value} instance into the requested parameter type, as long as they are registered
|
||||
* using their respective {@code register} functions.</p>
|
||||
*
|
||||
* <p>Built-in {@link ValueConverter}s include but are not limited to converters to convert {@link List}s to actual Java lists while also
|
||||
* converting every item inside of the {@link List} to the specified generic parameter ({@code <>}), with the same applying for maps</p>
|
||||
*
|
||||
* <p>Parameters can be given the annotations (present in the {@link Locator} and {@link Param} interfaces) in order to restrict them or
|
||||
* make them more permissive to accept types, such as {@link Param.AllowSingleton} for lists.</p>
|
||||
*
|
||||
* <p>You can also declare optional parameters by using Java's {@link Optional} as the type of one of your parameters, though it must be
|
||||
* at the end of the function, just before varargs if present and/or any other {@link Optional} parameters.</p>
|
||||
*
|
||||
* <p>Output of the annotated methods will also be converted to a compatible {@link LazyValue} using the registered {@link OutputConverter}s,
|
||||
* allowing to remove the need of explicitly converting to a {@link Value} and then to a {@link LazyValue} just to end the method (though you can
|
||||
* return a {@link LazyValue} if you want.</p>
|
||||
*
|
||||
* <p>For a variable argument count, the Java varargs notation can be used in the last parameter, converting the function into a variable argument
|
||||
* function that will pass all the rest of parameters to that last varargs parameter, also converted into the specified type.</p>
|
||||
*
|
||||
* <p>To begin, use the {@link #parseFunctionClass(Class)} method in a class with methods annotated with the {@link ScarpetFunction} annotation.</p>
|
||||
*
|
||||
* @see ScarpetFunction
|
||||
* @see Locator.Block
|
||||
* @see Locator.Vec3d
|
||||
* @see Param.Strict
|
||||
* @see Param.AllowSingleton
|
||||
* @see Param.KeyValuePairs
|
||||
* @see Param.Custom
|
||||
* @see Optional
|
||||
* @see OutputConverter#registerToValue(Class, java.util.function.Function)
|
||||
* @see OutputConverter#register(Class, java.util.function.Function)
|
||||
* @see ValueCaster#register(Class, String)
|
||||
* @see SimpleTypeConverter#registerType(Class, Class, java.util.function.Function, String)
|
||||
* @see Param.Params#registerStrictConverter(Class, boolean, ValueConverter)
|
||||
* @see Param.Params#registerCustomConverterFactory(java.util.function.BiFunction)
|
||||
*/
|
||||
public final class AnnotationParser
|
||||
{
|
||||
private static final List<ParsedFunction> functionList = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* <p>Parses a given {@link Class} and registers its annotated methods, the ones with the {@link ScarpetFunction} annotation,
|
||||
* to be used in the Scarpet language.</p>
|
||||
*
|
||||
* <p><b>Only call this method once per class per lifetime of the JVM!</b> (for example, at {@link CarpetExtension#onGameStarted()})</p>
|
||||
*
|
||||
* <p>There is a set of requirements for the class and its methods:</p>
|
||||
* <ul>
|
||||
* <li>Class must be concrete. That is, no interfaces or abstract classes should be passed</li>
|
||||
* <li>Class must have the default constructor (or an equivalent) available. That is done in order to not need the {@code static} modifier in every method, making them faster to
|
||||
* code and simpler to look at.</li>
|
||||
* <li>Annotated methods must not be {@code static}. See claim above</li>
|
||||
* <li>Annotated methods must not throw checked exceptions. They can throw regular {@link RuntimeException}s (including but not limited to
|
||||
* {@link InternalExpressionException}).
|
||||
* Basically, it's fine as long as you don't add a {@code throws} declaration to your methods.</li>
|
||||
* <li>Varargs (or effectively varargs) annotated methods must explicitly declare a maximum number of parameters to ingest in the {@link ScarpetFunction}
|
||||
* annotation. They can still declare an unlimited amount by setting that maximum to {@code -1}. "Effectively varargs" means a function that has
|
||||
* a parameter using/requiring a {@link ValueConverter} that has declared {@link ValueConverter#consumesVariableArgs()}.</li>
|
||||
* <li>Annotated methods must not have a parameter with generics as the varargs parameter. This is just because it was painful for me (altrisi) and
|
||||
* didn't want to support it. Those will crash with a {@code ClassCastException}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @see ScarpetFunction
|
||||
* @param <T> The generic type of the class to parse.
|
||||
* @param clazz The class to parse
|
||||
*/
|
||||
public static <T> void parseFunctionClass(Class<T> clazz)
|
||||
{
|
||||
if (Modifier.isAbstract(clazz.getModifiers()))
|
||||
throw new IllegalArgumentException("Function class must be concrete! Class: " + clazz.getSimpleName());
|
||||
T instance;
|
||||
try
|
||||
{
|
||||
instance = clazz.getConstructor().newInstance();
|
||||
}
|
||||
catch (ReflectiveOperationException e)
|
||||
{
|
||||
throw new IllegalArgumentException(
|
||||
"Couldn't create instance of given " + clazz + ". Make sure default constructor is available", e);
|
||||
}
|
||||
|
||||
Method[] methodz = clazz.getDeclaredMethods();
|
||||
for (Method method : methodz)
|
||||
{
|
||||
if (!method.isAnnotationPresent(ScarpetFunction.class))
|
||||
continue;
|
||||
// Checks
|
||||
if (Modifier.isStatic(method.getModifiers()))
|
||||
throw new IllegalArgumentException("Annotated method '" + method.getName() + "', provided in '" + clazz + "' must not be static");
|
||||
if (method.getExceptionTypes().length != 0)
|
||||
throw new IllegalArgumentException("Annotated method '" + method.getName() +"', provided in '"+clazz+"' must not declare checked exceptions");
|
||||
|
||||
ParsedFunction function = new ParsedFunction(method, instance);
|
||||
functionList.add(function);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Adds all parsed functions to the given {@link Expression}.</p>
|
||||
* <p>This is handled automatically by Carpet</p>
|
||||
*
|
||||
* @param expr The expression to add every function to
|
||||
*/
|
||||
public static void apply(Expression expr)
|
||||
{
|
||||
for (ParsedFunction function : functionList)
|
||||
expr.addLazyFunction(function.name, function.scarpetParamCount, function);
|
||||
}
|
||||
|
||||
private static class ParsedFunction implements TriFunction<Context, Context.Type, List<LazyValue>, LazyValue>
|
||||
{
|
||||
private final String name;
|
||||
private final boolean isMethodVarArgs;
|
||||
private final int methodParamCount;
|
||||
private final List<ValueConverter<?>> valueConverters;
|
||||
private final Class<?> varArgsType;
|
||||
private final boolean primitiveVarArgs;
|
||||
private final ValueConverter<?> varArgsConverter;
|
||||
private final OutputConverter<Object> outputConverter;
|
||||
private final boolean isEffectivelyVarArgs;
|
||||
private final int minParams;
|
||||
private final int maxParams;
|
||||
private final MethodHandle handle;
|
||||
private final int scarpetParamCount;
|
||||
private final Context.Type contextType;
|
||||
|
||||
private ParsedFunction(final Method method, final Object instance)
|
||||
{
|
||||
this.name = method.getName();
|
||||
this.isMethodVarArgs = method.isVarArgs();
|
||||
this.methodParamCount = method.getParameterCount();
|
||||
|
||||
Parameter[] methodParameters = method.getParameters();
|
||||
this.valueConverters = new ObjectArrayList<>();
|
||||
for (int i = 0; i < this.methodParamCount; i++)
|
||||
{
|
||||
Parameter param = methodParameters[i];
|
||||
if (!isMethodVarArgs || i != this.methodParamCount - 1) // Varargs converter is separate
|
||||
this.valueConverters.add(ValueConverter.fromAnnotatedType(param.getAnnotatedType()));
|
||||
}
|
||||
Class<?> originalVarArgsType = isMethodVarArgs ? methodParameters[methodParamCount - 1].getType().getComponentType() : null;
|
||||
this.varArgsType = ClassUtils.primitiveToWrapper(originalVarArgsType); // Primitive array cannot be cast to Obj[]
|
||||
this.primitiveVarArgs = originalVarArgsType != null && originalVarArgsType.isPrimitive();
|
||||
this.varArgsConverter = isMethodVarArgs ? ValueConverter.fromAnnotatedType(methodParameters[methodParamCount - 1].getAnnotatedType()) : null;
|
||||
@SuppressWarnings("unchecked") // Yes. Making a T is not worth
|
||||
OutputConverter<Object> converter = OutputConverter.get((Class<Object>) method.getReturnType());
|
||||
this.outputConverter = converter;
|
||||
|
||||
this.isEffectivelyVarArgs = isMethodVarArgs ? true : valueConverters.stream().anyMatch(ValueConverter::consumesVariableArgs);
|
||||
this.minParams = valueConverters.stream().mapToInt(ValueConverter::valueConsumption).sum(); // Note: In !varargs, this is params
|
||||
int maxParams = this.minParams; // Unlimited == Integer.MAX_VALUE
|
||||
if (this.isEffectivelyVarArgs)
|
||||
{
|
||||
maxParams = method.getAnnotation(ScarpetFunction.class).maxParams();
|
||||
if (maxParams == -2)
|
||||
throw new IllegalArgumentException("No maximum number of params specified for " + name + ", use -1 for unlimited. "
|
||||
+ "Provided in " + instance.getClass());
|
||||
if (maxParams < this.minParams)
|
||||
throw new IllegalArgumentException("Provided maximum number of params for " + name + " is smaller than method's param count."
|
||||
+ "Provided in " + instance.getClass());
|
||||
if (maxParams == -1)
|
||||
maxParams = Integer.MAX_VALUE;
|
||||
}
|
||||
this.maxParams = maxParams;
|
||||
|
||||
// Why MethodHandles?
|
||||
// MethodHandles are blazing fast (in some situations they can even compile to a single invokeVirtual), but slightly complex to work with.
|
||||
// Their "polymorphic signature" makes them (by default) require the exact signature of the method and return type, in order to call the
|
||||
// functions directly, not even accepting Objects. Therefore we change them to spreaders (accept array instead), return type of Object,
|
||||
// and we also bind them to our instance. That makes them ever-so-slightly slower, since they have to cast the params, but it's not
|
||||
// noticeable, and comparing the overhead that reflection's #invoke had over this, the difference is substantial
|
||||
// (checking access at invoke vs create).
|
||||
// Note: there is also MethodHandle#invoke and #invokeWithArguments, but those run #asType at every invocation, which is quite slow, so we
|
||||
// are basically running it here.
|
||||
try
|
||||
{
|
||||
MethodHandle tempHandle = MethodHandles.publicLookup().unreflect(method).asFixedArity().asSpreader(Object[].class, this.methodParamCount);
|
||||
this.handle = tempHandle.asType(tempHandle.type().changeReturnType(Object.class)).bindTo(instance);
|
||||
} catch (IllegalAccessException e)
|
||||
{
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
|
||||
this.scarpetParamCount = this.isEffectivelyVarArgs ? -1 : this.minParams;
|
||||
this.contextType = method.getAnnotation(ScarpetFunction.class).contextType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LazyValue apply(Context context, Context.Type t, List<LazyValue> lazyValues)
|
||||
{
|
||||
List<Value> lv = AbstractLazyFunction.unpackLazy(lazyValues, context, contextType);
|
||||
if (isEffectivelyVarArgs)
|
||||
{
|
||||
if (lv.size() < minParams)
|
||||
throw new InternalExpressionException("Function '" + name + "' expected at least " + minParams + " arguments, got " + lv.size() + ". "
|
||||
+ getUsage());
|
||||
if (lv.size() > maxParams)
|
||||
throw new InternalExpressionException("Function '" + name + " expected up to " + maxParams + " arguments, got " + lv.size() + ". "
|
||||
+ getUsage());
|
||||
} else
|
||||
{
|
||||
if (lv.size() != minParams) // min == max if not varargs. We do this cause we don't use function's arg checking to be able to return lazy
|
||||
throw new InternalExpressionException("Function '" + name + "' expected " + minParams + " arguments, got " + lv.size() +". " + getUsage());
|
||||
}
|
||||
Object[] params = getMethodParams(lv, context, t);
|
||||
try
|
||||
{
|
||||
return outputConverter.convert(handle.invokeExact(params));
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
if (e instanceof RuntimeException)
|
||||
throw (RuntimeException) e;
|
||||
throw (Error) e; // Stack overflow or something. Methods are guaranteed not to throw checked exceptions
|
||||
}
|
||||
}
|
||||
|
||||
// Hot code: Must be optimized
|
||||
private Object[] getMethodParams(final List<Value> lv, final Context context, Context.Type theLazyT)
|
||||
{
|
||||
Object[] params = new Object[methodParamCount];
|
||||
ListIterator<Value> lvIterator = lv.listIterator();
|
||||
|
||||
int regularArgs = isMethodVarArgs ? methodParamCount - 1 : methodParamCount;
|
||||
for (int i = 0; i < regularArgs; i++)
|
||||
{
|
||||
params[i] = valueConverters.get(i).checkAndConvert(lvIterator, context, theLazyT);
|
||||
if (params[i] == null)
|
||||
throw new InternalExpressionException("Incorrect argument passsed to '" + name + "' function.\n" + getUsage());
|
||||
}
|
||||
if (isMethodVarArgs)
|
||||
{
|
||||
int remaining = lv.size() - lvIterator.nextIndex();
|
||||
Object[] varArgs;
|
||||
if (varArgsConverter.consumesVariableArgs())
|
||||
{
|
||||
List<Object> varArgsList = new ArrayList<>();
|
||||
while (lvIterator.hasNext())
|
||||
{
|
||||
Object obj = varArgsConverter.checkAndConvert(lvIterator, context, theLazyT);
|
||||
if (obj == null)
|
||||
throw new InternalExpressionException("Incorrect argument passsed to '" + name + "' function.\n" + getUsage());
|
||||
varArgsList.add(obj);
|
||||
}
|
||||
varArgs = varArgsList.toArray();
|
||||
} else
|
||||
{
|
||||
varArgs = (Object[]) Array.newInstance(varArgsType, remaining / varArgsConverter.valueConsumption());
|
||||
for (int i = 0; lvIterator.hasNext(); i++)
|
||||
{
|
||||
varArgs[i] = varArgsConverter.checkAndConvert(lvIterator, context, theLazyT);
|
||||
if (varArgs[i] == null)
|
||||
throw new InternalExpressionException("Incorrect argument passsed to '" + name + "' function.\n" + getUsage());
|
||||
}
|
||||
}
|
||||
params[methodParamCount - 1] = primitiveVarArgs ? ArrayUtils.toPrimitive(varArgs) : varArgs; // Copies the array
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
private String getUsage()
|
||||
{
|
||||
// Possibility: More descriptive messages using param.getName()? Would need changing gradle setup to keep those
|
||||
StringBuilder builder = new StringBuilder("Usage: '");
|
||||
builder.append(name);
|
||||
builder.append('(');
|
||||
builder.append(valueConverters.stream().map(ValueConverter::getTypeName).filter(Objects::nonNull).collect(Collectors.joining(", ")));
|
||||
if (varArgsConverter != null)
|
||||
{
|
||||
builder.append(", ");
|
||||
builder.append(varArgsConverter.getTypeName());
|
||||
builder.append("s...)");
|
||||
} else
|
||||
{
|
||||
builder.append(')');
|
||||
}
|
||||
builder.append("'");
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private AnnotationParser() {}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package carpet.script.annotation;
|
||||
|
||||
import carpet.logging.HUDController;
|
||||
import carpet.script.annotation.Param.AllowSingleton;
|
||||
import carpet.script.exception.InternalExpressionException;
|
||||
import net.minecraft.network.packet.s2c.play.TitleS2CPacket;
|
||||
import net.minecraft.server.network.ServerPlayerEntity;
|
||||
import net.minecraft.text.BaseText;
|
||||
import net.minecraft.text.Text;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static net.minecraft.network.packet.s2c.play.TitleS2CPacket.Action;
|
||||
|
||||
public class FunctionsTestClass {
|
||||
@ScarpetFunction(maxParams = 6)
|
||||
public Integer display_title2(@AllowSingleton List<ServerPlayerEntity> targets, String actionString, Optional<Text> content, Integer... times) {
|
||||
TitleS2CPacket.Action action;
|
||||
switch (actionString.toLowerCase(Locale.ROOT))
|
||||
{
|
||||
case "title":
|
||||
action = Action.TITLE;
|
||||
break;
|
||||
case "subtitle":
|
||||
action = Action.SUBTITLE;
|
||||
break;
|
||||
case "actionbar":
|
||||
action = Action.ACTIONBAR;
|
||||
break;
|
||||
case "clear":
|
||||
action = Action.CLEAR;
|
||||
break;
|
||||
case "player_list_header":
|
||||
case "player_list_footer":
|
||||
action = null;
|
||||
break;
|
||||
default:
|
||||
throw new InternalExpressionException("'display_title' requires 'title', 'subtitle', 'actionbar', 'player_list_header', 'player_list_footer' or 'clear' as second argument");
|
||||
}
|
||||
if (action == null) // Player list
|
||||
{
|
||||
Map<ServerPlayerEntity, BaseText> map = actionString.equals("player_list_header") ? HUDController.scarpet_headers : HUDController.scarpet_footers;
|
||||
if (!content.isPresent()) // null
|
||||
for (ServerPlayerEntity p : targets)
|
||||
map.remove(p);
|
||||
else
|
||||
for (ServerPlayerEntity p : targets)
|
||||
map.put(p, (BaseText) content.get());
|
||||
return targets.size();
|
||||
}
|
||||
targets.forEach(p -> {
|
||||
if (times.length == 3) p.networkHandler.sendPacket(new TitleS2CPacket(Action.TIMES, null, times[0], times[1], times[2]));
|
||||
p.networkHandler.sendPacket(new TitleS2CPacket(action, content.orElse(null)));
|
||||
});
|
||||
return targets.size();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package carpet.script.annotation;
|
||||
|
||||
import java.lang.reflect.AnnotatedParameterizedType;
|
||||
import java.lang.reflect.AnnotatedType;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import carpet.script.value.ListValue;
|
||||
import carpet.script.value.Value;
|
||||
|
||||
/**
|
||||
* <p>Converts a given {@link ListValue} into a {@link List} of values converted to {@code <T>}.</p>
|
||||
*
|
||||
* <p>If the {@link Param.AllowSingleton} annotation is specified, allows creating a singleton from
|
||||
* a loose element compatible with the type conversion.</p>
|
||||
*
|
||||
* <p>Lists provided by this converter are <b>not</b> linked to the initial list, and therefore will not
|
||||
* reflect changes in either of them</p>
|
||||
*
|
||||
* @param <T> The type of the element that will be inside the list
|
||||
*/
|
||||
final class ListConverter<T> implements ValueConverter<List<T>>
|
||||
{
|
||||
private final ValueConverter<T> itemConverter;
|
||||
private final boolean allowSingletonCreation;
|
||||
|
||||
@Override
|
||||
public String getTypeName()
|
||||
{
|
||||
return (allowSingletonCreation ? itemConverter.getTypeName() + " or " : "") + "list of " + itemConverter.getTypeName() + "s";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<T> convert(Value value)
|
||||
{
|
||||
return value instanceof ListValue ? convertListValue((ListValue) value) : allowSingletonCreation ? convertSingleton(value) : null;
|
||||
}
|
||||
|
||||
private List<T> convertListValue(ListValue values)
|
||||
{
|
||||
List<T> list = new ArrayList<>(values.getItems().size());
|
||||
for (Value value : values)
|
||||
{
|
||||
T converted = itemConverter.convert(value);
|
||||
if (converted == null)
|
||||
return null;
|
||||
list.add(converted);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private List<T> convertSingleton(Value val)
|
||||
{
|
||||
T converted = itemConverter.convert(val);
|
||||
if (converted == null)
|
||||
return null;
|
||||
return Collections.singletonList(converted);
|
||||
|
||||
}
|
||||
|
||||
private ListConverter(AnnotatedType itemType, boolean allowSingletonCreation)
|
||||
{
|
||||
itemConverter = ValueConverter.fromAnnotatedType(itemType);
|
||||
this.allowSingletonCreation = allowSingletonCreation;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Returns a new {@link ListConverter} to convert to the given {@link AnnotatedType}.</p>
|
||||
*
|
||||
* <p>The returned {@link ValueConverter} will convert the objects inside the list to the
|
||||
* generics specified in the {@link AnnotatedType}, and the {@link ValueConverter} will
|
||||
* be set to accept non-list (but correct) items and make a singleton out of them
|
||||
* if the {@link Param.AllowSingleton} annotation has been specified.</p>
|
||||
*
|
||||
* @apiNote This method expects the {@link AnnotatedType} to already be of {@link List} type, and, while it will
|
||||
* technically accept a non-{@link List} {@link AnnotatedType}, it will fail with an {@link ArrayIndexOutOfBoundsException}
|
||||
* if it doesn't has at least one generic parameter.
|
||||
* @param annotatedType The type to get generics information from
|
||||
* @return A new {@link ListConverter} for the data specified in the {@link AnnotatedType}
|
||||
*/
|
||||
static ListConverter<?> fromAnnotatedType(AnnotatedType annotatedType)
|
||||
{
|
||||
AnnotatedParameterizedType paramType = (AnnotatedParameterizedType) annotatedType;
|
||||
AnnotatedType itemType = paramType.getAnnotatedActualTypeArguments()[0];
|
||||
boolean allowSingletonCreation = annotatedType.isAnnotationPresent(Param.AllowSingleton.class);
|
||||
return new ListConverter<>(itemType, allowSingletonCreation);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package carpet.script.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.reflect.AnnotatedType;
|
||||
import java.util.Iterator;
|
||||
|
||||
import org.apache.commons.lang3.NotImplementedException;
|
||||
|
||||
import carpet.script.Context;
|
||||
import carpet.script.argument.Argument;
|
||||
import carpet.script.argument.BlockArgument;
|
||||
import carpet.script.argument.FunctionArgument;
|
||||
import carpet.script.argument.Vector3Argument;
|
||||
import carpet.script.bundled.Module;
|
||||
import carpet.script.value.BlockValue;
|
||||
import carpet.script.value.FunctionValue;
|
||||
import carpet.script.value.Value;
|
||||
|
||||
import static java.lang.annotation.ElementType.PARAMETER;
|
||||
import static java.lang.annotation.ElementType.TYPE_USE;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
/**
|
||||
* <p>Class that holds annotation for {@link Argument} locators to be used in Scarpet functions.</p>
|
||||
*
|
||||
* <p>Note: Nothing in this class has been implemented at this point in time, since it requires Carpet changes. There is an implementation for
|
||||
* Locator.Block working, but not implemented.</p>
|
||||
*/
|
||||
public interface Locator
|
||||
{
|
||||
/**
|
||||
* <p>Represents that the annotated argument must be gotten by passing the arguments in there into a {@link BlockArgument} locator.</p>
|
||||
*
|
||||
* <p>Must be used in either {@link BlockValue} or {@link BlockArgument} parameters</p>
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
@Target({ PARAMETER, TYPE_USE })
|
||||
@Deprecated // Not implemented, although implementation available
|
||||
public @interface Block
|
||||
{
|
||||
/**
|
||||
* <p>Whether or not should the locator accept a single {@link String} as the parameter and let parsing to {@link BlockValue}.</p>
|
||||
*/
|
||||
boolean acceptString() default false;
|
||||
|
||||
/**
|
||||
* <p>Whether or not should the {@link BlockValue} argument be optional.</p> <p>Requires the annotation to be present in a
|
||||
* {@link BlockArgument} type, since it may return that the {@link BlockValue} is {@code null}, which would be considered as an incorrect
|
||||
* type.</p>
|
||||
*/
|
||||
boolean optional() default false;
|
||||
|
||||
/**
|
||||
* <p>Whether or not should the {@link BlockArgument} locator accept any string as the argument.</p> <p>Requires the annotation to be present
|
||||
* in a {@link BlockArgument} type, since it may just return a {@link String}</p>
|
||||
*/
|
||||
boolean anyString() default false;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Represents that the annotated argument must be gotten by passing the arguments in there into a {@link Vector3Argument} locator.</p>
|
||||
*
|
||||
* <p>Must be used in either a {@link Vector3Argument} or a {@link Vec3d} parameter, though the latest may not get all the data.</p>
|
||||
*
|
||||
* <p>Note: This locator has not been implemented yet (TODO)</p>
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
@Target({ PARAMETER, TYPE_USE })
|
||||
@Deprecated // Not implemented
|
||||
public @interface Vec3d
|
||||
{
|
||||
/**
|
||||
* <p>Whether or not should the {@link Vector3Argument} locator accept an optional direction aside from the
|
||||
* {@link net.minecraft.util.math.Vec3d}</p> <p>This parameter can only be used in a {@link Vector3Argument} type, since else there is no way
|
||||
* to get the direction too.</p>
|
||||
*/
|
||||
boolean optionalDirection() default false;
|
||||
|
||||
/**
|
||||
* <p>Whether or not should the {@link Vector3Argument} locator accept an entity aside to get the {@link Vec3d} from and return that entity
|
||||
* too</p> <p>Note that you will only be able to get that entity if the annotation is present in a {@link Vector3Argument}</p>
|
||||
*/
|
||||
boolean optionalEntity() default false;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Represents that the annotated argument must be gotten by passing the arguments in this annotation into a {@link FunctionArgument}
|
||||
* locator</p>
|
||||
*
|
||||
* <p>Can be used in both {@link FunctionArgument} and {@link FunctionValue} types, but the last won't have access to arguments provided to the
|
||||
* function, even though they will still be consumed from the arguments the function was called with.</p>
|
||||
*
|
||||
* <p><b>This will consume any remaining parameters passed to the function, therefore any other parameters after this will throw.</b></p>
|
||||
*
|
||||
* <p>Note: This locator has not been implemented yet (TODO)</p>
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
@Target({ PARAMETER, TYPE_USE })
|
||||
@Deprecated // Not implemented
|
||||
public @interface Function
|
||||
{
|
||||
/**
|
||||
* <p>Whether this Locator should allow no function to be passed.</p> <p>This is not compatible with {@link FunctionValue} type, since a
|
||||
* converter returning {@code null} will throw as if the passed argument was incorrect. You can still use it when targeting
|
||||
* {@link FunctionArgument}</p>
|
||||
*/
|
||||
boolean allowNone() default false;
|
||||
|
||||
/**
|
||||
* <p>Whether the locator should check that the number of arguments passed along with the function matches the number of arguments that the
|
||||
* located function requires. Note that FunctionLocators consume all remaining arguments even if this is set to {@code false}.</p>
|
||||
*/
|
||||
boolean checkArgs();
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Class that holds locators and methods to get them</p>
|
||||
*
|
||||
* <p>Not part of the public API, just that interfaces must have all members public</p>
|
||||
*/
|
||||
static final class Locators
|
||||
{
|
||||
private Locators() {}
|
||||
|
||||
static <R> ValueConverter<R> fromAnnotatedType(AnnotatedType annoType, Class<R> type)
|
||||
{
|
||||
if (annoType.isAnnotationPresent(Block.class))
|
||||
return new BlockLocator<R>(annoType.getAnnotation(Block.class), type);
|
||||
if (annoType.isAnnotationPresent(Function.class))
|
||||
return new FunctionLocator<R>(annoType.getAnnotation(Function.class), type);
|
||||
if (annoType.isAnnotationPresent(Vec3d.class))
|
||||
return new Vec3dLocator<R>(annoType.getAnnotation(Vec3d.class), type);
|
||||
throw new IllegalStateException("Locator#fromAnnotatedType got called with an incompatible AnnotatedType");
|
||||
}
|
||||
|
||||
private static class BlockLocator<R> extends AbstractLocator<R>
|
||||
{
|
||||
private final boolean returnBlockValue;
|
||||
private final boolean acceptString;
|
||||
private final boolean anyString;
|
||||
private final boolean optional;
|
||||
|
||||
public BlockLocator(Block annotation, Class<R> type)
|
||||
{
|
||||
this.acceptString = annotation.acceptString();
|
||||
this.anyString = annotation.anyString();
|
||||
this.optional = annotation.optional();
|
||||
this.returnBlockValue = type == BlockValue.class;
|
||||
if (returnBlockValue && (anyString || optional))
|
||||
throw new IllegalArgumentException("Can only use anyString or optional parameters of Locator.Block if targeting a BlockArgument");
|
||||
if (!returnBlockValue && type != BlockArgument.class)
|
||||
throw new IllegalArgumentException("Locator.Block can only be used against BlockArgument or BlockValue types!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName()
|
||||
{
|
||||
return "block";
|
||||
}
|
||||
|
||||
@Override
|
||||
public R checkAndConvert(Iterator<Value> valueIterator, Context context, Context.Type theLazyT)
|
||||
{
|
||||
BlockArgument locator = null; // BlockArgument.findIn((CarpetContext)context, valueIterator (requires changing to Value), 0,
|
||||
// acceptString, optional, anyString);
|
||||
// return (R) (returnBlockValue ? locator.block : locator);
|
||||
throw new NotImplementedException("Locator.Block still requires adapting BlockArgument to accept iterators (which is actually simple)");
|
||||
}
|
||||
}
|
||||
|
||||
private static class Vec3dLocator<R> extends AbstractLocator<R>
|
||||
{
|
||||
private final boolean optionalDirection;
|
||||
private final boolean optionalEntity;
|
||||
private final boolean returnVec3d;
|
||||
|
||||
public Vec3dLocator(Vec3d annotation, Class<R> type)
|
||||
{
|
||||
this.optionalDirection = annotation.optionalDirection();
|
||||
this.optionalEntity = annotation.optionalEntity();
|
||||
this.returnVec3d = type == net.minecraft.util.math.Vec3d.class; // Because of the locator
|
||||
if (returnVec3d && optionalDirection)
|
||||
throw new IllegalArgumentException("optionalDirection Locator.Vec3d cannot be used for Vec3d type, use Vector3Argument instead");
|
||||
if (!returnVec3d && type != Vector3Argument.class)
|
||||
throw new IllegalArgumentException("Locator.Vec3d can only be used in Vector3Argument or Vec3d types");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName()
|
||||
{
|
||||
return "position";
|
||||
}
|
||||
|
||||
@Override
|
||||
public R checkAndConvert(Iterator<Value> valueIterator, Context context, Context.Type theLazyT)
|
||||
{
|
||||
Vector3Argument locator = null;
|
||||
// TODO Make the locator
|
||||
// return (R) (returnVec3d ? locator.vec : locator);
|
||||
throw new NotImplementedException("Locator.Vec3d still require adapting Vector3Argument to accept iterators!");
|
||||
}
|
||||
}
|
||||
|
||||
private static class FunctionLocator<R> extends AbstractLocator<R>
|
||||
{
|
||||
private final boolean returnFunctionValue;
|
||||
private final boolean allowNone;
|
||||
private final boolean checkArgs;
|
||||
|
||||
FunctionLocator(Function annotation, Class<R> type)
|
||||
{
|
||||
this.returnFunctionValue = type == FunctionValue.class;
|
||||
if (!returnFunctionValue && type != FunctionArgument.class)
|
||||
throw new IllegalArgumentException(
|
||||
"Params annotated with Locator.Function must be of either FunctionArgument or FunctionValue type");
|
||||
this.allowNone = annotation.allowNone();
|
||||
this.checkArgs = annotation.checkArgs();
|
||||
if (returnFunctionValue && allowNone)
|
||||
throw new IllegalArgumentException("Cannot use allowNone of Locator.Function in FunctionValue types, use FunctionArgument");
|
||||
}
|
||||
|
||||
@Override
|
||||
public R checkAndConvert(Iterator<Value> valueIterator, Context context, Context.Type theLazyT)
|
||||
{
|
||||
Module module = context.host.main;
|
||||
FunctionArgument locator = null;
|
||||
// TODO Make the locator
|
||||
// return (R) (returnFunctionValue ? locator.function : locator);
|
||||
throw new NotImplementedException("Locator.FunctionValue still requires adapting the annotation system to somehow get lv size"
|
||||
+ "or FunctionArgument to not depend on it!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName()
|
||||
{
|
||||
return "function";
|
||||
}
|
||||
}
|
||||
|
||||
private static abstract class AbstractLocator<R> implements ValueConverter<R>, Locator
|
||||
{
|
||||
@Override
|
||||
public R convert(Value value)
|
||||
{
|
||||
throw new UnsupportedOperationException("Cannot call a locator in a parameter that doesn't contain a context!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean consumesVariableArgs()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int valueConsumption()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract R checkAndConvert(Iterator<Value> valueIterator, Context context, Context.Type theLazyT);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package carpet.script.annotation;
|
||||
|
||||
import java.lang.reflect.AnnotatedParameterizedType;
|
||||
import java.lang.reflect.AnnotatedType;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import carpet.script.Context;
|
||||
import carpet.script.value.ListValue;
|
||||
import carpet.script.value.MapValue;
|
||||
import carpet.script.value.Value;
|
||||
|
||||
/**
|
||||
* <p>Converts a {@link MapValue} to a {@link Map}, converting all of its contents to their respective types.</p>
|
||||
*
|
||||
* <p>If the {@link Param.KeyValuePairs} annotation is specified, uses its subclass at {@link PairConverter} and allows passing either a map, a list
|
||||
* like [key, value, key2, value2,...] or the same as the list inlined in the function's body, unless {@link Param.KeyValuePairs#allowMultiparam()} is
|
||||
* set to {@code false}.</p>
|
||||
*
|
||||
* <p>Maps provided by this converter are <b>not</b> linked to the initial map, and therefore will not reflect changes in either of them.</p>
|
||||
*
|
||||
* @param <K> The type of the map's keys
|
||||
* @param <V> The type of the map's values
|
||||
*/
|
||||
class MapConverter<K, V> implements ValueConverter<Map<K, V>>
|
||||
{
|
||||
protected final ValueConverter<K> keyConverter;
|
||||
protected final ValueConverter<V> valueConverter;
|
||||
|
||||
@Override
|
||||
public String getTypeName()
|
||||
{
|
||||
return "map with " + keyConverter.getTypeName() + "s as the key and " + valueConverter.getTypeName() + "s as the value";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<K, V> convert(Value value)
|
||||
{
|
||||
Map<K, V> result = new HashMap<>();
|
||||
if (value instanceof MapValue)
|
||||
{
|
||||
for (Entry<Value, Value> entry : ((MapValue) value).getMap().entrySet())
|
||||
{
|
||||
K key = keyConverter.convert(entry.getKey());
|
||||
V val = valueConverter.convert(entry.getValue());
|
||||
if (key == null || val == null)
|
||||
return null;
|
||||
result.put(key, val);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private MapConverter(AnnotatedType keyType, AnnotatedType valueType)
|
||||
{
|
||||
keyConverter = ValueConverter.fromAnnotatedType(keyType);
|
||||
valueConverter = ValueConverter.fromAnnotatedType(valueType);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Returns a new {@link MapConverter} to convert to the given {@link AnnotatedType}.</p>
|
||||
*
|
||||
* <p>The returned {@link ValueConverter} will convert the objects inside the map (keys and values) to the generics specified in the
|
||||
* {@link AnnotatedType}.</p>
|
||||
*
|
||||
* <p>If the provided {@link AnnotatedType} is annotated with {@link Param.KeyValuePairs}, it will return an implementation of
|
||||
* {@link MapConverter} that provides extra checks in order to allow inputs of either {@code {k->v,k2->v2,...}}, {@code [k,v,k2,v2,...]} or
|
||||
* {@code fn(...,k,v,k2,v2,...)}</p>
|
||||
*
|
||||
* @apiNote This method expects the {@link AnnotatedType} to already be of {@link Map} type, and, while it will technically accept a
|
||||
* non-{@link Map} {@link AnnotatedType}, it will fail if it doesn't has at least two generic parameters with an
|
||||
* {@link ArrayIndexOutOfBoundsException}.
|
||||
* @param annotatedType The type to get generics information from
|
||||
* @return A new {@link MapConverter} for the data specified in the {@link AnnotatedType}
|
||||
*/
|
||||
static MapConverter<?, ?> fromAnnotatedType(AnnotatedType annotatedType)
|
||||
{
|
||||
AnnotatedType[] annotatedGenerics = ((AnnotatedParameterizedType) annotatedType).getAnnotatedActualTypeArguments();
|
||||
return annotatedType.isAnnotationPresent(Param.KeyValuePairs.class)
|
||||
? new PairConverter<>(annotatedGenerics[0], annotatedGenerics[1], annotatedType.getAnnotation(Param.KeyValuePairs.class))
|
||||
: new MapConverter<>(annotatedGenerics[0], annotatedGenerics[1]);
|
||||
}
|
||||
|
||||
private static final class PairConverter<K, V> extends MapConverter<K, V>
|
||||
{
|
||||
private final boolean acceptMultiParam;
|
||||
|
||||
private PairConverter(AnnotatedType keyType, AnnotatedType valueType, Param.KeyValuePairs config)
|
||||
{
|
||||
super(keyType, valueType);
|
||||
acceptMultiParam = config.allowMultiparam();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean consumesVariableArgs()
|
||||
{
|
||||
return acceptMultiParam;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<K, V> convert(Value value) {
|
||||
return value instanceof MapValue ? super.convert(value)
|
||||
: value instanceof ListValue ? convertList(((ListValue)value).getItems())
|
||||
: null; // Multiparam mode can only be used in evalAndConvert
|
||||
}
|
||||
|
||||
|
||||
private Map<K, V> convertList(List<Value> valueList)
|
||||
{
|
||||
if (valueList.size() % 2 == 1)
|
||||
return null;
|
||||
Map<K, V> map = new HashMap<>();
|
||||
Iterator<Value> val = valueList.iterator();
|
||||
while (val.hasNext())
|
||||
{
|
||||
K key = keyConverter.convert(val.next());
|
||||
V value = valueConverter.convert(val.next());
|
||||
if (key == null || value == null)
|
||||
return null;
|
||||
map.put(key, value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<K, V> checkAndConvert(Iterator<Value> valueIterator, Context context, Context.Type theLazyT)
|
||||
{
|
||||
if (!valueIterator.hasNext())
|
||||
return null;
|
||||
Value val = valueIterator.next();
|
||||
if (!acceptMultiParam || val instanceof MapValue || (val instanceof ListValue && !(keyConverter instanceof ListConverter)))
|
||||
return convert(val); // @KeyValuePairs Map<List<Something>, Boolean> will not support list consumption
|
||||
|
||||
Map<K, V> map = new HashMap<>();
|
||||
K key = keyConverter.convert(val); //First pair is manual since we got it to check for a different conversion mode
|
||||
V value = valueConverter.checkAndConvert(valueIterator, context, theLazyT);
|
||||
if (key == null || value == null)
|
||||
return null;
|
||||
map.put(key, value);
|
||||
while (valueIterator.hasNext())
|
||||
{
|
||||
key = keyConverter.checkAndConvert(valueIterator, context, theLazyT);
|
||||
value = valueConverter.checkAndConvert(valueIterator, context, theLazyT);
|
||||
if (key == null || value == null)
|
||||
return null;
|
||||
map.put(key, value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName()
|
||||
{
|
||||
return "either a map of key-value pairs" + (acceptMultiParam ? "," : " or") + " a list in the form of [key, value, key2, value2,...]"
|
||||
+ (acceptMultiParam ? " or those key-value pairs in the function" : "") + " (keys being " + keyConverter.getTypeName()
|
||||
+ "s and values being " + valueConverter.getTypeName() + "s)";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package carpet.script.annotation;
|
||||
|
||||
import java.lang.reflect.AnnotatedParameterizedType;
|
||||
import java.lang.reflect.AnnotatedType;
|
||||
import java.util.Iterator;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Optional;
|
||||
|
||||
import carpet.script.Context;
|
||||
import carpet.script.value.Value;
|
||||
|
||||
/**
|
||||
* <p>{@link ValueConverter} that accepts a parameter to not be present on function call.</p>
|
||||
*
|
||||
* <p>Note that it will not work properly if it's not at the end of the function, since values are consumed in an ordered way, therefore it will throw
|
||||
* if the value is not present since it will try to evaluate the next parameter into {@code <R>} and horribly fail, or it will draw values from the
|
||||
* {@link ValueConverter}s after it, horribly failing too..</p>
|
||||
*
|
||||
* <p>Why {@link Optional}?</p>
|
||||
*
|
||||
* <p>It allows passing three states:</p>
|
||||
* <ul>
|
||||
* <li>A wrapped object, if the value was present and correct</li>
|
||||
* <li>An incorrect value ({@code null}), if the value was present but incorrect, as per the contract of {@link ValueConverter} and the way
|
||||
* {@link AnnotationParser} will consider {@link null} values</li>
|
||||
* <li>An empty {@link Optional}, if the value was not present (or {@code null})</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param <R> The type of the internal {@link ValueConverter}, basically the generic type of the {@link Optional}
|
||||
*/
|
||||
final class OptionalConverter<R> implements ValueConverter<Optional<R>>
|
||||
{
|
||||
private final ValueConverter<R> typeConverter;
|
||||
|
||||
@Override
|
||||
public String getTypeName()
|
||||
{
|
||||
return "optional " + typeConverter.getTypeName();
|
||||
}
|
||||
|
||||
private OptionalConverter(AnnotatedType type)
|
||||
{
|
||||
typeConverter = ValueConverter.fromAnnotatedType(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @implNote Unlike most other converters, {@link OptionalConverter} will not call this method from
|
||||
* {@link #checkAndConvert(Iterator, Context, Context.Type)} and is only used as a fallback in types that don't support it.
|
||||
*/
|
||||
@Override
|
||||
public Optional<R> convert(Value value)
|
||||
{
|
||||
if (value.isNull())
|
||||
return Optional.empty();
|
||||
R converted = typeConverter.convert(value);
|
||||
if (converted == null)
|
||||
return null;
|
||||
return Optional.of(converted);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<R> checkAndConvert(Iterator<Value> valueIterator, Context context, Context.Type theLazyT)
|
||||
{
|
||||
if (!valueIterator.hasNext() || valueIterator.next().isNull())
|
||||
return Optional.empty();
|
||||
((ListIterator<Value>) valueIterator).previous();
|
||||
R converted = typeConverter.checkAndConvert(valueIterator, context, theLazyT);
|
||||
if (converted == null)
|
||||
return null;
|
||||
return Optional.of(converted);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean consumesVariableArgs()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int valueConsumption()
|
||||
{
|
||||
return 0; // Optional parameters therefore require a minimum of 0
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Returns a new {@link OptionalConverter} to convert to the type in the given {@link AnnotatedType} if there is at least one element left in
|
||||
* the iterator, with any parameters, annotations or anything that was present in there.</p>
|
||||
*
|
||||
* <p>The given {@link ValueConverter} will be called after ensuring that there is at least one element left in the iterator to the type specified
|
||||
* in the generics of this {@link AnnotatedType}, or return an empty {@link Optional} if there is nothing left or the first value meets
|
||||
* {@link Value#isNull()}.
|
||||
*
|
||||
* @apiNote This method expects the {@link AnnotatedType} to already be of {@link Optional} type, and, while it will technically accept a
|
||||
* non-{@link Optional} {@link AnnotatedType}, it will fail with an {@link ArrayIndexOutOfBoundsException} if it doesn't has at least one
|
||||
* generic parameter.
|
||||
* @param annotatedType The type to get generics information from
|
||||
* @return A new {@link OptionalConverter} for the data specified in the {@link AnnotatedType}
|
||||
*/
|
||||
static OptionalConverter<?> fromAnnotatedType(AnnotatedType annotatedType)
|
||||
{
|
||||
AnnotatedParameterizedType paramType = (AnnotatedParameterizedType) annotatedType;
|
||||
AnnotatedType wrappedType = paramType.getAnnotatedActualTypeArguments()[0];
|
||||
return new OptionalConverter<>(wrappedType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package carpet.script.annotation;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.commons.lang3.ClassUtils;
|
||||
|
||||
import carpet.script.LazyValue;
|
||||
import carpet.script.value.EntityValue;
|
||||
import carpet.script.value.FormattedTextValue;
|
||||
import carpet.script.value.NBTSerializableValue;
|
||||
import carpet.script.value.NumericValue;
|
||||
import carpet.script.value.StringValue;
|
||||
import carpet.script.value.Value;
|
||||
import carpet.script.value.ValueConversions;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.text.Text;
|
||||
import net.minecraft.util.Identifier;
|
||||
import net.minecraft.util.dynamic.GlobalPos;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.math.Vec3d;
|
||||
|
||||
/**
|
||||
* <p>A converter from a given {@link Object} of type T into a {@link LazyValue}, used in order to convert the outputs of methods into usable Scarpet
|
||||
* values.</p>
|
||||
*
|
||||
* @see #register(Class, Function)
|
||||
* @see #registerToValue(Class, Function)
|
||||
* @param <T> The type to convert from into a {@link LazyValue}
|
||||
*/
|
||||
public final class OutputConverter<T>
|
||||
{
|
||||
private static final Map<Class<?>, OutputConverter<?>> byResult = new HashMap<>();
|
||||
private static final OutputConverter<Value> VALUE = new OutputConverter<>(v -> (c, t) -> v);
|
||||
static
|
||||
{
|
||||
register(LazyValue.class, Function.identity()); // Primitives are handled. Things are boxed in the process anyway, therefore
|
||||
register(Boolean.class, v -> (v ? LazyValue.TRUE : LazyValue.FALSE)); // would recommend boxed outputs, so you can use null
|
||||
register(Void.TYPE, v -> LazyValue.NULL);
|
||||
registerToValue(Integer.class, NumericValue::of);
|
||||
registerToValue(Double.class, NumericValue::of);
|
||||
registerToValue(Float.class, NumericValue::of);
|
||||
registerToValue(Long.class, NumericValue::of);
|
||||
registerToValue(String.class, StringValue::new);
|
||||
registerToValue(Entity.class, EntityValue::new);
|
||||
registerToValue(Text.class, FormattedTextValue::new);
|
||||
registerToValue(Tag.class, NBTSerializableValue::new);
|
||||
registerToValue(BlockPos.class, ValueConversions::of);
|
||||
registerToValue(Vec3d.class, ValueConversions::of);
|
||||
registerToValue(ItemStack.class, ValueConversions::of);
|
||||
registerToValue(Identifier.class, ValueConversions::of);
|
||||
registerToValue(GlobalPos.class, ValueConversions::of);
|
||||
}
|
||||
|
||||
private final Function<T, LazyValue> converter;
|
||||
|
||||
private OutputConverter(Function<T, LazyValue> converter)
|
||||
{
|
||||
this.converter = converter;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Returns the {@link OutputConverter} for the specified returnType.</p>
|
||||
*
|
||||
* @param <T> The type of the {@link OutputConverter} you are looking for
|
||||
* @param returnType The class that the returned {@link OutputConverter} converts from
|
||||
* @return The {@link OutputConverter} for the specified returnType
|
||||
*/
|
||||
@SuppressWarnings("unchecked") // OutputConverters are stored with their class, for sure since the map is private (&& class has same generic as
|
||||
// converter)
|
||||
public static <T> OutputConverter<T> get(Class<T> returnType)
|
||||
{
|
||||
if (Value.class.isAssignableFrom(returnType))
|
||||
return (OutputConverter<T>) VALUE;
|
||||
returnType = (Class<T>) ClassUtils.primitiveToWrapper(returnType); // wrapper holds same generic as primitive: wrapped
|
||||
return (OutputConverter<T>) Objects.requireNonNull(byResult.get(returnType),
|
||||
"Unregistered output type: " + returnType + ". Register in OutputConverter");
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Converts the given input object into a {@link LazyValue}, to be used in return values of Scarpet functions</p>
|
||||
*
|
||||
* <p>Returns {@link LazyValue#NULL} if passed a {@code null} input</p>
|
||||
*
|
||||
* @param input The value to convert
|
||||
* @return The converted value
|
||||
*/
|
||||
public LazyValue convert(T input)
|
||||
{
|
||||
return input == null ? LazyValue.NULL : converter.apply(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Registers a new type to be able to be used as the return value of methods, converting from inputType to a {@link LazyValue}.</p>
|
||||
*
|
||||
* @see #registerToValue(Class, Function)
|
||||
* @param <T> The type of the input type
|
||||
* @param inputType The class of T
|
||||
* @param converter The function that converts the an instance of T to a {@link LazyValue}
|
||||
*/
|
||||
public static <T> void register(Class<T> inputType, Function<T, LazyValue> converter)
|
||||
{
|
||||
OutputConverter<T> instance = new OutputConverter<T>(converter);
|
||||
if (byResult.containsKey(inputType))
|
||||
throw new IllegalArgumentException(inputType + " already has a registered OutputConverter");
|
||||
byResult.put(inputType, instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Registers a new type to be able to be used as the return value of methods, converting from inputType to a {@link LazyValue}, with the
|
||||
* converter returning a {@link Value} type.</p>
|
||||
*
|
||||
* @see #register(Class, Function)
|
||||
* @param <T> The type of the input type
|
||||
* @param inputType The class of T
|
||||
* @param converter The function that converts an instance of T to a {@link Value}
|
||||
*/
|
||||
public static <T> void registerToValue(Class<T> inputType, Function<T, Value> converter)
|
||||
{
|
||||
OutputConverter<T> instance = new OutputConverter<>(converter.andThen(v -> (c, t) -> v));
|
||||
if (byResult.containsKey(inputType))
|
||||
throw new IllegalArgumentException(inputType + " already has a registered OutputConverter");
|
||||
byResult.put(inputType, instance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
package carpet.script.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.reflect.AnnotatedType;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import carpet.CarpetServer;
|
||||
import carpet.script.Context;
|
||||
import carpet.script.value.BooleanValue;
|
||||
import carpet.script.value.EntityValue;
|
||||
import carpet.script.value.FormattedTextValue;
|
||||
import carpet.script.value.NumericValue;
|
||||
import carpet.script.value.StringValue;
|
||||
import carpet.script.value.Value;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.network.ServerPlayerEntity;
|
||||
import net.minecraft.text.Text;
|
||||
|
||||
import static java.lang.annotation.ElementType.PARAMETER;
|
||||
import static java.lang.annotation.ElementType.TYPE_USE;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
/**
|
||||
* <p>Class that holds annotations for Scarpet parameters.</p>
|
||||
*
|
||||
* @see Param.Strict
|
||||
* @see Param.AllowSingleton
|
||||
* @see Param.Custom
|
||||
* @see Locator.Block
|
||||
* @see Locator.Vec3d
|
||||
* @see Locator.Function
|
||||
*
|
||||
*/
|
||||
public interface Param
|
||||
{
|
||||
/**
|
||||
* <p>Determines that this parameter accepts being passing a value directly instead of a list of those values.</p>
|
||||
*
|
||||
* <p>Can only be used in {@link List} parameters.</p>
|
||||
*
|
||||
* <p>The function method will receive a singleton of the item in question if there's a single value.</p>
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
@Target({ PARAMETER, TYPE_USE })
|
||||
public @interface AllowSingleton
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Determines that this (and optionally the following parameters) accept either a map of the specified key-value pairs, a list type [key,
|
||||
* value, key2, value2,...] or the same as the list but directly in the parameters (can be disabled in {@link #allowMultiparam()}).</p>
|
||||
*
|
||||
* <p>Can only be used in {@link Map} types, and {@link #allowMultiparam()} requires it to not be in a type parameter (since lists and maps
|
||||
* contains groups of single items)</p>
|
||||
*
|
||||
* <p><b>IMPORTANT:</b> Using this annotation with {@link #allowMultiparam()} will make this element consume each and every remaining value in the
|
||||
* function call, therefore it will cause any other parameters (that are not varargs) to throw as if they were not present, unless they are
|
||||
* optional (defined by using Java's {@link Optional} type). They could only be accessed if the parameter at this location is specifically a list
|
||||
* or map.<br> Having it as {@code true} will also cause the function to be considered of variable arguments even if it doesn't have varargs.</p>
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
@Target({ PARAMETER, TYPE_USE })
|
||||
public @interface KeyValuePairs
|
||||
{
|
||||
/**
|
||||
* <p>Whether or not this accepts the key-value pairs directly in the function call as myFunction(..., key, value, key2, value2)</p>
|
||||
*
|
||||
* <p>Having this set to {@code true} (as it is by default) has the side effects of effectively converting the method in a variable parameter
|
||||
* count method, and consuming everything remaining in the parameter list unless it finds as first parameter a map or list to generate the map
|
||||
* from, causing any following parameters (except varargs) to throw as if they were not present, unless they are optional.</p>
|
||||
*/
|
||||
boolean allowMultiparam() default true;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Defines that the parameter's converter has to be retrieved from the custom converter storage, in order to allow extensions to register
|
||||
* <b>complex</b> {@link ValueConverter}s. You can register such converters in {@link Params#registerCustomConverterFactory(BiFunction)}, although
|
||||
* if you only need a simple {@link Value}->something converter you should be looking at {@link SimpleTypeConverter} instead</p>
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
@Target({ PARAMETER, TYPE_USE })
|
||||
public @interface Custom
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Defines that a parameter of type {@link String}, {@link Text}, {@link ServerPlayerEntity}, {@link Boolean} or other registered strict type
|
||||
* <b>must</b> be of its corresponding {@link Value} in order to be accepted (respectively {@link StringValue}, {@link FormattedTextValue},
|
||||
* {@link EntityValue} or {@link BooleanValue}).</p>
|
||||
*
|
||||
* <p>If this annotation is not specified, Carpet will accept any other {@link Value} and call respectively {@link Value#getString()},
|
||||
* {@code new LiteralText(Value#getString())}, {@link EntityValue#getPlayerByValue(MinecraftServer, Value)} or {@link Value#getBoolean()}.</p>
|
||||
*
|
||||
* <p>You can define "shallow strictness" ({@link #shallow()}) if you want to allow passing both a {@link StringValue} or a
|
||||
* {@link FormattedTextValue} to a {@link Text} parameter or a {@link NumericValue} to a {@link BooleanValue}, but not any {@link Value}.</p>
|
||||
*
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
@Target({ PARAMETER, TYPE_USE })
|
||||
public @interface Strict
|
||||
{
|
||||
/**
|
||||
* <p>Defines whether this parameter can accept types with "shallow strictness", that is, in order to get a {@link Text}, accepting either a
|
||||
* {@link StringValue} or a {@link FormattedTextValue} as the parameter, or in order to get a {@link Boolean}, accepting either a
|
||||
* {@link NumericValue} or a {@link BooleanValue}.</p>
|
||||
*
|
||||
* <p>Without shallow mode, it would only accept from specifically a {@link FormattedTextValue} or {@link BooleanValue} respectively.
|
||||
*
|
||||
* <p>Using this in an unsupported type will throw {@link IllegalArgumentException}, just as if you used the annotation in an unsupported
|
||||
* type.</p>
|
||||
*
|
||||
* <p>This is {@code false} by default.</p>
|
||||
*/
|
||||
boolean shallow() default false;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Class that holds the actual converters and converter getting logic for those annotated types and things.</p>
|
||||
*
|
||||
* <p>It also holds the registry for strict {@link ValueConverter}s.</p>
|
||||
*
|
||||
* @see #registerStrictConverter(Class, boolean, ValueConverter)
|
||||
*
|
||||
*/
|
||||
public static final class Params
|
||||
{
|
||||
/**
|
||||
* <p>A {@link ValueConverter} that outputs the given {@link LazyValue} when running {@link #checkAndConvert(Iterator, Context, Context.Type)}, and
|
||||
* throws {@link UnsupportedOperationException} when trying to convert a {@link Value} directly.</p>
|
||||
*
|
||||
* <p>Public in order to allow custom {@link ValueConverter} to check whether values should be evaluated while testing conditions.</p>
|
||||
*/
|
||||
/*public static final ValueConverter<LazyValue> LAZY_VALUE_IDENTITY = new ValueConverter<LazyValue>()
|
||||
{ // No longer possible
|
||||
|
||||
@Override
|
||||
public LazyValue convert(Value val)
|
||||
{
|
||||
throw new UnsupportedOperationException(
|
||||
"Called convert() with a Value in LazyValue identity, where only evalAndConvert is supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public LazyValue checkAndConvert(Iterator<Value> lazyValueIterator, Context c, Context.Type theLazyT)
|
||||
{
|
||||
return lazyValueIterator.hasNext() ? lazyValueIterator.next() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName()
|
||||
{
|
||||
return "something";
|
||||
}
|
||||
};*/
|
||||
|
||||
/**
|
||||
* <p>A {@link ValueConverter} that outputs the {@link Context} in which the function has been called, and throws {@link UnsupportedOperationException} when trying to convert a {@link Value}
|
||||
* directly.</p>
|
||||
*/
|
||||
static final ValueConverter<Context> CONTEXT_PROVIDER = new ValueConverter<Context>()
|
||||
{
|
||||
@Override public String getTypeName() { return null; }
|
||||
|
||||
@Override
|
||||
public Context convert(Value value)
|
||||
{
|
||||
throw new UnsupportedOperationException("Called convert() with Value in Context Provider converter, where only checkAndConvert is supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Context checkAndConvert(Iterator<Value> valueIterator, Context context, Context.Type theLazyT)
|
||||
{
|
||||
return context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int valueConsumption()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* <p>A {@link ValueConverter} that outputs the {@link Context.Type} which the function has been called, or throws {@link UnsupportedOperationException} when trying to convert a {@link Value}
|
||||
* directly.</p>
|
||||
*/
|
||||
static final ValueConverter<Context.Type> CONTEXT_TYPE_PROVIDER = new ValueConverter<Context.Type>()
|
||||
{
|
||||
@Override public String getTypeName() { return null; }
|
||||
|
||||
@Override
|
||||
public Context.Type convert(Value value)
|
||||
{
|
||||
throw new UnsupportedOperationException("Called convert() with a Value in TheLazyT Provider, where only checkAndConvert is supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Context.Type checkAndConvert(Iterator<Value> valueIterator, Context context, Context.Type theLazyT)
|
||||
{
|
||||
return theLazyT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int valueConsumption()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* <p>Strict converters</p>
|
||||
*
|
||||
* <p>Stored as {@code <Pair<Type, shallow?>, Converter>}</p>
|
||||
*/
|
||||
private static Map<Pair<Class<?>, Boolean>, ValueConverter<?>> strictParamsByClassAndShallowness = new HashMap<>();
|
||||
static
|
||||
{ // TODO Specify strictness in name?
|
||||
registerStrictConverter(String.class, false, new SimpleTypeConverter<>(StringValue.class, StringValue::getString, "string"));
|
||||
registerStrictConverter(Text.class, false, new SimpleTypeConverter<>(FormattedTextValue.class, FormattedTextValue::getText, "text"));
|
||||
registerStrictConverter(Text.class, true, new SimpleTypeConverter<>(StringValue.class, FormattedTextValue::getTextByValue, "text"));
|
||||
registerStrictConverter(ServerPlayerEntity.class, false, new SimpleTypeConverter<>(EntityValue.class,
|
||||
v -> EntityValue.getPlayerByValue(CarpetServer.minecraft_server, v), "online player entity"));
|
||||
registerStrictConverter(Boolean.class, false, new SimpleTypeConverter<>(BooleanValue.class, BooleanValue::getBoolean, "boolean"));
|
||||
registerStrictConverter(Boolean.class, true, new SimpleTypeConverter<>(NumericValue.class, NumericValue::getBoolean, "boolean"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ya' know, gets the {@link ValueConverter} given the {@link Strict} annotation.
|
||||
*
|
||||
* @param type The {@link AnnotatedType} to search the annotation data and class in
|
||||
* @return The {@link ValueConverter} for the specified type and annotation data
|
||||
* @throws IllegalArgumentException If the type doesn't accept the {@link Strict} annotation or if it has been used incorrectly (shallow in
|
||||
* unsupported places)
|
||||
*/
|
||||
static ValueConverter<?> getStrictConverter(AnnotatedType type)
|
||||
{
|
||||
boolean shallow = type.getAnnotation(Strict.class).shallow();
|
||||
Class<?> clazz = (Class<?>) type.getType();
|
||||
Pair<Class<?>, Boolean> key = Pair.of(clazz, shallow);
|
||||
ValueConverter<?> converter = strictParamsByClassAndShallowness.get(key);
|
||||
if (converter != null)
|
||||
return converter;
|
||||
throw new IllegalArgumentException("Incorrect use of @Param.Strict annotation");
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Registers a new {@link Param.Strict} parameter converter with the specified shallowness.</p>
|
||||
*
|
||||
* <p>Registered types should follow the general contract of the rest of {@link Param.Strict} parameter converters, that is, don't have a
|
||||
* shallow-strict converter registered without having a fully-strict converter available. In order to register completely non-strict
|
||||
* converters, those should be registered in their respective {@link ValueConverter} classes, usually in {@link SimpleTypeConverter}.</p>
|
||||
*
|
||||
* @param <T> The type class of the return type of the given converter and therefore the generic of itself
|
||||
* @param type The class instance of the conversion result.
|
||||
* @param shallow {@code true} if you are registering a shallow-strict parameter, {@code false} if a "fully" strict one
|
||||
* @param converter The {@link ValueConverter} for the given type and shallowness.
|
||||
*/
|
||||
public static <T> void registerStrictConverter(Class<T> type, boolean shallow, ValueConverter<T> converter)
|
||||
{
|
||||
Pair<Class<?>, Boolean> key = Pair.of(type, shallow);
|
||||
if (strictParamsByClassAndShallowness.containsKey(key))
|
||||
throw new IllegalArgumentException(type + " already has a registered " + (shallow ? "" : "non-") + "shallow StrictConverter");
|
||||
strictParamsByClassAndShallowness.put(key, converter);
|
||||
}
|
||||
|
||||
private static List<BiFunction<AnnotatedType, Class<?>, ValueConverter<?>>> customFactories = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* <p>Allows extensions to register <b>COMPLEX</b> {@link ValueConverter} factories in order to be used with the {@link Param.Custom}
|
||||
* annotation.</p> <p><b>If you only need to register a converter from a {@link Value} to a type, use
|
||||
* {@link SimpleTypeConverter#registerType(Class, Class, java.util.function.Function, String)} instead. This is intended to be used when you
|
||||
* need more granular control over the conversion, such as custom extra parameters via annotations, converters using multiple values, or even
|
||||
* a variable number of values.</b></p> <p>The annotation parser will loop through all registered custom converter factories when searching
|
||||
* for the appropriate {@link ValueConverter} for a parameter annotated with the {@link Param.Custom} annotation.</p> <p>Factories are
|
||||
* expected to return {@code null} when the provided arguments don't match a {@link ValueConverter} they are able to create (or reuse).</p>
|
||||
* <p>You have {@link ValueCaster#get(Class)} and {@link ValueConverter#fromAnnotatedType(AnnotatedType)} available in case you need to get
|
||||
* valid {@link ValueConverter}s for things such as nested types, intermediary conversions or whatever you really need them for.</p>
|
||||
*
|
||||
* @param <T> The type that the ValueConverter will convert to. Its class will also be passed to the factory
|
||||
* @param factory A {@link BiFunction} that provides {@link ValueConverter}s given an {@link AnnotatedType} and the {@link Class} of its type,
|
||||
* for convenience reasons. The factory must return {@code null} if the specific conditions required to return a valid
|
||||
* converter are not met, therefore letting other registered factories try get theirs. Factories must also ensure that the
|
||||
* returned {@link ValueConverter} converts to the given {@link Class}, and that the {@link ValueConverter} follows the
|
||||
* contract of {@link ValueConverter}s, which can be found in its Javadoc. Factories should try to be specific in order to
|
||||
* avoid possible collisions with other extensions.
|
||||
*/
|
||||
@SuppressWarnings("unchecked") // this makes no sense... But I guess its preferable to enforce typesafety in callers
|
||||
public static <T> void registerCustomConverterFactory(BiFunction<AnnotatedType, Class<T>, ValueConverter<T>> factory)
|
||||
{
|
||||
customFactories.add((BiFunction<AnnotatedType, Class<?>, ValueConverter<?>>) (Object) factory);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked") // Stored correctly
|
||||
static <R> ValueConverter<R> getCustomConverter(AnnotatedType annoType, Class<R> type)
|
||||
{
|
||||
ValueConverter<R> result;
|
||||
for (BiFunction<AnnotatedType, Class<?>, ValueConverter<?>> factory : customFactories)
|
||||
{
|
||||
if ((result = (ValueConverter<R>) factory.apply(annoType, type)) != null)
|
||||
return result;
|
||||
}
|
||||
throw new IllegalArgumentException("No custom converter found for Param.Custom annotated param with type " + annoType.getType().getTypeName());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package carpet.script.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.lang.annotation.ElementType.METHOD;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
import carpet.script.Context;
|
||||
import carpet.script.LazyValue;
|
||||
import carpet.script.value.Value;
|
||||
|
||||
/**
|
||||
* <p>Defines a method that can be used as a function in the Scarpet language.</p>
|
||||
*
|
||||
* <p>Methods annotated with this annotation are not required to accept and return the typical {@code Context context, Integer t, List<Value> lv}, but
|
||||
* instead can specify whatever parameters they actually need that will be automatically converted from their respective {@link Value}s and passed to
|
||||
* the method as the expected type. Functions will automatically fail if given parameters are not compatible with the specified ones, or if the number
|
||||
* of provided arguments is either too large or too small.</p>
|
||||
*
|
||||
* <p>Types to be used in those functions must be already registered in their respective {@link ValueConverter} implementations.<br>
|
||||
* In order to register a new type to convert to, you can do so in {@link SimpleTypeConverter#registerType(Class, Class, java.util.function.Function, String)},
|
||||
* and in order to register a new variant of {@link Value}, use {@link ValueCaster#register(Class, String)}.<br>
|
||||
* In order to convert the output of your method to a {@link LazyValue} you will also need to register its conversion in {@link OutputConverter}</p>
|
||||
*
|
||||
* <p>In order for Carpet to find methods annotated with this annotation, you must add your function class(es) to Carpet by running
|
||||
* {@link AnnotationParser#parseFunctionClass(Class)} ONCE. The provided {@link Class} must be concrete and provide the default constructor or an
|
||||
* equivalent to it.</p>
|
||||
*
|
||||
* <p>Methods annotated with this annotation must not be static and must not declare throwing any checked exceptions.</p>
|
||||
*
|
||||
* <p>If one of the method's parameters is {@link Context}, Carpet will pass the actual {@link Context} of the expression to the
|
||||
* method. If one of the method's parameters is {@link Context.Type}, Carpet will pass the Context Type the function was called inside. That
|
||||
* is different from the Context Type provided in this annotation in that it's the one it was called with, while the one in the annotation
|
||||
* is the one that will be used when evaluating the lazy values passed to the function.</p>
|
||||
*
|
||||
* @see AnnotationParser
|
||||
* @see AnnotationParser#parseFunctionClass(Class)
|
||||
* @see Param.Strict
|
||||
* @see Param.AllowSingleton
|
||||
* @see Param.KeyValuePairs
|
||||
* @see Param.Custom
|
||||
* @see Locator.Block
|
||||
* @see Locator.Vec3d
|
||||
* @see Optional
|
||||
*/
|
||||
@Documented
|
||||
@Target(METHOD)
|
||||
@Retention(RUNTIME)
|
||||
public @interface ScarpetFunction
|
||||
{
|
||||
/**
|
||||
* <p>If the function can accept a variable number of parameters, either by declaring its last parameter as a varargs parameter or by having one
|
||||
* of their parameters use a converter that consumes a variable number of arguments, this must define the maximum number of parameters this
|
||||
* function can take.</p>
|
||||
*
|
||||
* <p>The parser will throw in case a function can accept a variable number of parameters but no maxParams value has been specified in its
|
||||
* {@link ScarpetFunction} annotation. <br>
|
||||
* The value, however, will be ignored if the function has a fixed number of parameters.</p>
|
||||
*
|
||||
* <p>Note that this maximum number of parameters refers to the limit of parameters that can be passed to a function from Scarpet, not the maximum
|
||||
* number of parameters the method will receive in its varargs parameter. Therefore, if using, for example, a locator argument, you should
|
||||
* consider that those can take either a single triple of values or 3 independent values, that would be counted in the maximum number of
|
||||
* parameters.</p>
|
||||
*
|
||||
* <p>Use -1 to specify an unlimited number of parameters.</p>
|
||||
*
|
||||
* @return The maximum number of parameters this function can accept
|
||||
*/
|
||||
int maxParams() default -2;
|
||||
|
||||
/**
|
||||
* <p>Defines the Context Type that will be used when evaluating arguments to annotated methods.</p>
|
||||
*
|
||||
* <p>Note that this is not the same as the output from a {@link Context.Type} parameter, since that returns the Context Type the method was
|
||||
* called with, while this defines what Context Type will be used to evaluate the arguments.</p>
|
||||
*
|
||||
* <p>Defaults to {@link Context.Type#NONE}, like any regular ContextFunctions</p>
|
||||
*
|
||||
* @see Context
|
||||
*/
|
||||
Context.Type contextType() default Context.Type.NONE;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package carpet.script.annotation;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import carpet.CarpetServer;
|
||||
import carpet.script.exception.InternalExpressionException;
|
||||
import carpet.script.value.EntityValue;
|
||||
import carpet.script.value.FormattedTextValue;
|
||||
import carpet.script.value.NumericValue;
|
||||
import carpet.script.value.Value;
|
||||
import carpet.script.value.ValueConversions;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.server.network.ServerPlayerEntity;
|
||||
import net.minecraft.text.Text;
|
||||
import net.minecraft.world.World;
|
||||
|
||||
/**
|
||||
* <p>A simple {@link ValueConverter} implementation that converts from a specified subclass of {@link Value} into {@code <R>} by using a given
|
||||
* function.</p>
|
||||
*
|
||||
* <p>{@link SimpleTypeConverter}s are reused whenever asked for one, since they don't have any complexity.</p>
|
||||
*
|
||||
* @see #registerType(Class, Class, Function, String)
|
||||
*
|
||||
* @param <T> The type of the required input {@link Value}
|
||||
* @param <R> The type that this converter converts to
|
||||
*/
|
||||
public final class SimpleTypeConverter<T extends Value, R> implements ValueConverter<R>
|
||||
{
|
||||
private static final Map<Class<?>, SimpleTypeConverter<? extends Value, ?>> byResult = new HashMap<>();
|
||||
static
|
||||
{
|
||||
registerType(Value.class, ServerPlayerEntity.class, val -> EntityValue.getPlayerByValue(CarpetServer.minecraft_server, val), "online player");
|
||||
registerType(EntityValue.class, Entity.class, EntityValue::getEntity, "entity");
|
||||
registerType(Value.class, World.class, val -> ValueConversions.dimFromValue(val, CarpetServer.minecraft_server), "dimension");
|
||||
registerType(Value.class, Text.class, FormattedTextValue::getTextByValue, "text");
|
||||
registerType(Value.class, String.class, Value::getString, "string"); // Check out @Param.Strict for more specific types
|
||||
|
||||
// Primitives are also query those classes
|
||||
registerType(NumericValue.class, Long.class, NumericValue::getLong, "number");
|
||||
registerType(NumericValue.class, Double.class, NumericValue::getDouble, "number");
|
||||
registerType(NumericValue.class, Integer.class, NumericValue::getInt, "number");
|
||||
registerType(Value.class, Boolean.class, Value::getBoolean, "boolean"); // Check out @Param.Strict for more specific types
|
||||
}
|
||||
|
||||
private final Function<T, R> converter;
|
||||
private final Class<T> valueClass;
|
||||
private final String typeName;
|
||||
|
||||
/**
|
||||
* <p>The default constructor for {@link SimpleTypeConverter}.</p>
|
||||
*
|
||||
* <p>This is public in order to provide an implementation to use when registering {@link ValueConverter}s for the {@link Param.Strict} annotation
|
||||
* registry, and it's not intended way to register new {@link SimpleTypeConverter}</p> <p>Use {@link #registerType(Class, Class, Function, String)} for
|
||||
* that.</p>
|
||||
*
|
||||
* @param inputType The required type for the input {@link Value}
|
||||
* @param converter The function to convert an instance of inputType into R.
|
||||
*/
|
||||
public SimpleTypeConverter(Class<T> inputType, Function<T, R> converter, String typeName)
|
||||
{
|
||||
this.converter = converter;
|
||||
this.valueClass = inputType;
|
||||
this.typeName = typeName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName()
|
||||
{
|
||||
return typeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link SimpleTypeConverter} for the specified outputType.
|
||||
*
|
||||
* @param <R> The type of the {@link SimpleTypeConverter} you are looking for
|
||||
* @param outputType The class that the returned {@link SimpleTypeConverter} converts to
|
||||
* @return The {@link SimpleTypeConverter} for the specified outputType
|
||||
*/
|
||||
@SuppressWarnings("unchecked") // T always extends Value, R is always the same as map's key, since map is private.
|
||||
static <R> SimpleTypeConverter<Value, R> get(Class<R> outputType)
|
||||
{
|
||||
return (SimpleTypeConverter<Value, R>) byResult.get(outputType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R convert(Value value)
|
||||
{
|
||||
return valueClass.isInstance(value) ? converter.apply(valueClass.cast(value)) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Registers a new conversion from a {@link Value} subclass to a Java type.</p>
|
||||
*
|
||||
* @param <T> The {@link Value} subtype required for this conversion, for automatic checked casting
|
||||
* @param <R> The type of the resulting object
|
||||
* @param requiredInputType The {@link Class} of {@code <T>}
|
||||
* @param outputType The {@link Class} of {@code <R>>}
|
||||
* @param converter A function that converts from the given {@link Value} subtype to the given type. Should ideally return {@code null}
|
||||
* when given {@link Value} cannot be converted to the {@code <R>}, to follow the {@link ValueConverter} contract, but it
|
||||
* can also throw an {@link InternalExpressionException} by itself if really necessary.
|
||||
* @param typeName The name of the type, following the conventions of {@link ValueConverter#getTypeName()}
|
||||
*/
|
||||
public static <T extends Value, R> void registerType(Class<T> requiredInputType, Class<R> outputType,
|
||||
Function<T, R> converter, String typeName)
|
||||
{
|
||||
SimpleTypeConverter<T, R> type = new SimpleTypeConverter<>(requiredInputType, converter, typeName);
|
||||
byResult.put(outputType, type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package carpet.script.annotation;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import carpet.script.value.AbstractListValue;
|
||||
import carpet.script.value.BlockValue;
|
||||
import carpet.script.value.BooleanValue;
|
||||
import carpet.script.value.EntityValue;
|
||||
import carpet.script.value.FormattedTextValue;
|
||||
import carpet.script.value.FunctionValue;
|
||||
import carpet.script.value.ListValue;
|
||||
import carpet.script.value.MapValue;
|
||||
import carpet.script.value.NBTSerializableValue;
|
||||
import carpet.script.value.NumericValue;
|
||||
import carpet.script.value.StringValue;
|
||||
import carpet.script.value.ThreadValue;
|
||||
import carpet.script.value.Value;
|
||||
|
||||
/**
|
||||
* <p>Simple {@link ValueConverter} implementation that casts a {@link Value} into one of its subclasses, either for use directly in parameters or
|
||||
* converters, or as an already working middle step.</p>
|
||||
*
|
||||
* <p>{@link ValueCaster}s are reused whenever asked for one, since they don't have any complexity.</p>
|
||||
*
|
||||
* @see #register(Class, String)
|
||||
*
|
||||
* @param <R> The {@link Value} subclass this {@link ValueCaster} casts to
|
||||
*/
|
||||
public final class ValueCaster<R> implements ValueConverter<R> // R always extends Value, not explicitly because of type checking
|
||||
{
|
||||
private static final Map<Class<? extends Value>, ValueCaster<? extends Value>> byResult = new HashMap<>();
|
||||
static
|
||||
{
|
||||
register(Value.class, "value");
|
||||
register(BlockValue.class, "block");
|
||||
register(EntityValue.class, "entity");
|
||||
register(FormattedTextValue.class, "formatted text");
|
||||
register(FunctionValue.class, "function");
|
||||
register(ListValue.class, "list");
|
||||
register(MapValue.class, "map");
|
||||
register(AbstractListValue.class, "list or similar"); // For LazyListValue basically? Not sure what should use this
|
||||
register(NBTSerializableValue.class, "nbt object");
|
||||
register(NumericValue.class, "number");
|
||||
register(BooleanValue.class, "boolean");
|
||||
register(StringValue.class, "string");
|
||||
register(ThreadValue.class, "thread");
|
||||
}
|
||||
|
||||
private final Class<R> outputType;
|
||||
private final String typeName;
|
||||
|
||||
private ValueCaster(Class<R> outputType, String typeName)
|
||||
{
|
||||
this.outputType = outputType;
|
||||
this.typeName = typeName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName()
|
||||
{
|
||||
return typeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Returns the registered {@link ValueCaster} for the specified outputType.</p>
|
||||
*
|
||||
* @param <R> The type of the {@link ValueCaster} you are looking for
|
||||
* @param outputType The class of the {@link Value} the returned {@link ValueCaster} casts to
|
||||
* @return The {@link ValueCaster} for the specified outputType
|
||||
*/
|
||||
@SuppressWarnings("unchecked") // Casters are stored with their exact class, for sure since the map is private (&& class has same generic as
|
||||
// caster)
|
||||
public static <R> ValueCaster<R> get(Class<R> outputType)
|
||||
{
|
||||
return (ValueCaster<R>) byResult.get(outputType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R convert(Value value)
|
||||
{
|
||||
if (!outputType.isInstance(value))
|
||||
return null;
|
||||
return outputType.cast(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Registers a new {@link Value} to be able to use it in {@link SimpleTypeConverter}</p>
|
||||
*
|
||||
* @param <R> The {@link Value} subclass
|
||||
* @param valueClass The class of T
|
||||
* @param typeName A {@link String} representing the name of this type. It will be used in error messages when there is no higher type
|
||||
* required<!--, with the form //Outdated concept <code>(function name) requires a (typeName) to be passed as (argName, if
|
||||
* available)</code>-->
|
||||
*/
|
||||
public static <R extends Value> void register(Class<R> valueClass, String typeName)
|
||||
{
|
||||
ValueCaster<R> caster = new ValueCaster<R>(valueClass, typeName);
|
||||
byResult.putIfAbsent(valueClass, caster);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package carpet.script.annotation;
|
||||
|
||||
import java.lang.reflect.AnnotatedType;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.commons.lang3.ClassUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import carpet.script.Context;
|
||||
import carpet.script.annotation.Param.Params;
|
||||
import carpet.script.value.Value;
|
||||
|
||||
/**
|
||||
* <p>Classes implementing this interface are able to convert {@link Value} instances into {@code <R>}, in order to easily use them in parameters for
|
||||
* Scarpet functions created using the {@link ScarpetFunction} annotation.</p>
|
||||
*
|
||||
* @param <R> The result type that the passed {@link Value}s will be converted to
|
||||
*/
|
||||
public interface ValueConverter<R>
|
||||
{
|
||||
/**
|
||||
* <p>Returns the the user-friendly name of the result this {@link ValueConverter} converts to, without {@code a} or {@code an}, and without
|
||||
* capitalizing the first letter.</p>
|
||||
*
|
||||
* <p>This method can return {@code null}, in which case users of this function should hide this {@link ValueConverter} from any aids or usages of
|
||||
* the function, meaning that the {@link ValueConverter} is only providing some meta information that isn't directly provided through the Scarpet
|
||||
* language.</p>
|
||||
*
|
||||
* <p>Those aids calling this method may append an {@code s} to the return value of this method, in case the type is used in places where more
|
||||
* than one may be present, such as lists or varargs.</p>
|
||||
*
|
||||
* @apiNote This method is intended to only be called when an error has occurred and therefore there is a need to print a stacktrace with some
|
||||
* helpful usage instructions.
|
||||
*/
|
||||
public String getTypeName();
|
||||
|
||||
/**
|
||||
* <p>Converts the given {@link Value} to {@code <R>}, which was defined when being registered.</p>
|
||||
*
|
||||
* <p> Returns {@code null} if one of the conversions failed, either because the {@link Value} was incompatible in some position of the chain, or
|
||||
* because the actual converting function returned {@code null} (which usually only occurs when the {@link Value} is incompatible/does not hold
|
||||
* the appropriate information)</p>
|
||||
*
|
||||
* <p>Functions using the converter can use {@link #getTypeName()} to get the name of the type this was trying to convert to, in case they are not
|
||||
* trying to convert to anything else, where it would be recommended to tell the user the name of the final type instead.</p>
|
||||
*
|
||||
* @param value The {@link Value} to convert
|
||||
* @return The converted value, or {@code null} if the conversion failed in the process
|
||||
* @apiNote <p>While most implementations of this method should and will return the type from this method, implementations that <b>require</b>
|
||||
* parameters from {@link #checkAndConvert(Iterator, Context, Context.Type)} or that require multiple parameters may decide to throw
|
||||
* {@link UnsupportedOperationException} in this method and override {@link #checkAndConvert(Iterator, Context, Context.Type)} instead. Those
|
||||
* implementations, however, should not be available for map or list types, since those can only operate with {@link Value}.</p>
|
||||
* <p>Currently, the only implementations requiring that are {@link Params#CONTEXT_PROVIDER} and {@link Params#CONTEXT_TYPE_PROVIDER}</p>
|
||||
* <p>Implementations can also provide different implementations for this and {@link #checkAndConvert(Iterator, Context, Context.Type)}, in case
|
||||
* they can support it in some situations that can't be used else, such as inside of lists or maps, although they should try to provide
|
||||
* in {@link #checkAndConvert(Iterator, Context, Context.Type)} at least the same conversion as the one from this method.</p>
|
||||
* <p>Even with the above reasons, {@link ValueConverter} users should try to implement {@link #convert(Value)} whenever possible instead of
|
||||
* {@link #checkAndConvert(Iterator, Context, Context.Type)}, since it allows its usage in generics of lists and maps.</p>
|
||||
*/
|
||||
@Nullable
|
||||
public R convert(Value value);
|
||||
|
||||
/**
|
||||
* <p>Returns whether this {@link ValueConverter} consumes a variable number of elements from the {@link Iterator} passed to it via
|
||||
* {@link #checkAndConvert(Iterator, Context, Context.Type)}.</p>
|
||||
*
|
||||
* @implNote The default implementation returns {@code false}
|
||||
* @see #valueConsumption()
|
||||
*/
|
||||
default public boolean consumesVariableArgs()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Declares the number of {@link Value}s this converter consumes from the {@link Iterator} passed to it in
|
||||
* {@link #checkAndConvert(Iterator, Context, Context.Type)}.</p>
|
||||
*
|
||||
* <p>If this {@link ValueConverter} can accept a variable number of arguments (therefore the result of calling {@link #consumesVariableArgs()}
|
||||
* <b>must</b> return {@code true}), it will return the minimum number of arguments it will consume.</p>
|
||||
*
|
||||
* @implNote The default implementation returns {@code 1}
|
||||
*
|
||||
*/
|
||||
default public int valueConsumption()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Gets the proper {@link ValueConverter} for the given {@link AnnotatedType}, considering the type of {@code R[]} as {@code R}.</p>
|
||||
*
|
||||
* <p>This function does not only consider the actual type (class) of the passed {@link AnnotatedType}, but also its annotations and generic
|
||||
* parameters in order to get the most specific {@link ValueConverter}.</p>
|
||||
*
|
||||
* <p>Some processing is delegated to the appropriate implementations of {@link ValueConverter} in order to get registered converters or generate
|
||||
* specific ones for some functions.</p>
|
||||
*
|
||||
* @param <R> The type of the class the returned {@link ValueConverter} will convert to. It is declared from the type in the
|
||||
* {@link AnnotatedType} directly inside the function.
|
||||
* @param annoType The {@link AnnotatedType} to search a {@link ValueConverter}.
|
||||
* @return A usable {@link ValueConverter} to convert from a {@link Value} to {@code <R>}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <R> ValueConverter<R> fromAnnotatedType(AnnotatedType annoType)
|
||||
{
|
||||
Class<R> type = annoType.getType() instanceof ParameterizedType ? // We are defining R here.
|
||||
(Class<R>) ((ParameterizedType) annoType.getType()).getRawType() :
|
||||
(Class<R>) annoType.getType();
|
||||
// I (altrisi) won't implement generics in varargs. Those are just PAINFUL. They have like 3-4 nested types and don't have the generics
|
||||
// and annotations in the same place, plus they have a different "conversion hierarchy" than the rest, making everything require
|
||||
// special methods to get the class from type, generics from type and annotations from type. Not worth the effort for me.
|
||||
// Example: AnnotatedGenericTypeArray (or similar) being (@Paran.KeyValuePairs Map<String, String>... name)
|
||||
// Those will just fail with a ClassCastException.
|
||||
if (type.isArray())
|
||||
type = (Class<R>) type.getComponentType(); // Varargs
|
||||
type = (Class<R>) ClassUtils.primitiveToWrapper(type); // It will be boxed anyway, this saves unboxing-boxing
|
||||
if (type == List.class)
|
||||
return (ValueConverter<R>) ListConverter.fromAnnotatedType(annoType); // Already checked that type is List
|
||||
if (type == Map.class)
|
||||
return (ValueConverter<R>) MapConverter.fromAnnotatedType(annoType); // Already checked that type is Map
|
||||
if (type == Optional.class)
|
||||
return (ValueConverter<R>) OptionalConverter.fromAnnotatedType(annoType);
|
||||
if (annoType.getDeclaredAnnotations().length != 0)
|
||||
{
|
||||
if (annoType.isAnnotationPresent(Param.Custom.class))
|
||||
return Param.Params.getCustomConverter(annoType, type); // Throws if incorrect usage
|
||||
if (annoType.isAnnotationPresent(Param.Strict.class))
|
||||
return (ValueConverter<R>) Params.getStrictConverter(annoType); // Throws if incorrect usage
|
||||
if (annoType.getAnnotations()[0].annotationType().getEnclosingClass() == Locator.class)
|
||||
return Locator.Locators.fromAnnotatedType(annoType, type);
|
||||
}
|
||||
|
||||
// Class only checks
|
||||
if (type.isAssignableFrom(Value.class))
|
||||
return Objects.requireNonNull(ValueCaster.get(type), "Value subclass " + type + " is not registered. Register it in ValueCaster to use it");
|
||||
// if (type == LazyValue.class) // No longer supported
|
||||
// return (ValueConverter<R>) Params.LAZY_VALUE_IDENTITY;
|
||||
if (type == Context.class)
|
||||
return (ValueConverter<R>) Params.CONTEXT_PROVIDER;
|
||||
if (type == Context.Type.class)
|
||||
return (ValueConverter<R>) Params.CONTEXT_TYPE_PROVIDER;
|
||||
return Objects.requireNonNull(SimpleTypeConverter.get(type), "Type " + type + " is not registered. Register it in SimpleTypeConverter to use it");
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Checks for the presence of the next {@link Value} in the given {@link Iterator}, evaluates it with the given {@link Context} and then
|
||||
* converts it to this {@link ValueConverter}'s output type.</p>
|
||||
*
|
||||
* <p>This should be the preferred way to call the converter, since it <!--allows some conversions that may not be supported by using directly a
|
||||
* {@link Value},--> allows multi-param converters and allows meta converters (such as the {@link Context} provider)</p>
|
||||
*
|
||||
* @implSpec Implementations of this method are not required to move the {@link Iterator} to the next position, such as in the case of meta
|
||||
* providers like {@link Context}. <p>Implementations can also use more than a single parameter when being called with this function,
|
||||
* but in such case they must implement {@link #valueConsumption()} to return how many parameters do they consume at minimum, and, if
|
||||
* they may consume variable arguments, implement {@link #consumesVariableArgs()}</p> <p>This method holds the same nullability
|
||||
* constraints as {@link #convert(Value)}</p>
|
||||
* @param valueIterator An {@link Iterator} holding the {@link Value} to convert in next position
|
||||
* @param context The {@link Context} this function has been called with. This was used when this passed lazy values instead in order to
|
||||
* evaluate, now it's used to get the context. It is now ignored by the default implementation
|
||||
* @param theLazyT The {@code t} that the original function was called with. It is ignored by the default implementation.
|
||||
* @return The next {@link Value} (s) converted to the type {@code <R>} of this {@link ValueConverter}
|
||||
* @implNote This method's default implementation runs the {@link #convert(Value)} function in the next {@link Value} ignoring {@link Context} and
|
||||
* {@code theLazyT}.
|
||||
*/
|
||||
default public R checkAndConvert(Iterator<Value> valueIterator, Context context, Context.Type theLazyT)
|
||||
{
|
||||
if (!valueIterator.hasNext())
|
||||
return null;
|
||||
return convert(valueIterator.next());
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
package carpet.script.utils;
|
||||
|
||||
import carpet.fakes.BiomeEffectsInterface;
|
||||
import carpet.script.value.BlockValue;
|
||||
import carpet.script.value.ListValue;
|
||||
import carpet.script.value.NumericValue;
|
||||
import carpet.script.value.StringValue;
|
||||
import carpet.script.value.Value;
|
||||
import carpet.script.value.ValueConversions;
|
||||
import net.minecraft.client.color.world.FoliageColors;
|
||||
import net.minecraft.server.world.ServerWorld;
|
||||
import net.minecraft.util.math.MathHelper;
|
||||
import net.minecraft.util.registry.Registry;
|
||||
import net.minecraft.world.biome.Biome;
|
||||
import net.minecraft.world.gen.feature.ConfiguredFeature;
|
||||
@@ -18,16 +21,23 @@ import java.util.function.BiFunction;
|
||||
|
||||
public class BiomeInfo
|
||||
{
|
||||
// copy of from Biome.getDefaultFoliageColor
|
||||
private static int getDefaultFoliageColor(Biome b) {
|
||||
double d = MathHelper.clamp(b.getTemperature(), 0.0F, 1.0F);
|
||||
double e = MathHelper.clamp(b.getDownfall(), 0.0F, 1.0F);
|
||||
return FoliageColors.getColor(d, e);
|
||||
}
|
||||
|
||||
public final static Map<String, BiFunction<ServerWorld, Biome, Value>> biomeFeatures = new HashMap<String, BiFunction<ServerWorld, Biome, Value>>(){{
|
||||
put("top_material", (w, b) -> new BlockValue( b.getGenerationSettings().getSurfaceConfig().getTopMaterial(), null, null));
|
||||
put("under_material", (w, b) -> new BlockValue( b.getGenerationSettings().getSurfaceConfig().getUnderMaterial(), null, null));
|
||||
put("category", (w, b) -> StringValue.of(b.getCategory().getName()));
|
||||
put("temperature", (w, b) -> NumericValue.of(b.getTemperature()));
|
||||
put("fog_color", (w, b) -> ValueConversions.ofRGB(b.getFogColor()));
|
||||
put("foliage_color", (w, b) -> ValueConversions.ofRGB(b.getFoliageColor()));
|
||||
put("sky_color", (w, b) -> ValueConversions.ofRGB(b.getSkyColor()));
|
||||
put("water_color", (w, b) -> ValueConversions.ofRGB(b.getWaterColor()));
|
||||
put("water_fog_color", (w, b) -> ValueConversions.ofRGB(b.getWaterFogColor()));
|
||||
put("fog_color", (w, b) -> ValueConversions.ofRGB(((BiomeEffectsInterface)b.getEffects()).getCMFogColor()));
|
||||
put("foliage_color", (w, b) -> ValueConversions.ofRGB(((BiomeEffectsInterface)b.getEffects()).getCMFoliageColor().orElseGet(() -> getDefaultFoliageColor(b))));
|
||||
put("sky_color", (w, b) -> ValueConversions.ofRGB(((BiomeEffectsInterface)b.getEffects()).getCMSkyColor()));
|
||||
put("water_color", (w, b) -> ValueConversions.ofRGB(((BiomeEffectsInterface)b.getEffects()).getCMWaterColor()));
|
||||
put("water_fog_color", (w, b) -> ValueConversions.ofRGB(((BiomeEffectsInterface)b.getEffects()).getCMWaterFogColor()));
|
||||
put("humidity", (w, b) -> NumericValue.of(b.getDownfall()));
|
||||
put("precipitation", (w, b) -> StringValue.of(b.getPrecipitation().getName()));
|
||||
put("depth", (w, b) -> NumericValue.of(b.getDepth()));
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
package carpet.script.utils;
|
||||
|
||||
import carpet.script.CarpetScriptServer;
|
||||
import carpet.settings.ParsedRule;
|
||||
import carpet.settings.Validator;
|
||||
import carpet.utils.Messenger;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.command.CommandException;
|
||||
import net.minecraft.server.command.ServerCommandSource;
|
||||
import net.minecraft.util.WorldSavePath;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.io.StringWriter;
|
||||
import java.io.Writer;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
/**
|
||||
* A class used to save scarpet app store scripts to disk
|
||||
*/
|
||||
public class ScriptDownloader
|
||||
{
|
||||
/** A local copy of the scarpet repo's file structure, to avoid multiple queries to github.com while typing out the
|
||||
* {@code /script download} command and getting the suggestions.
|
||||
*/
|
||||
public static final PathNode appStoreRoot = PathNode.folder(null, "");
|
||||
|
||||
/** This is the base link to the scarpet app repo from the github api.
|
||||
*/
|
||||
private static String scarpetRepoLink = "https://api.github.com/repos/gnembon/scarpet/contents/programs/";
|
||||
|
||||
public static class ScarpetAppStoreValidator extends Validator<String>
|
||||
{
|
||||
@Override public String validate(ServerCommandSource source, ParsedRule<String> currentRule, String newValue, String string)
|
||||
{
|
||||
appStoreRoot.sealed = false;
|
||||
appStoreRoot.children = new HashMap<>();
|
||||
if (newValue.equalsIgnoreCase("none"))
|
||||
{
|
||||
scarpetRepoLink = null;
|
||||
return newValue;
|
||||
}
|
||||
if (newValue.endsWith("/")) newValue = newValue.replaceAll("/$", "");
|
||||
scarpetRepoLink = "https://api.github.com/repos/"+newValue+"/";
|
||||
return newValue;
|
||||
}
|
||||
@Override
|
||||
public String description() { return "Appstore link should point to a valid github repository";}
|
||||
}
|
||||
|
||||
public static class PathNode
|
||||
{
|
||||
public String name;
|
||||
public PathNode parent;
|
||||
public Map<String, PathNode> children;
|
||||
public boolean sealed;
|
||||
public String value;
|
||||
public static PathNode folder(PathNode parent, String name)
|
||||
{
|
||||
PathNode node = new PathNode(parent, name);
|
||||
node.children = new HashMap<>();
|
||||
node.value = null;
|
||||
node.sealed = false;
|
||||
return node;
|
||||
}
|
||||
|
||||
public static PathNode scriptFile(PathNode parent, String name, String value)
|
||||
{
|
||||
PathNode node = new PathNode(parent, name);
|
||||
node.children = null;
|
||||
node.value = value;
|
||||
node.sealed = true;
|
||||
return node;
|
||||
}
|
||||
|
||||
public boolean isLeaf()
|
||||
{
|
||||
return value != null;
|
||||
}
|
||||
public String pathElement()
|
||||
{
|
||||
return name+(isLeaf()?"":"/");
|
||||
}
|
||||
public String getPath()
|
||||
{
|
||||
return createPrePath().toString();
|
||||
}
|
||||
private StringBuilder createPrePath()
|
||||
{
|
||||
return this == appStoreRoot ? new StringBuilder() : parent.createPrePath().append(pathElement());
|
||||
}
|
||||
private PathNode(PathNode parent, String name)
|
||||
{
|
||||
this.parent = parent;
|
||||
this.name = name;
|
||||
this.sealed = false;
|
||||
}
|
||||
|
||||
public void fillChildren() throws IOException
|
||||
{
|
||||
if (sealed) return;
|
||||
if (scarpetRepoLink == null) throw new IOException("Accessing scarpet app repo is disabled");
|
||||
|
||||
String queryPath = scarpetRepoLink + getPath();
|
||||
CarpetScriptServer.LOG.error("Fetching: "+queryPath);
|
||||
String response;
|
||||
try
|
||||
{
|
||||
URL appURL = new URL(queryPath);
|
||||
response = ScriptDownloader.getStringFromStream(appURL.openStream());
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
CarpetScriptServer.LOG.error("Problems fetching: "+e);
|
||||
sealed = true;
|
||||
return;
|
||||
}
|
||||
JsonArray files = new JsonParser().parse(response).getAsJsonArray();
|
||||
for(JsonElement je : files)
|
||||
{
|
||||
JsonObject jo = je.getAsJsonObject();
|
||||
String name = jo.get("name").getAsString();
|
||||
if (jo.get("type").getAsString().equals("dir"))
|
||||
{
|
||||
children.put(name, folder(this, name));
|
||||
}
|
||||
else if (name.matches("(\\w+\\.scl?)"))
|
||||
{
|
||||
String value = jo.get("download_url").getAsString();
|
||||
children.put(name, scriptFile(this, name, value));
|
||||
}
|
||||
}
|
||||
CarpetScriptServer.LOG.error("nodes for "+ getPath()+" are "+children.values().stream().map(PathNode::pathElement).collect(Collectors.joining(",")));
|
||||
sealed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if doing down the directory structire cannot continue since the matching element is either a leaf or
|
||||
* a string not matching of any node.
|
||||
* @param pathElement
|
||||
* @return
|
||||
*/
|
||||
public boolean cannotContinueFor(String pathElement) throws IOException
|
||||
{
|
||||
if (isLeaf()) return true;
|
||||
fillChildren();
|
||||
return !children.containsKey(pathElement);
|
||||
}
|
||||
|
||||
public List<String> createPathSuggestions() throws IOException
|
||||
{
|
||||
if (isLeaf())
|
||||
{
|
||||
CarpetScriptServer.LOG.error("suggestions for leaf :"+name);
|
||||
return Collections.singletonList(getPath());
|
||||
}
|
||||
fillChildren();
|
||||
String prefix = getPath();
|
||||
CarpetScriptServer.LOG.error("suggestions for node :"+name);
|
||||
return children.values().stream().map(s -> prefix+s.pathElement().replaceAll("/$", "")).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public PathNode drillDown(String pathElement) throws IOException
|
||||
{
|
||||
if (isLeaf()) throw new IOException(pathElement+" is not a folder");
|
||||
fillChildren();
|
||||
if (!children.containsKey(pathElement)) throw new IOException("Folder "+pathElement+" is not present");
|
||||
return children.get(pathElement);
|
||||
}
|
||||
|
||||
public String getValue(String file) throws IOException
|
||||
{
|
||||
PathNode leaf = drillDown(file);
|
||||
if (!leaf.isLeaf()) throw new IOException(file+" is not a file");
|
||||
return leaf.value;
|
||||
}
|
||||
}
|
||||
|
||||
/** This method searches for valid file names from the user-inputted string, e.g if the user has thus far typed
|
||||
* {@code survival/a} then it will return all the files in the {@code survival} directory of the scarpet repo (and
|
||||
* will automatically highlight those starting with a), and the string {@code survival/} as the current most valid path.
|
||||
*
|
||||
* @param currentPath The path down which we want to search for files
|
||||
* @return A pair of the current valid path, as well as the set of all the file/directory names at the end of that path
|
||||
*/
|
||||
public static List<String> suggestionsFromPath(String currentPath) throws IOException
|
||||
{
|
||||
String[] path = currentPath.split("/");
|
||||
PathNode appKiosk = appStoreRoot;
|
||||
for(String pathElement : path)
|
||||
{
|
||||
if (appKiosk.cannotContinueFor(pathElement)) return appKiosk.createPathSuggestions();
|
||||
appKiosk = appKiosk.children.get(pathElement);
|
||||
}
|
||||
return appKiosk.createPathSuggestions();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** A simple shorthand for calling the {@link ScriptDownloader#getScriptCode} and {@link ScriptDownloader#saveScriptToFile}
|
||||
* methods to avoid repeating code and so it makes more sense what it's exactly doing.
|
||||
*
|
||||
* @param path The user-inputted path to the script
|
||||
* @param global Whether or not we wanna save it to the local scripts folder or in the global config folder
|
||||
* @return {@code 1} if we succesfully saved the script, {@code 0} otherwise
|
||||
*/
|
||||
|
||||
public static int downloadScript(CommandContext<ServerCommandSource> cc, String path, boolean global)
|
||||
{
|
||||
Pair<String,String> code = getScriptCode(path);
|
||||
return saveScriptToFile(path, code.getLeft(), code.getRight(), cc, global);
|
||||
}
|
||||
|
||||
/** Gets the code once the user inputs the command.
|
||||
*
|
||||
* @param appPath The user inputted path to the scarpet script
|
||||
* @return Pair of app file name and content
|
||||
*/
|
||||
public static Pair<String,String> getScriptCode(String appPath)
|
||||
{
|
||||
String[] path = appPath.split("/");
|
||||
PathNode appKiosk = appStoreRoot;
|
||||
try
|
||||
{
|
||||
for(String pathElement : Arrays.copyOfRange(path, 0, path.length-1))
|
||||
appKiosk = appKiosk.drillDown(pathElement);
|
||||
String appName = path[path.length-1];
|
||||
URL appURL = new URL(appKiosk.getValue(appName));
|
||||
HttpURLConnection http = (HttpURLConnection) appURL.openConnection();
|
||||
return Pair.of(appName, getStringFromStream((InputStream) http.getContent()));
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new CommandException(Messenger.c("rb '"+ appPath + "' is not a valid path to a scarpet app: "+e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
public static int saveScriptToFile(String path, String appFileName, String code, CommandContext<ServerCommandSource> cc, boolean globalSavePath){
|
||||
String scriptPath;
|
||||
String message;
|
||||
if(globalSavePath && !cc.getSource().getMinecraftServer().isDedicated())
|
||||
{ //cos config folder only is in clients
|
||||
scriptPath = FabricLoader.getInstance().getConfigDir().resolve("carpet/scripts/appstore").toAbsolutePath()+"/"+path;
|
||||
message = "global script config folder";
|
||||
}
|
||||
else
|
||||
{
|
||||
scriptPath = cc.getSource().getMinecraftServer().getSavePath(WorldSavePath.ROOT).resolve("scripts").toAbsolutePath()+"/"+appFileName;
|
||||
message = "world scripts folder";
|
||||
}
|
||||
try
|
||||
{
|
||||
Path scriptLocation = Paths.get(scriptPath);
|
||||
Path scriptFolderLocation = Paths.get(scriptPath.substring(0, scriptPath.lastIndexOf('/')));//folder location without file name
|
||||
if (!Files.exists(scriptFolderLocation))
|
||||
Files.createDirectories(scriptFolderLocation);
|
||||
|
||||
if(Files.exists(scriptLocation))
|
||||
Messenger.m(cc.getSource(), String.format("gi Note: overwriting existing file '%s'", appFileName));
|
||||
|
||||
|
||||
Messenger.m(cc.getSource(), "gi Placing script in " + message);
|
||||
|
||||
FileWriter fileWriter = new FileWriter(scriptPath);
|
||||
fileWriter.write(code);
|
||||
fileWriter.close();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
Messenger.m(cc.getSource(), "r Error in downloading script");
|
||||
return 0;
|
||||
}
|
||||
Messenger.m(cc.getSource(), "gi Successfully created "+ appFileName + " in " + message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/** Returns the string from the inputstream gotten from the html request.
|
||||
* Thanks to App Shah in <a href="https://crunchify.com/in-java-how-to-read-github-file-contents-using-httpurlconnection-convert-stream-to-string-utility/">this post</a>
|
||||
* for this code.
|
||||
*
|
||||
* @return the string input from the InputStream
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
public static String getStringFromStream(InputStream inputStream) throws IOException
|
||||
{
|
||||
if (inputStream != null)
|
||||
{
|
||||
Writer stringWriter = new StringWriter();
|
||||
|
||||
char[] charBuffer = new char[2048];
|
||||
try
|
||||
{
|
||||
Reader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
|
||||
int counter;
|
||||
while ((counter = reader.read(charBuffer)) != -1)
|
||||
stringWriter.write(charBuffer, 0, counter);
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
inputStream.close();
|
||||
}
|
||||
return stringWriter.toString();
|
||||
}
|
||||
else
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user