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
This commit is contained in:
1 parent
f56b22be6a
commit
5f16ffd933
3 files changed
+267
-8
No files matched your search
@@ -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 <global or local?> <path>` 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.
|
||||
@@ -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<String> 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<String> 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<Suggestions> suggestDownloadableApps(
|
||||
CommandContext<ServerCommandSource> context,
|
||||
SuggestionsBuilder suggestionsBuilder
|
||||
) {
|
||||
String previous = suggestionsBuilder.getRemaining();
|
||||
Pair<String, Set<String>> suggestions = ScriptDownloader.fileNamesFromPath(previous);
|
||||
String currentValidPath = suggestions.getLeft();
|
||||
|
||||
suggestions.getRight().forEach(s -> suggestionsBuilder.suggest(currentValidPath + s));
|
||||
|
||||
return suggestionsBuilder.buildFuture();
|
||||
}
|
||||
|
||||
public static void register(CommandDispatcher<ServerCommandSource> dispatcher)
|
||||
{
|
||||
LiteralArgumentBuilder<ServerCommandSource> b = literal("globals").
|
||||
@@ -302,10 +316,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,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<String, 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<ServerCommandSource> 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<ServerCommandSource> 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 <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 "";
|
||||
}
|
||||
}
|
||||
|
||||
/** 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<String, Map> updateLocalRepoStructure(String currentPath) throws IOException{
|
||||
|
||||
Map<String, 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<String, Set<String>> fileNamesFromPath(String currentPath){
|
||||
String[] path = currentPath.split("/");
|
||||
|
||||
Map<String, 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());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user