From 5f16ffd933dccd37790cb9d2c523eb79ff04daf9 Mon Sep 17 00:00:00 2001
From: Ghoulboy
Date: Wed, 19 May 2021 22:30:39 +0200
Subject: [PATCH 1/6] Adding /script download (#826)
This reverts commit e98adb77a52d1e274ec320e0cafab34b24c1c738.
* First attempt at creating download system...
* Last try, I had uncommitted changes which I forgot to add.
If it fails now i swear imma delete my fork of fabric carpet.
* OK, this is the last chance. I have checked and double checked, this should not fail.
Todo make them more detailed, Im not the best person to do that tbh...
* Towards a suggestion builder
(Also committing stuff in ScriptDownloader incase it crashes)
* Finished first theoretically functional draft of suggestion builder
* improving suggestion building with actual github api
* Collapsing identical error messages
* Got suggestions to work!
Now gotta get the actual file to save to disk properly
* replacing whitespace with url whitespace to make it read form 'creativ building tools' etc
* Successfully downloaded script into folder location!
* only clients can access global scripts folder, so don't want to cause error message
* Compacting code and updating docs
* Fixed getting folders from file names
---
docs/scarpet/api/ScriptCommand.md | 8 +
.../java/carpet/commands/ScriptCommand.java | 35 ++-
.../java/carpet/helpers/ScriptDownloader.java | 234 ++++++++++++++++++
3 files changed, 268 insertions(+), 9 deletions(-)
create mode 100644 src/main/java/carpet/helpers/ScriptDownloader.java
diff --git a/docs/scarpet/api/ScriptCommand.md b/docs/scarpet/api/ScriptCommand.md
index f2f2aa6a..30ea5bf0 100644
--- a/docs/scarpet/api/ScriptCommand.md
+++ b/docs/scarpet/api/ScriptCommand.md
@@ -167,3 +167,11 @@ This would slow down the computation of fib(40) from a minute to two, but allows
and be responsive to commands, using about half of each tick to advance the computation. Obviously depending on the
problem, and available hardware, certain things can take more or less time to execute, so portioning of work with
calling `gametick` should be balanced in each case separately
+
+# `/script download` command
+
+`/script download ` command allows to download and save scripts directly from [scarpet app store](https://www.github.com/gnembon/scarpet).
+You can choose whether to save the script in the global config scripts folder or the local per-world script folder,
+(on servers it always saves in the scripts folder) and it will be saved there, e.g `/script download local survival/angel_block.sc`
+would save the `angel_block.sc` file [here](https://github.com/gnembon/scarpet/blob/master/programs/survival/angel_block.sc)
+to a folder called `appstore/survival` in the world's scripts folder.
diff --git a/src/main/java/carpet/commands/ScriptCommand.java b/src/main/java/carpet/commands/ScriptCommand.java
index 9746211e..dfce4b6a 100644
--- a/src/main/java/carpet/commands/ScriptCommand.java
+++ b/src/main/java/carpet/commands/ScriptCommand.java
@@ -2,6 +2,7 @@ package carpet.commands;
import carpet.CarpetServer;
import carpet.CarpetSettings;
+import carpet.helpers.ScriptDownloader;
import carpet.script.CarpetEventServer;
import carpet.script.CarpetExpression;
import carpet.script.CarpetScriptHost;
@@ -31,18 +32,13 @@ import net.minecraft.command.argument.BlockStateArgumentType;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.util.Clearable;
+import net.minecraft.util.Pair;
import net.minecraft.util.math.BlockPos;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.BlockBox;
import org.apache.commons.lang3.StringUtils;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Locale;
-import java.util.Set;
-import java.util.TreeSet;
+import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import java.util.function.Supplier;
@@ -61,6 +57,7 @@ public class ScriptCommand
Set allFunctions = (new CarpetExpression(null, "null", null, null)).getExpr().getFunctionNames();
scarpetFunctions = new TreeSet<>(Expression.none.getFunctionNames());
APIFunctions = allFunctions.stream().filter(s -> !scarpetFunctions.contains(s)).collect(Collectors.toCollection(TreeSet::new));
+ ScriptDownloader.updateLocalRepoStructure();
}
public static List suggestFunctions(ScriptHost host, String previous, String prefix)
@@ -105,6 +102,23 @@ 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 suggestDownloadableApps(
+ CommandContext context,
+ SuggestionsBuilder suggestionsBuilder
+ ) {
+ String previous = suggestionsBuilder.getRemaining();
+ Pair> suggestions = ScriptDownloader.fileNamesFromPath(previous);
+ String currentValidPath = suggestions.getLeft();
+
+ suggestions.getRight().forEach(s -> suggestionsBuilder.suggest(currentValidPath + s));
+
+ return suggestionsBuilder.buildFuture();
+ }
+
public static void register(CommandDispatcher dispatcher)
{
LiteralArgumentBuilder b = literal("globals").
@@ -128,7 +142,7 @@ public class ScriptCommand
null,
null,
""
- )).
+ )) .
then(argument("arguments", StringArgumentType.greedyString()).
executes( (cc) -> invoke(
cc,
@@ -302,10 +316,13 @@ public class ScriptCommand
StringArgumentType.getString(cc, "call")
)?1:0))));
+ LiteralArgumentBuilder 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").
diff --git a/src/main/java/carpet/helpers/ScriptDownloader.java b/src/main/java/carpet/helpers/ScriptDownloader.java
new file mode 100644
index 00000000..2aa43d89
--- /dev/null
+++ b/src/main/java/carpet/helpers/ScriptDownloader.java
@@ -0,0 +1,234 @@
+package carpet.helpers;
+
+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.api.EnvType;
+import net.fabricmc.loader.api.FabricLoader;
+import net.minecraft.command.CommandException;
+import net.minecraft.server.command.ServerCommandSource;
+import net.minecraft.text.LiteralText;
+import net.minecraft.util.Pair;
+import net.minecraft.util.WorldSavePath;
+
+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.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A class used to save scarpet app store scripts to disk
+ */
+
+@SuppressWarnings("rawtypes")
+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. Therefore, we save the file structure at the beginning
+ * of the server's starting, and update it with {@link ScriptDownloader#updateLocalRepoStructure()} if we get an error
+ * in the query, thereby allowing us to re-enter the command and get the correct app (unless of course the inputted
+ * path is itself wrong)
+ */
+ public static Map localScarpetRepoStructure = new HashMap<>();
+
+ /** This is the link to the scarpet app repo from the github api, so if that ever changes then this variable would
+ * have to change as well to match.
+ */
+ private static final String scarpetRepoLink = "https://api.github.com/repos/gnembon/scarpet/contents/programs/";
+
+ /** 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 cc, String path, boolean global){
+ String code = getScriptCode(path);
+ return saveScriptToFile(path, code, cc, global);
+ }
+
+ /** Gets the code once the user inputs the command. The code isn't saved in {@link ScriptDownloader#localScarpetRepoStructure}
+ * as the scarpet repo is very large and may get much larger in the future, and that may cause RAM issues if we have
+ * the entire thing saved in memory.
+ *
+ * @param path The user inputted path to the scarpet script
+ * @return the HTML request from the path program using {@link ScriptDownloader#getStringFromStream} method to convert
+ * HTML response into a string
+ */
+ public static String getScriptCode(String path){
+ try {
+ String link = "https://raw.githubusercontent.com/gnembon/scarpet/master/programs/"+ path.replace(" ", "%20");
+ URL appURL = new URL(link);
+ HttpURLConnection http = (HttpURLConnection) appURL.openConnection();
+ return getStringFromStream((InputStream) http.getContent());
+ } catch (IOException e){//todo add checks to distinguish between incorrect file path or change in scarpet repo structure
+ throw new CommandException(new LiteralText("'"+ path + "' is not a valid path to a scarpet app"));
+ }
+ }
+
+ public static int saveScriptToFile(String name, String code, CommandContext cc, boolean globalSavePath){
+ Path scriptLocation;
+ String location;
+ if(globalSavePath && FabricLoader.getInstance().getEnvironmentType()== EnvType.CLIENT) {//cos config folder only is in clients
+ scriptLocation = FabricLoader.getInstance().getConfigDir().resolve("carpet/scripts/appstore");
+ location = "global script config folder";
+ } else {
+ scriptLocation = cc.getSource().getMinecraftServer().getSavePath(WorldSavePath.ROOT).resolve("scripts/appstore");
+ location = "world scripts folder";
+ }
+ try {
+ String scriptPath = scriptLocation.toAbsolutePath() + "/" + name;
+ 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'", name));
+ }
+
+ Messenger.m(cc.getSource(), "gi Placing script in " + location);
+
+ 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 "+ name + " in " + location);
+ return 1;
+ }
+
+ /** Returns the string from the inputstream gotten from the html request.
+ * Thanks to App Shah in this post
+ * 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 "";
+ }
+ }
+
+ /** Updates local copy of scarpet repo, giving an error otherwise which ought to be reported to fabric-carpet devs,
+ * in particular Ghoulboy if possible as he wrote this code.
+ *
+ * @author Ghoulboy
+ */
+
+ public static void updateLocalRepoStructure(){
+ try{
+ localScarpetRepoStructure = updateLocalRepoStructure("");
+ } catch (IOException | IllegalStateException exc){//should not happen as long as repo name stays the same
+ System.out.println("ERROR: ScriptDownloader#scarpetRepoLink variable is out of date, please update your carpet version, and if the problem persists please submit a bug report here: https://github.com/gnembon/fabric-carpet/issues/new");
+ throw new CommandException(new LiteralText("Internal scarpet app store repo structure changed, please contact fabric-carpet developers, enclosing a copy of the server log!"));
+ }
+ }
+
+ /** A DFS to save the file structure (not the code) in the scarpet repo, called at server start and whenever running
+ * the command {@code /script download} command causes an oopsie, so the internal scarpet repo structure is changed
+ * and we therefore need to update it. We don't save the code here in case it needs updating from the last time, and
+ * also not to slow down the search for
+ *
+ * @param currentPath The current path down which we are looking for code.
+ * @return The scarpet repo structure as a map which you navigate down, so you don't need to query to github API
+ * multiple times
+ * @throws IOException only if {@link ScriptDownloader#scarpetRepoLink} structure is out of date, i.e scarpet internal
+ * repo structure has changed
+ */
+
+ public static Map updateLocalRepoStructure(String currentPath) throws IOException{
+
+ Map ret = new HashMap<>();
+
+ String queryPath = scarpetRepoLink + currentPath;
+
+ URL appURL = new URL(queryPath);
+
+ String response = ScriptDownloader.getStringFromStream(appURL.openStream());
+
+ JsonArray files = new JsonParser().parse(response).getAsJsonArray();
+
+ for(JsonElement je : files){
+ JsonObject jo = je.getAsJsonObject();
+ String name = jo.get("name").getAsString();
+ String filePath = jo.get("path").getAsString().substring(9).replace(" ", "%20");
+
+ if (jo.get("type").getAsString().equals("dir") && !name.contains("shared")) {//cos that may be a directory for shared data, so we don't wanna search there
+ ret.put(name, updateLocalRepoStructure(filePath));
+ } else if (name.matches("(\\w+\\.scl?)")){
+ ret.put(name, null);
+ }
+ }
+
+ return ret;
+ }
+
+ /** 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 Pair> fileNamesFromPath(String currentPath){
+ String[] path = currentPath.split("/");
+
+ Map currentLookedAtFiles = localScarpetRepoStructure;
+ StringBuilder currentValidPath = new StringBuilder();
+
+
+ for(String folder : path){
+ if(!currentLookedAtFiles.containsKey(folder) || folder.matches(".+\\.sc"))
+ return new Pair<>(currentValidPath.toString(), currentLookedAtFiles.keySet());
+
+ currentValidPath.append(folder).append("/");
+
+
+ Map newFolder = currentLookedAtFiles.get(folder);
+
+ if(newFolder == null)
+ return new Pair<>(currentValidPath.toString(), currentLookedAtFiles.keySet());
+
+ currentLookedAtFiles = newFolder;
+ }
+
+ return new Pair<>(currentValidPath.toString(), currentLookedAtFiles.keySet());
+ }
+}
From 866f1f149722b634e63c5d932640299694a0b3d4 Mon Sep 17 00:00:00 2001
From: gnembon
Date: Wed, 19 May 2021 20:46:19 -0300
Subject: [PATCH 2/6] moved downloader
---
src/main/java/carpet/commands/ScriptCommand.java | 2 +-
.../java/carpet/{helpers => script/utils}/ScriptDownloader.java | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
rename src/main/java/carpet/{helpers => script/utils}/ScriptDownloader.java (99%)
diff --git a/src/main/java/carpet/commands/ScriptCommand.java b/src/main/java/carpet/commands/ScriptCommand.java
index dfce4b6a..ecf8e930 100644
--- a/src/main/java/carpet/commands/ScriptCommand.java
+++ b/src/main/java/carpet/commands/ScriptCommand.java
@@ -2,7 +2,7 @@ package carpet.commands;
import carpet.CarpetServer;
import carpet.CarpetSettings;
-import carpet.helpers.ScriptDownloader;
+import carpet.script.utils.ScriptDownloader;
import carpet.script.CarpetEventServer;
import carpet.script.CarpetExpression;
import carpet.script.CarpetScriptHost;
diff --git a/src/main/java/carpet/helpers/ScriptDownloader.java b/src/main/java/carpet/script/utils/ScriptDownloader.java
similarity index 99%
rename from src/main/java/carpet/helpers/ScriptDownloader.java
rename to src/main/java/carpet/script/utils/ScriptDownloader.java
index 2aa43d89..b5c9fadf 100644
--- a/src/main/java/carpet/helpers/ScriptDownloader.java
+++ b/src/main/java/carpet/script/utils/ScriptDownloader.java
@@ -1,4 +1,4 @@
-package carpet.helpers;
+package carpet.script.utils;
import carpet.utils.Messenger;
import com.google.gson.JsonArray;
From 4466c7778025a1d537d5424aa794a5ecd8bf7ee6 Mon Sep 17 00:00:00 2001
From: gnembon
Date: Fri, 21 May 2021 03:50:38 -0300
Subject: [PATCH 3/6] small refactor, customizable appstore, minimize app
calls.
---
src/main/java/carpet/CarpetSettings.java | 13 +
.../java/carpet/commands/ScriptCommand.java | 33 +-
.../carpet/script/utils/ScriptDownloader.java | 388 +++++++++++-------
3 files changed, 278 insertions(+), 156 deletions(-)
diff --git a/src/main/java/carpet/CarpetSettings.java b/src/main/java/carpet/CarpetSettings.java
index 69a4d5a7..c1752319 100644
--- a/src/main/java/carpet/CarpetSettings.java
+++ b/src/main/java/carpet/CarpetSettings.java
@@ -1,5 +1,6 @@
package carpet;
+import carpet.script.utils.ScriptDownloader;
import carpet.settings.ParsedRule;
import carpet.settings.Rule;
import carpet.settings.SettingsManager;
@@ -455,6 +456,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 //contents/"
+ },
+ 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";
diff --git a/src/main/java/carpet/commands/ScriptCommand.java b/src/main/java/carpet/commands/ScriptCommand.java
index ecf8e930..69461c77 100644
--- a/src/main/java/carpet/commands/ScriptCommand.java
+++ b/src/main/java/carpet/commands/ScriptCommand.java
@@ -2,6 +2,7 @@ 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;
@@ -21,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;
@@ -32,13 +34,19 @@ import net.minecraft.command.argument.BlockStateArgumentType;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.util.Clearable;
-import net.minecraft.util.Pair;
import net.minecraft.util.math.BlockPos;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.BlockBox;
import org.apache.commons.lang3.StringUtils;
-import java.util.*;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.TreeSet;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import java.util.function.Supplier;
@@ -57,7 +65,6 @@ public class ScriptCommand
Set allFunctions = (new CarpetExpression(null, "null", null, null)).getExpr().getFunctionNames();
scarpetFunctions = new TreeSet<>(Expression.none.getFunctionNames());
APIFunctions = allFunctions.stream().filter(s -> !scarpetFunctions.contains(s)).collect(Collectors.toCollection(TreeSet::new));
- ScriptDownloader.updateLocalRepoStructure();
}
public static List suggestFunctions(ScriptHost host, String previous, String prefix)
@@ -109,14 +116,18 @@ public class ScriptCommand
private static CompletableFuture suggestDownloadableApps(
CommandContext context,
SuggestionsBuilder suggestionsBuilder
- ) {
- String previous = suggestionsBuilder.getRemaining();
- Pair> suggestions = ScriptDownloader.fileNamesFromPath(previous);
- String currentValidPath = suggestions.getLeft();
-
- suggestions.getRight().forEach(s -> suggestionsBuilder.suggest(currentValidPath + s));
-
- return suggestionsBuilder.buildFuture();
+ ) throws CommandSyntaxException {
+ try {
+ String previous = suggestionsBuilder.getRemaining();
+ List 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 dispatcher)
diff --git a/src/main/java/carpet/script/utils/ScriptDownloader.java b/src/main/java/carpet/script/utils/ScriptDownloader.java
index b5c9fadf..0ac6775e 100644
--- a/src/main/java/carpet/script/utils/ScriptDownloader.java
+++ b/src/main/java/carpet/script/utils/ScriptDownloader.java
@@ -1,18 +1,19 @@
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.api.EnvType;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.command.CommandException;
import net.minecraft.server.command.ServerCommandSource;
-import net.minecraft.text.LiteralText;
-import net.minecraft.util.Pair;
import net.minecraft.util.WorldSavePath;
+import org.apache.commons.lang3.tuple.Pair;
import java.io.BufferedReader;
import java.io.FileWriter;
@@ -28,29 +29,195 @@ 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.Set;
+import java.util.stream.Collectors;
+
/**
* A class used to save scarpet app store scripts to disk
*/
-
-@SuppressWarnings("rawtypes")
-public class ScriptDownloader {
-
+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. Therefore, we save the file structure at the beginning
- * of the server's starting, and update it with {@link ScriptDownloader#updateLocalRepoStructure()} if we get an error
- * in the query, thereby allowing us to re-enter the command and get the correct app (unless of course the inputted
- * path is itself wrong)
+ * {@code /script download} command and getting the suggestions.
*/
- public static Map localScarpetRepoStructure = new HashMap<>();
+ public static final PathNode appStoreRoot = PathNode.folder(null, "");
- /** This is the link to the scarpet app repo from the github api, so if that ever changes then this variable would
- * have to change as well to match.
+ /** This is the base link to the scarpet app repo from the github api.
*/
- private static final String scarpetRepoLink = "https://api.github.com/repos/gnembon/scarpet/contents/programs/";
+ private static String scarpetRepoLink = "https://api.github.com/repos/gnembon/scarpet/contents/programs/";
+
+ public static class ScarpetAppStoreValidator extends Validator
+ {
+ @Override public String validate(ServerCommandSource source, ParsedRule 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 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 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 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.
@@ -60,62 +227,72 @@ public class ScriptDownloader {
* @return {@code 1} if we succesfully saved the script, {@code 0} otherwise
*/
- public static int downloadScript(CommandContext cc, String path, boolean global){
- String code = getScriptCode(path);
- return saveScriptToFile(path, code, cc, global);
+ public static int downloadScript(CommandContext cc, String path, boolean global)
+ {
+ Pair code = getScriptCode(path);
+ return saveScriptToFile(path, code.getLeft(), code.getRight(), cc, global);
}
- /** Gets the code once the user inputs the command. The code isn't saved in {@link ScriptDownloader#localScarpetRepoStructure}
- * as the scarpet repo is very large and may get much larger in the future, and that may cause RAM issues if we have
- * the entire thing saved in memory.
+ /** Gets the code once the user inputs the command.
*
- * @param path The user inputted path to the scarpet script
- * @return the HTML request from the path program using {@link ScriptDownloader#getStringFromStream} method to convert
- * HTML response into a string
+ * @param appPath The user inputted path to the scarpet script
+ * @return Pair of app file name and content
*/
- public static String getScriptCode(String path){
- try {
- String link = "https://raw.githubusercontent.com/gnembon/scarpet/master/programs/"+ path.replace(" ", "%20");
- URL appURL = new URL(link);
+ public static Pair 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 getStringFromStream((InputStream) http.getContent());
- } catch (IOException e){//todo add checks to distinguish between incorrect file path or change in scarpet repo structure
- throw new CommandException(new LiteralText("'"+ path + "' is not a valid path to a scarpet app"));
+ 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 name, String code, CommandContext cc, boolean globalSavePath){
- Path scriptLocation;
- String location;
- if(globalSavePath && FabricLoader.getInstance().getEnvironmentType()== EnvType.CLIENT) {//cos config folder only is in clients
- scriptLocation = FabricLoader.getInstance().getConfigDir().resolve("carpet/scripts/appstore");
- location = "global script config folder";
- } else {
- scriptLocation = cc.getSource().getMinecraftServer().getSavePath(WorldSavePath.ROOT).resolve("scripts/appstore");
- location = "world scripts folder";
+ public static int saveScriptToFile(String path, String appFileName, String code, CommandContext 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";
}
- try {
- String scriptPath = scriptLocation.toAbsolutePath() + "/" + name;
- scriptLocation = Paths.get(scriptPath);
+ 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)) {
+ if (!Files.exists(scriptFolderLocation))
Files.createDirectories(scriptFolderLocation);
- }
- if(Files.exists(scriptLocation)){
- Messenger.m(cc.getSource(), String.format("gi Note: overwriting existing file '%s'", name));
- }
+ 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 " + location);
+
+ Messenger.m(cc.getSource(), "gi Placing script in " + message);
FileWriter fileWriter = new FileWriter(scriptPath);
fileWriter.write(code);
fileWriter.close();
- } catch (IOException e) {
+ }
+ catch (IOException e)
+ {
Messenger.m(cc.getSource(), "r Error in downloading script");
return 0;
}
- Messenger.m(cc.getSource(), "gi Successfully created "+ name + " in " + location);
+ Messenger.m(cc.getSource(), "gi Successfully created "+ appFileName + " in " + message);
return 1;
}
@@ -126,109 +303,30 @@ public class ScriptDownloader {
* @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) {
+ public static String getStringFromStream(InputStream inputStream) throws IOException
+ {
+ if (inputStream != null)
+ {
Writer stringWriter = new StringWriter();
char[] charBuffer = new char[2048];
- try{
+ try
+ {
Reader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
int counter;
- while ((counter = reader.read(charBuffer)) != -1) {
+ while ((counter = reader.read(charBuffer)) != -1)
stringWriter.write(charBuffer, 0, counter);
- }
- } finally {
+
+ }
+ finally
+ {
inputStream.close();
}
return stringWriter.toString();
- } else {
+ }
+ else
+ {
return "";
}
}
-
- /** Updates local copy of scarpet repo, giving an error otherwise which ought to be reported to fabric-carpet devs,
- * in particular Ghoulboy if possible as he wrote this code.
- *
- * @author Ghoulboy
- */
-
- public static void updateLocalRepoStructure(){
- try{
- localScarpetRepoStructure = updateLocalRepoStructure("");
- } catch (IOException | IllegalStateException exc){//should not happen as long as repo name stays the same
- System.out.println("ERROR: ScriptDownloader#scarpetRepoLink variable is out of date, please update your carpet version, and if the problem persists please submit a bug report here: https://github.com/gnembon/fabric-carpet/issues/new");
- throw new CommandException(new LiteralText("Internal scarpet app store repo structure changed, please contact fabric-carpet developers, enclosing a copy of the server log!"));
- }
- }
-
- /** A DFS to save the file structure (not the code) in the scarpet repo, called at server start and whenever running
- * the command {@code /script download} command causes an oopsie, so the internal scarpet repo structure is changed
- * and we therefore need to update it. We don't save the code here in case it needs updating from the last time, and
- * also not to slow down the search for
- *
- * @param currentPath The current path down which we are looking for code.
- * @return The scarpet repo structure as a map which you navigate down, so you don't need to query to github API
- * multiple times
- * @throws IOException only if {@link ScriptDownloader#scarpetRepoLink} structure is out of date, i.e scarpet internal
- * repo structure has changed
- */
-
- public static Map updateLocalRepoStructure(String currentPath) throws IOException{
-
- Map ret = new HashMap<>();
-
- String queryPath = scarpetRepoLink + currentPath;
-
- URL appURL = new URL(queryPath);
-
- String response = ScriptDownloader.getStringFromStream(appURL.openStream());
-
- JsonArray files = new JsonParser().parse(response).getAsJsonArray();
-
- for(JsonElement je : files){
- JsonObject jo = je.getAsJsonObject();
- String name = jo.get("name").getAsString();
- String filePath = jo.get("path").getAsString().substring(9).replace(" ", "%20");
-
- if (jo.get("type").getAsString().equals("dir") && !name.contains("shared")) {//cos that may be a directory for shared data, so we don't wanna search there
- ret.put(name, updateLocalRepoStructure(filePath));
- } else if (name.matches("(\\w+\\.scl?)")){
- ret.put(name, null);
- }
- }
-
- return ret;
- }
-
- /** 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 Pair> fileNamesFromPath(String currentPath){
- String[] path = currentPath.split("/");
-
- Map currentLookedAtFiles = localScarpetRepoStructure;
- StringBuilder currentValidPath = new StringBuilder();
-
-
- for(String folder : path){
- if(!currentLookedAtFiles.containsKey(folder) || folder.matches(".+\\.sc"))
- return new Pair<>(currentValidPath.toString(), currentLookedAtFiles.keySet());
-
- currentValidPath.append(folder).append("/");
-
-
- Map newFolder = currentLookedAtFiles.get(folder);
-
- if(newFolder == null)
- return new Pair<>(currentValidPath.toString(), currentLookedAtFiles.keySet());
-
- currentLookedAtFiles = newFolder;
- }
-
- return new Pair<>(currentValidPath.toString(), currentLookedAtFiles.keySet());
- }
}
From 7fd49e0f043dee3cfe416ff1015a32e37694c348 Mon Sep 17 00:00:00 2001
From: gnembon
Date: Fri, 21 May 2021 04:22:18 -0300
Subject: [PATCH 4/6] fix client methods in biome, fixes #870
---
.../carpet/fakes/BiomeEffectsInterface.java | 12 +++++
.../mixins/BiomeEffects_scarpetMixin.java | 53 +++++++++++++++++++
.../java/carpet/script/utils/BiomeInfo.java | 20 +++++--
src/main/resources/carpet.mixins.json | 1 +
4 files changed, 81 insertions(+), 5 deletions(-)
create mode 100644 src/main/java/carpet/fakes/BiomeEffectsInterface.java
create mode 100644 src/main/java/carpet/mixins/BiomeEffects_scarpetMixin.java
diff --git a/src/main/java/carpet/fakes/BiomeEffectsInterface.java b/src/main/java/carpet/fakes/BiomeEffectsInterface.java
new file mode 100644
index 00000000..c3a5acc1
--- /dev/null
+++ b/src/main/java/carpet/fakes/BiomeEffectsInterface.java
@@ -0,0 +1,12 @@
+package carpet.fakes;
+
+import java.util.Optional;
+
+public interface BiomeEffectsInterface
+{
+ int getCMFogColor();
+ Optional getCMFoliageColor();
+ int getCMSkyColor();
+ int getCMWaterColor();
+ int getCMWaterFogColor();
+}
diff --git a/src/main/java/carpet/mixins/BiomeEffects_scarpetMixin.java b/src/main/java/carpet/mixins/BiomeEffects_scarpetMixin.java
new file mode 100644
index 00000000..e8291304
--- /dev/null
+++ b/src/main/java/carpet/mixins/BiomeEffects_scarpetMixin.java
@@ -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 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 getCMFoliageColor()
+ {
+ return foliageColor;
+ }
+
+ @Override
+ public int getCMSkyColor()
+ {
+ return skyColor;
+ }
+
+ @Override
+ public int getCMWaterColor()
+ {
+ return waterColor;
+ }
+
+ @Override
+ public int getCMWaterFogColor()
+ {
+ return waterFogColor;
+ }
+}
diff --git a/src/main/java/carpet/script/utils/BiomeInfo.java b/src/main/java/carpet/script/utils/BiomeInfo.java
index 9ff63005..30a003b9 100644
--- a/src/main/java/carpet/script/utils/BiomeInfo.java
+++ b/src/main/java/carpet/script/utils/BiomeInfo.java
@@ -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> biomeFeatures = new HashMap>(){{
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()));
diff --git a/src/main/resources/carpet.mixins.json b/src/main/resources/carpet.mixins.json
index 02262198..126de55c 100644
--- a/src/main/resources/carpet.mixins.json
+++ b/src/main/resources/carpet.mixins.json
@@ -175,6 +175,7 @@
"Scoreboard_scarpetMixin",
"TradeOutputSlot_scarpetEventMixin",
+ "BiomeEffects_scarpetMixin",
"ChunkTicketManager_scarpetMixin",
"StatType_scarpetMixin",
"PointOfInterest_scarpetMixin",
From cc7a9c01be2e4e0466062983f6f73f57c5a21523 Mon Sep 17 00:00:00 2001
From: altrisi
Date: Fri, 21 May 2021 06:33:00 +0200
Subject: [PATCH 5/6] Annotated Scarpet (#772)
* Progress by now.
* Output conversions plus a few extra random things
* More progress, mostly list converting and a way to get converters
* Apply ValueConverters, refactor valueconverter getter to
accept types with annotations and generics, made listconverter pass
those params and annotations recursively. And probably more things.
* Some extra docs, the 100% most important thing
* Added MapConverter, strict Strings and Texts, some docs additions
and commented out heuristics
* Rework StrictParam to use SimpleTypeConverters and be saved in a map
* Minor changes and docs & added non-fully-functional context "converter"
* Added support for variable value count converters, multi-value eaters &
0 value eaters (Context).
* Docs update, enable Context converter, fix minor TODOs
* Move (most) parameter annotations into a single class,
remove deprecated ValueCaster#cast, update some docs.
* Small refactor of Param.Strict registering, added boolean to it
* Added PairConverter, moved Context and LV ValueConverters to Params
* Document public methods of AnnotationParser
* Add a lot more docs, make it ready for the first preview PR
* Move ValueConverter population outside of makeFunction,
check lv size for > max arguments, minor doc updates and workaround for
Mercury#28 until released
* Added optional parameters (declared using Java's Optional),
made evalAndConvert check for the presence of the next iteration,
updated test class
* Generalize Value OutputConverter
* ListConverter and MapConverter now follow ValueConverter contract of
incorrect value handling, returning a complete null for an incorrect
value anywhere. Also small fix to OptionalConverter and docs.
Getting ready for checking null in AnnotationParser!
* Change reflection's #invoke to a MethodHandle to boost speed by a lot
Commented
* Dedupe code, update and remove outdated todos, add 2 new ValueCasters
* Merge lambdas in display_title2 test function to be more accurate
* Check nulls somehow, I guess
* Move base function logic into a class to stop
passing hundreds of parameters into static functions and therefore simplify it
* Reduce visibility and fix a bug with PairConverter
* Hide Context provider in usage report
* Add support for primitive varargs
* Added TheLazyT converter
* Drop #getPrefixedTypeName and use 'something' as name of values
* Use value again because of varargs, document that
* Remove a completed TODO
* Remove old disabled heuristic value type name getter
* Fix TheLazyT
* Preparing locators before touching current Carpet infrastructure
* Add notes about non-implementedness of remaining locators
* Added APIs to register custom ValueConverters
* Mention new APIs in javadocs
* Adapt to new Carpet changes and deprecated non-implemented annotations
* Update example function for display_title()
* Better implementation for player_list example
* Fix example error message
* FunctionArgument no longer has generics
* Modernize Text converters
* OutputConverter is final, plus other small changes
* Few non-behavioural changes
* Rename LazyFunction to ScarpetFunction
* Drop LazyValue in parameters, not in return
Updating docs was a nightmare
* CARPET CHANGE - Make Fluff.AbstractLazyFunction.unpackArgs call unpackLazy (was duped)
* Make errors more like current Scarpet errors
* Allow customizing ContextType when evaluating arguments
* Add a few missing docs, comment out outdated ValueCaster doc
It's been a while, wow
* Fix for new Carpet signature :irritatered:
Please don't break API methods! Extensions may be using them, and they
will just crash the game or behave in unexpected ways else!
* Format all code to Carpet's. Powered by Eclipse
Reviewed to be sure it's ok. No functional changes, except from a small correction
* Always use full name for Context.Type
* Add parsing for all current Scarpet api and language classes
And remove test functions
* Allow void return type
* Fix no param methods
Whoops!
* Remove outdated comment and fix small javadoc linting problem
* Drop Param.TheLazyT in favour of just Context.Type type
And small performance improvement to varargs varargs since fastutil's toArray is crap
* Quote functions in error messages
* Make SimpleTypeConverter independent of ValueCaster
'Real world' usage demonstrated it can be annoying else, mostly for wrapper only values where you just want the wrapped object
---
src/main/java/carpet/CarpetServer.java | 1 +
.../java/carpet/script/CarpetExpression.java | 2 +
.../carpet/script/CarpetScriptServer.java | 35 ++
src/main/java/carpet/script/Fluff.java | 10 +-
.../script/annotation/AnnotationParser.java | 320 +++++++++++++++++
.../script/annotation/FunctionsTestClass.java | 60 ++++
.../script/annotation/ListConverter.java | 90 +++++
.../carpet/script/annotation/Locator.java | 270 +++++++++++++++
.../script/annotation/MapConverter.java | 163 +++++++++
.../script/annotation/OptionalConverter.java | 107 ++++++
.../script/annotation/OutputConverter.java | 129 +++++++
.../java/carpet/script/annotation/Param.java | 323 ++++++++++++++++++
.../script/annotation/ScarpetFunction.java | 85 +++++
.../annotation/SimpleTypeConverter.java | 112 ++++++
.../carpet/script/annotation/ValueCaster.java | 101 ++++++
.../script/annotation/ValueConverter.java | 177 ++++++++++
16 files changed, 1976 insertions(+), 9 deletions(-)
create mode 100644 src/main/java/carpet/script/annotation/AnnotationParser.java
create mode 100644 src/main/java/carpet/script/annotation/FunctionsTestClass.java
create mode 100644 src/main/java/carpet/script/annotation/ListConverter.java
create mode 100644 src/main/java/carpet/script/annotation/Locator.java
create mode 100644 src/main/java/carpet/script/annotation/MapConverter.java
create mode 100644 src/main/java/carpet/script/annotation/OptionalConverter.java
create mode 100644 src/main/java/carpet/script/annotation/OutputConverter.java
create mode 100644 src/main/java/carpet/script/annotation/Param.java
create mode 100644 src/main/java/carpet/script/annotation/ScarpetFunction.java
create mode 100644 src/main/java/carpet/script/annotation/SimpleTypeConverter.java
create mode 100644 src/main/java/carpet/script/annotation/ValueCaster.java
create mode 100644 src/main/java/carpet/script/annotation/ValueConverter.java
diff --git a/src/main/java/carpet/CarpetServer.java b/src/main/java/carpet/CarpetServer.java
index ae9e4dce..126d8d85 100644
--- a/src/main/java/carpet/CarpetServer.java
+++ b/src/main/java/carpet/CarpetServer.java
@@ -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)
diff --git a/src/main/java/carpet/script/CarpetExpression.java b/src/main/java/carpet/script/CarpetExpression.java
index 94c3cf10..67496d99 100644
--- a/src/main/java/carpet/script/CarpetExpression.java
+++ b/src/main/java/carpet/script/CarpetExpression.java
@@ -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));
}
diff --git a/src/main/java/carpet/script/CarpetScriptServer.java b/src/main/java/carpet/script/CarpetScriptServer.java
index 92185750..5f056e98 100644
--- a/src/main/java/carpet/script/CarpetScriptServer.java
+++ b/src/main/java/carpet/script/CarpetScriptServer.java
@@ -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);
+ }
}
diff --git a/src/main/java/carpet/script/Fluff.java b/src/main/java/carpet/script/Fluff.java
index d3d64281..26d281e5 100644
--- a/src/main/java/carpet/script/Fluff.java
+++ b/src/main/java/carpet/script/Fluff.java
@@ -125,15 +125,7 @@ public abstract class Fluff
public List unpackArgs(List lzargs, Context c, Context.Type contextType)
{
- List 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 args = unpackLazy(lzargs, c, contextType);
if (!numParamsVaries() && getNumParams() != args.size())
throw new InternalExpressionException("Function " + getName() + " expected " + getNumParams() + " parameters, got " + args.size());
return args;
diff --git a/src/main/java/carpet/script/annotation/AnnotationParser.java b/src/main/java/carpet/script/annotation/AnnotationParser.java
new file mode 100644
index 00000000..aa605526
--- /dev/null
+++ b/src/main/java/carpet/script/annotation/AnnotationParser.java
@@ -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;
+
+/**
+ *
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.
+ *
+ *
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.
+ *
+ *
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.
+ *
+ *
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
+ *
+ *
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.
+ *
+ *
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.
+ *
+ *
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.
+ *
+ *
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.
+ *
+ *
To begin, use the {@link #parseFunctionClass(Class)} method in a class with methods annotated with the {@link ScarpetFunction} annotation.
Parses a given {@link Class} and registers its annotated methods, the ones with the {@link ScarpetFunction} annotation,
+ * to be used in the Scarpet language.
+ *
+ *
Only call this method once per class per lifetime of the JVM! (for example, at {@link CarpetExtension#onGameStarted()})
+ *
+ *
There is a set of requirements for the class and its methods:
+ *
+ *
Class must be concrete. That is, no interfaces or abstract classes should be passed
+ *
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.
+ *
Annotated methods must not be {@code static}. See claim above
+ *
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.
+ *
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()}.
+ *
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}
+ *
+ *
+ * @see ScarpetFunction
+ * @param The generic type of the class to parse.
+ * @param clazz The class to parse
+ */
+ public static void parseFunctionClass(Class 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);
+ }
+ }
+
+ /**
+ *
Adds all parsed functions to the given {@link Expression}.
+ *
This is handled automatically by Carpet
+ *
+ * @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, LazyValue>
+ {
+ private final String name;
+ private final boolean isMethodVarArgs;
+ private final int methodParamCount;
+ private final List> valueConverters;
+ private final Class> varArgsType;
+ private final boolean primitiveVarArgs;
+ private final ValueConverter> varArgsConverter;
+ private final OutputConverter
*
*
Types to be used in those functions must be already registered in their respective {@link ValueConverter} implementations.
- * In order to register a new type to convert to, you can do so in {@link SimpleTypeConverter#registerType(Class, Class, java.util.function.Function)},
+ * 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)}.
* In order to convert the output of your method to a {@link LazyValue} you will also need to register its conversion in {@link OutputConverter}
{@link SimpleTypeConverter}s are reused whenever asked for one, since they don't have any complexity.
*
- * @see #registerType(Class, Class, Function)
+ * @see #registerType(Class, Class, Function, String)
*
* @param The type of the required input {@link Value}
* @param The type that this converter converts to
@@ -53,7 +53,7 @@ public final class SimpleTypeConverter implements ValueConve
*
The default constructor for {@link SimpleTypeConverter}.
*
*
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}
Use {@link #registerType(Class, Class, Function)} for
+ * registry, and it's not intended way to register new {@link SimpleTypeConverter}
Use {@link #registerType(Class, Class, Function, String)} for
* that.
*
* @param inputType The required type for the input {@link Value}