From 91de6ec3079c7981ffd04952f47e6312b7d17419 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 29 Jun 2021 18:00:40 +0000 Subject: [PATCH 1/4] Merge docs for 'Carpet Mod 1.4.41 for Minecraft 1.17.1 and 1.16.5' --- docs/scarpet/Full.md | 56 ++++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/docs/scarpet/Full.md b/docs/scarpet/Full.md index 9bd20fcd..b1ea600a 100644 --- a/docs/scarpet/Full.md +++ b/docs/scarpet/Full.md @@ -996,7 +996,7 @@ indicating if the given seed has been used or not. returns a noise value from `0.0` to `1.0` (roughly) for 1, 2 or 3 dimensional coordinate. The default seed it samples from is `0`, but seed can be specified as a 4th argument as well. In case you need 1D or 2D noise values with custom -seed, use `null` for `z`, or `y` and `z` arguments respectively. +seed, use `null` for `y` and `z`, or `z` arguments respectively. Perlin noise is based on a square grid and generates rougher maps comparing to Simplex, which is creamier. Querying for lower-dimensional result, rather than affixing unused dimensions to constants has a speed benefit, @@ -1680,8 +1680,7 @@ equivalent to `has(get(foo,a), b)` or `has(foo, a, b)` ### `delete(container, address, ...), delete(lvalue)` Removes specific entry from the container. For the lists - removes the element and shrinks it. For maps, it -removes the key from the map, and for nbt - removes content from a given path. For lists and maps returns previous -entry at the address, for nbt's - number of removed objects, with 0 indicating that the original value was unaffected. +removes the key from the map, and for nbt - removes content from a given path. Like with the `get` and `has`, `delete` can accept chained addresses, as well as l-value container access, removing the value from the leaf of the path provided, so `delete(foo, a, b)` is the @@ -2098,7 +2097,7 @@ baz(entities) -> // same thing ``` It works similarly to the auto command, but arguments get their inferred types based on the argument -names, looking at the full name, or the suffix after the last `_` that indicates the variable type. For instance, variable named `float` will +names, looking at the full name, or any suffix when splitting on `_` that indicates the variable type. For instance, variable named `float` will be parsed as a floating point number, but it can be named `'first_float'` or `'other_float'` as well. Any variable that is not supported, will be parsed as a `'string'` type. @@ -2150,7 +2149,7 @@ paths with functions to execute, and optionally, custom argument types. Commands the key (can be empty) consists of the execution path with the command syntax, which consists of literals (as is) and arguments (wrapped with `<>`), with the name / suffix of the name of the attribute indicating its type, and the value represent function to call, either function values, -defined function names, or functions with some default arguments. Values extracted from commands will be passed to the +defined function names, or functions with some default arguments. Argument names need to be unique for each command. Values extracted from commands will be passed to the functions and executed. By default, command list will be checked for ambiguities (commands with the same path up to some point that further use different attributes), causing app loading error if that happens, however this can be suppressed by specifying `'allow_command_conflicts'`. @@ -2163,8 +2162,14 @@ and less frequently used features, like forks and redirects, used pretty much on ### Command argument types -There are several default argument types that can be used directly without specifying custom types. Each argument can be -customized in the `'arguments'` section of the app config, specifying its base type, via `'type'` that needs +Argument types differ from actual argument names that the types are the suffixes of the used argument names, when separated with +`'_'` symbol. For example argument name `'from_pos'` will be interpreted as a built-in type `'int'` and provided to the command system +as a name `'from_pos'`, however if you define a custom type `'from_pos'`, your custom type will be used instead. +Longer suffixes take priority over shorter prefixes, then user defined suffixes mask build-in prefixes. + +There are several default argument types that can be used directly without specifying custom types. + +Each argument can be customized in the `'arguments'` section of the app config, specifying its base type, via `'type'` that needs to match any of the built-in types, with a series of optional modifiers. Shared modifiers include: * `suggest` - static list of suggestions to show above the command while typing * `suggester` - function taking one map argument, indicating current state of attributes in the parsed command @@ -3960,7 +3965,10 @@ In case you want to pass an event handler that is not defined in your module, pl ### `entity_load_handler(descriptor / descriptors, function)`, `entity_load_handler(descriptor / descriptors, call_name, ... args?)` Attaches a callback to when any entity matching the following type / types is loaded in the game, allowing to grab a handle -to an entity right when it is loaded to the world without querying them every tick. Callback expects one parameter - the entity. +to an entity right when it is loaded to the world without querying them every tick. Callback expects two parameters - the entity, +and a boolean indicating if the entity was newly created(`true`) or just loaded from disk. Single argument functions accepting +only entities are allowed, but deprecated and will be removed at some point. + If callback is `null`, then the current entity handler, if present, is removed. Consecutive calls to `entity_load_handler` will add / subtract of the currently targeted entity types pool. @@ -3970,10 +3978,10 @@ that it is not clear which player to use run the load call. ``` // veryfast method of getting rid of all the zombies. Callback is so early, its packets haven't reached yet the clients // so to save on log errors, removal of mobs needs to be scheduled for later. -entity_load_handler('zombie', _(e) -> schedule(0, _(outer(e)) -> modify(e, 'remove'))) +entity_load_handler('zombie', _(e, new) -> schedule(0, _(outer(e)) -> modify(e, 'remove'))) // making all zombies immediately faster and less susceptible to friction of any sort -entity_load_handler('zombie', _(e) -> entity_event(e, 'on_tick', _(e) -> modify(e, 'motion', 1.2*e~'motion'))) +entity_load_handler('zombie', _(e, new) -> entity_event(e, 'on_tick', _(e) -> modify(e, 'motion', 1.2*e~'motion'))) ``` Word of caution: entities can be loaded with chunks in various states, for instance when a chunk is being generated, this means @@ -3981,25 +3989,30 @@ that accessing world blocks would cause the game to freeze due to force generati sure to never assume the chunk is ready and use `entity_load_handler` to schedule actions around the loaded entity, or manipulate entity directly. +Also, it is possible that mobs that spawn with world generation, while being 'added' have their metadata serialized and cached +internally (vanilla limitation), so some modifications to these entities may have no effect on them. This affects mobs created with +world generation. + For instance the following handler is safe, as it only accesses the entity directly. It makes all spawned pigmen jump ``` -/script run entity_load_handler('zombified_piglin', _(e) -> modify(e, 'motion', 0, 1, 0) ) +/script run entity_load_handler('zombified_piglin', _(e, new) -> if(new, modify(e, 'motion', 0, 1, 0)) ) ``` But the following handler, attempting to despawn pigmen that spawn in portals, will cause the game to freeze due to cascading access to blocks that would cause neighbouring chunks to force generate, causing also error messages for all pigmen caused by packets send after entity is removed by script. ``` -/script run entity_load_handler('zombified_piglin', _(e) -> if(block(pos(e))=='nether_portal', modify(e, 'remove') ) ) +/script run entity_load_handler('zombified_piglin', _(e, new) -> if(new && block(pos(e))=='nether_portal', modify(e, 'remove') ) ) ``` Easiest method to circumvent these issues is delay the check, which may or may not cause cascade load to happen, but will definitely break the infinite chain. ``` -/script run entity_load_handler('zombified_piglin', _(e) -> schedule(0, _(outer(e)) -> if(block(pos(e))=='nether_portal', modify(e, 'remove') ) ) ) +/script run entity_load_handler('zombified_piglin', _(e, new) -> if(new, schedule(0, _(outer(e)) -> if(block(pos(e))=='nether_portal', modify(e, 'remove') ) ) ) ) ``` But the best is to perform the check first time the entity will be ticked - giving the game all the time to ensure chunk -is fully loaded and entity processing, removing the tick handler +is fully loaded and entity processing, removing the tick handler: ``` -/script run entity_load_handler('zombified_piglin', _(e) -> entity_event(e, 'on_tick', _(e) -> ( if(block(pos(e))=='nether_portal', modify(e, 'remove')); entity_event(e, 'on_tick', null) ) ) ) +/script run entity_load_handler('zombified_piglin', _(e, new) -> if(new, entity_event(e, 'on_tick', _(e) -> ( if(block(pos(e))=='nether_portal', modify(e, 'remove')); entity_event(e, 'on_tick', null) ) ) ) ) ``` +Looks little convoluted, but that's the safest method to ensure your app won't crash. ### `entity_event(e, event, function)`, `entity_event(e, event, call_name, ... args?)` @@ -4305,10 +4318,17 @@ Duplicate of `tick`, just automatically located in the nether. Use `__on_tick() ### `__on_tick_ender()` (Deprecated) Duplicate of `tick`, just automatically located in the end. Use `__on_tick() -> in_dimension('end', ... ` instead. -### `__on_chunk_generated(x,z)` +### `__on_chunk_generated(x, z)` Called right after a chunk at a given coordinate is full generated. `x` and `z` correspond -to the lowest x and z coords in the chunk. Event may (or may not) work with Optifine installed -at the same time. +to the lowest x and z coords in the chunk. Handling of this event is scheduled as an off-tick task happening after the +chunk is confirmed to be generated and loaded to the game, so +handling of this event is not technically guaranteed, if the game crashes while players are moving for example, and can +be missed on the next start, if the game successfully saves it and fails to execute all remaining tasks. In normal operation +this should not happen, but be warned. + +### `__on_chunk_loaded(x, z)` +Called right after a chunk at a given coordinate is loaded. All newly generated chunks are loaded as well. + `x` and `z` correspond to the lowest x and z coordinates in the chunk. ### `__on_lightning(block, mode)` Triggered right after a lightning strikes. Lightning entity as well as potential horseman trap would From 8613ecf9d8f5556033b5d5ae1bbf3aeeee914cbb Mon Sep 17 00:00:00 2001 From: altrisi Date: Wed, 30 Jun 2021 17:29:59 +0200 Subject: [PATCH 2/4] Fix creativeFlyDrag and creativeFlySpeed not loading from config (#969) --- src/main/java/carpet/CarpetSettings.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/carpet/CarpetSettings.java b/src/main/java/carpet/CarpetSettings.java index 87eeb1c1..a85a745f 100644 --- a/src/main/java/carpet/CarpetSettings.java +++ b/src/main/java/carpet/CarpetSettings.java @@ -820,6 +820,7 @@ public class CarpetSettings "but this also means it will work on vanilla servers as well" }, category = {CREATIVE, CLIENT}, + strict = false, validate = Validator.NONNEGATIVE_NUMBER.class ) public static double creativeFlySpeed = 1.0; @@ -835,6 +836,7 @@ public class CarpetSettings "but this also means it will work on vanilla servers as well" }, category = {CREATIVE, CLIENT}, + strict = false, validate = Validator.PROBABILITY.class ) public static double creativeFlyDrag = 0.09; From d92b412c0a0188f0c61f94593dd26311e26e4148 Mon Sep 17 00:00:00 2001 From: altrisi Date: Wed, 30 Jun 2021 17:30:55 +0200 Subject: [PATCH 3/4] Document `entities` argument type (#968) --- docs/scarpet/api/Overview.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/scarpet/api/Overview.md b/docs/scarpet/api/Overview.md index e4979462..ea8dc9bd 100644 --- a/docs/scarpet/api/Overview.md +++ b/docs/scarpet/api/Overview.md @@ -314,6 +314,7 @@ Here is a list of built-in types, with their return value formats, as well as a * `'dimension'`: string representing a valid dimension in the world. * `'anchor'`: string of `feet` or `eyes`. * `'entitytype'`: string representing a type of entity + * `'entities'`: entity selector, returns a list of entities directly. Can be configured with `'single'` to only accept a single entity (will return the entity instead of a singleton) and with `'players'` to only accept players. * `'floatrange'`: pair of two numbers where one is smaller than the other * `'players'`: returning a list of valid player name string, logged in or not. If configured with `'single'` returns only one player or `null`. * `'intrange'`: same as `'floatrange'`, but requiring integers. From 97e69d347e3a3f189b1923fcd5b34e2ea02032ae Mon Sep 17 00:00:00 2001 From: altrisi Date: Wed, 30 Jun 2021 17:42:24 +0200 Subject: [PATCH 4/4] Change how resource `type` works, allow proper app and library installation (#956) * Change how app store types work, allow proper app and library installation * Add node for resources in dependencies and docs * Fix NPE without `type` * Fix docs trying to change location of app type resource * Separate libraries and apps and add trash for them * Update docs * Create trash directory if it doesn't exist --- docs/scarpet/api/Overview.md | 42 +++-- .../language/FunctionsAndControlFlow.md | 4 + .../java/carpet/commands/ScriptCommand.java | 2 +- .../java/carpet/script/CarpetScriptHost.java | 11 +- .../carpet/script/utils/AppStoreManager.java | 159 ++++++++++++------ 5 files changed, 153 insertions(+), 65 deletions(-) diff --git a/docs/scarpet/api/Overview.md b/docs/scarpet/api/Overview.md index ea8dc9bd..bde44cff 100644 --- a/docs/scarpet/api/Overview.md +++ b/docs/scarpet/api/Overview.md @@ -110,30 +110,46 @@ in a list and contain of map-like resources descriptors, looking like 'resources' -> [ { 'source' -> 'https://raw.githubusercontent.com/gnembon/fabric-carpet/master/src/main/resources/assets/carpet/icon.png', - 'type' -> 'url', 'target' -> 'foo/photos.zip/foo/cm.png', }, { - 'source' -> 'survival/README.md', - 'type' -> 'store', + 'source' -> '/survival/README.md', 'target' -> 'survival_readme.md', 'shared' -> true, }, { - 'source' -> 'carpets.sc', - 'type' -> 'app', - 'target' -> 'apps/flying_carpets.sc', - 'shared' -> true, + 'source' -> 'circle.sc', // Relative path + 'target' -> 'apps/circle.sc', // This won't install the app, use 'libraries' for that }, ] ``` - `source` and `type` indicate resource location: either an arbitrary url (type `'url'`), - absolute location of a file in the app store (type `'store'`), -or a relative location in the same folder as the app in question (type `'app'`). -`'target'` points to the path in app data, or shared app data folder + `source` indicates resource location: either an arbitrary url (starting with `http://` or `https://`), + absolute location of a file in the app store (starting with a slash `/`), +or a relative location in the same folder as the app in question (the relative location directly). +`'target'` points to the path in app data, or shared app data folder. If not specified it will place the app into the main data folder with the name it has. if `'shared'` is specified and `true`. When re-downloading the app, all resources will be re-downloaded as well. -Currently, app resources -are only downloaded when using `/carpet download` command. +Currently, app resources are only downloaded when using `/carpet download` command. +* `libraries` - list of libraries or apps to be downloaded when installing the app from the app store. It needs to be a list of map-like resource +descriptors, like the above `resources` field. + ``` + 'libraries' -> [ + { + 'source' -> '/tutorial/carpets.sc' + }, + { + 'source' -> '/fundamentals/heap.sc', + 'target' -> 'heap-lib.sc' + } + ] + ``` + `source` indicates resource location and must point to a scarpet app or library. It can be either an arbitrary url (starting with `http://` + or `https://`), absolute location of a file in the app store (starting with a slash `/`), or a relative location in the same folder as the app + in question (the relative location directly). + `target` is an optional field indicating the new name of the app. If not specified it will place the app into the main data folder with the name it has. +If the app has relative resources dependencies, Carpet will use the app's path for relatives if the app was loaded from the same app store, or none if the +app was loaded from an external url. +If you need to `import()` from dependencies indicated in this block, make sure to have the `__config()` map before any import that references your +remote dependencies, in order to allow them to be downloaded and initialized before the import is executed. * `'arguments'` - defines custom argument types for legacy commands with `'legacy_command_type_support'` as well as for the custom commands defined with `'commands'`, see below. * `'commands'` - defines custom commands for the app to be executed with `/` command, see below. diff --git a/docs/scarpet/language/FunctionsAndControlFlow.md b/docs/scarpet/language/FunctionsAndControlFlow.md index b99810e4..f42017a8 100644 --- a/docs/scarpet/language/FunctionsAndControlFlow.md +++ b/docs/scarpet/language/FunctionsAndControlFlow.md @@ -141,6 +141,10 @@ but if symbols are used directly in the module body rather than functions, it ma Returns full list of available symbols that could be imported from this module, which can be used to debug import issues, and list contents of libraries. +You can load and import functions from dependencies in a remote app store's source specified in your config's `libraries` block, but make sure +to place your config _before_ the import in order to allow the remote dependency to be downloaded (currently, app resources are only downloaded +when using the `/carpet download` command). + ### `call(function, ? args ...)` calls a user defined function with specified arguments. It is equivalent to calling `function(args...)` directly diff --git a/src/main/java/carpet/commands/ScriptCommand.java b/src/main/java/carpet/commands/ScriptCommand.java index 368dd488..9e861fe0 100644 --- a/src/main/java/carpet/commands/ScriptCommand.java +++ b/src/main/java/carpet/commands/ScriptCommand.java @@ -110,7 +110,7 @@ public class ScriptCommand } /** - * A method to suggest the available scarpet scripts based off of the current player input and {@link AppStoreManager#appStoreRoot} + * A method to suggest the available scarpet scripts based off of the current player input and {@link AppStoreManager#APP_STORE_ROOT} * variable. */ private static CompletableFuture suggestDownloadableApps( diff --git a/src/main/java/carpet/script/CarpetScriptHost.java b/src/main/java/carpet/script/CarpetScriptHost.java index 184d2196..1f953d70 100644 --- a/src/main/java/carpet/script/CarpetScriptHost.java +++ b/src/main/java/carpet/script/CarpetScriptHost.java @@ -70,7 +70,7 @@ import static net.minecraft.server.command.CommandManager.literal; public class CarpetScriptHost extends ScriptHost { private final CarpetScriptServer scriptServer; - ServerCommandSource responsibleSource; + public ServerCommandSource responsibleSource; private Tag globalState; private int saveTimeout; @@ -328,6 +328,15 @@ public class CarpetScriptHost extends ScriptHost AppStoreManager.addResource(this, storeSource, resource); } } + Value libraries = config.get(new StringValue("libraries")); + if (libraries != null) + { + if (!(libraries instanceof ListValue)) throw new InternalExpressionException("App libraries not defined as a list"); + for (Value library : ((ListValue) libraries).getItems()) + { + AppStoreManager.addLibrary(this, storeSource, library); + } + } } appConfig = config; } diff --git a/src/main/java/carpet/script/utils/AppStoreManager.java b/src/main/java/carpet/script/utils/AppStoreManager.java index e418d0a3..db825c8f 100644 --- a/src/main/java/carpet/script/utils/AppStoreManager.java +++ b/src/main/java/carpet/script/utils/AppStoreManager.java @@ -5,11 +5,11 @@ import carpet.script.CarpetScriptHost; import carpet.script.CarpetScriptServer; import carpet.script.exception.InternalExpressionException; import carpet.script.value.MapValue; +import carpet.script.value.StringValue; import carpet.script.value.Value; import carpet.settings.ParsedRule; import carpet.settings.Validator; import carpet.utils.Messenger; -import com.google.common.collect.ImmutableList; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -21,7 +21,7 @@ import net.minecraft.util.WorldSavePath; import org.apache.commons.lang3.tuple.Triple; import java.io.BufferedReader; -import java.io.FileWriter; +import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -32,7 +32,6 @@ 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.nio.file.StandardCopyOption; import java.util.Arrays; import java.util.Collections; @@ -41,7 +40,6 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; - /** * A class used to save scarpet app store scripts to disk */ @@ -50,7 +48,7 @@ public class AppStoreManager /** 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 StoreNode appStoreRoot = StoreNode.folder(null, ""); + public static final StoreNode APP_STORE_ROOT = StoreNode.folder(null, ""); /** This is the base link to the scarpet app repo from the github api. */ @@ -61,8 +59,8 @@ public class AppStoreManager { @Override public String validate(ServerCommandSource source, ParsedRule currentRule, String newValue, String string) { - appStoreRoot.sealed = false; - appStoreRoot.children = new HashMap<>(); + APP_STORE_ROOT.sealed = false; + APP_STORE_ROOT.children = new HashMap<>(); if (newValue.equalsIgnoreCase("none")) { scarpetRepoLink = null; @@ -115,7 +113,7 @@ public class AppStoreManager } private StringBuilder createPrePath() { - return this == appStoreRoot ? new StringBuilder() : parent.createPrePath().append(pathElement()); + return this == APP_STORE_ROOT ? new StringBuilder() : parent.createPrePath().append(pathElement()); } private StoreNode(StoreNode parent, String name) { @@ -212,7 +210,7 @@ public class AppStoreManager public static List suggestionsFromPath(String currentPath) throws IOException { String[] path = currentPath.split("/"); - StoreNode appKiosk = appStoreRoot; + StoreNode appKiosk = APP_STORE_ROOT; for(String pathElement : path) { if (appKiosk.cannotContinueFor(pathElement)) break; @@ -237,6 +235,11 @@ public class AppStoreManager public static int downloadScript(ServerCommandSource source, String path) { Triple nodeInfo = getFileNode(path); + return downloadScript(source, path, nodeInfo, false); + } + + private static int downloadScript(ServerCommandSource source, String path, Triple nodeInfo, boolean useTrash) + { String code; try { @@ -246,7 +249,7 @@ public class AppStoreManager { throw new CommandException(Messenger.c("rb Failed to obtain app file content: "+e.getMessage())); } - if (!saveScriptToFile(source, path, nodeInfo.getLeft(), code, false)) return 0; + if (!saveScriptToFile(source, path, nodeInfo.getLeft(), code, false, useTrash)) return 0; boolean success = CarpetServer.scriptServer.addScriptHost(source, nodeInfo.getLeft().replaceFirst("\\.sc$", ""), null, true, false, false, nodeInfo.getRight()); return success?1:0; } @@ -258,7 +261,7 @@ public class AppStoreManager */ public static Triple getFileNode(String appPath) { - return getFileNodeFrom(appStoreRoot, appPath); + return getFileNodeFrom(APP_STORE_ROOT, appPath); } public static Triple getFileNodeFrom(StoreNode start, String appPath) @@ -280,32 +283,46 @@ public class AppStoreManager } - public static boolean saveScriptToFile(ServerCommandSource source, String path, String appFileName, String code, boolean globalSavePath){ - String scriptPath; - String location; - if(globalSavePath && !source.getMinecraftServer().isDedicated()) + public static boolean saveScriptToFile(ServerCommandSource source, String path, String appFileName, String code, boolean globalSavePath, boolean useTrash) + { + Path scriptLocation; + if (globalSavePath && !source.getMinecraftServer().isDedicated()) // never happens, this is always called with globalSavePath being false { //cos config folder only is in clients - scriptPath = FabricLoader.getInstance().getConfigDir().resolve("carpet/scripts/appstore").toAbsolutePath()+"/"+path; - location = "global script config folder"; + scriptLocation = FabricLoader.getInstance().getConfigDir().resolve("carpet/scripts/appstore").toAbsolutePath().resolve(path); } else { - scriptPath = source.getMinecraftServer().getSavePath(WorldSavePath.ROOT).resolve("scripts").toAbsolutePath()+"/"+appFileName; - location = "world scripts folder"; + scriptLocation = source.getMinecraftServer().getSavePath(WorldSavePath.ROOT).resolve("scripts").toAbsolutePath().resolve(appFileName); } try { - Path scriptLocation = Paths.get(scriptPath); Files.createDirectories(scriptLocation.getParent()); - if(Files.exists(scriptLocation)) - Messenger.m(source, String.format("gi Note: replaced existing app '%s'", appFileName)); - FileWriter fileWriter = new FileWriter(scriptPath); - fileWriter.write(code); - fileWriter.close(); + if (Files.exists(scriptLocation)) + { + if (useTrash) + { + Files.createDirectories(scriptLocation.getParent().resolve("trash")); + Path trashPath = scriptLocation.getParent().resolve("trash").resolve(path); + int i = 0; + while (Files.exists(trashPath)) + { + String[] nameAndExtension = appFileName.split("\\."); + String newFileName = String.format(nameAndExtension[0] + "%02d." + nameAndExtension[1],i); + trashPath = trashPath.getParent().resolve(newFileName); + i++; + } + Files.move(scriptLocation, trashPath); + } + Messenger.m(source, String.format("gi Note: replaced existing app '%s'"+(useTrash ? " (old moved to /trash folder)" : ""), appFileName)); + } + BufferedWriter writer = Files.newBufferedWriter(scriptLocation); + writer.write(code); + writer.close(); } catch (IOException e) { - Messenger.m(source, "r Error in downloading app: "+e.getMessage()); + Messenger.m(source, "r Error while downloading app: " + e.toString()); + CarpetScriptServer.LOG.warn("Error while downloading app", e); return false; } return true; @@ -348,42 +365,32 @@ public class AppStoreManager Files.copy(in, destination, StandardCopyOption.REPLACE_EXISTING); } } + + private static String getFullContentUrl(String original, StoreNode storeSource) + { + if (original.matches("^https?://.*$")) // We've got a full url here: Just use it + return original; + if (original.charAt(0) == '/') // We've got an absolute path: Use app store root + return getFileNode(original.substring(1)).getMiddle(); + return getFileNodeFrom(storeSource, original).getMiddle(); // Relative path: Use download location + } public static void addResource(CarpetScriptHost carpetScriptHost, StoreNode storeSource, Value resource) { if (!(resource instanceof MapValue)) throw new InternalExpressionException("This is not a valid resource map: "+resource.getString()); Map resourceMap = ((MapValue) resource).getMap().entrySet().stream().collect(Collectors.toMap(e -> e.getKey().getString(), Map.Entry::getValue)); - for (String key: ImmutableList.of("source", "target", "type")) - if (!resourceMap.containsKey(key)) throw new InternalExpressionException("Missing '"+key+"' field in resource descriptor: "+resource.getString()); + if (!resourceMap.containsKey("source")) throw new InternalExpressionException("Missing 'source' field in resource descriptor: "+resource.getString()); String source = resourceMap.get("source").getString(); - String target = resourceMap.get("target").getString(); + String contentUrl = getFullContentUrl(source, storeSource); + String target = resourceMap.computeIfAbsent("target", k -> new StringValue(contentUrl.substring(contentUrl.lastIndexOf('/') + 1))).getString(); boolean shared = resourceMap.getOrDefault("shared", Value.FALSE).getBoolean(); - String type = resourceMap.get("type").getString(); - String contentUrl; - if (type.equalsIgnoreCase("url")) - { - contentUrl = source; - } - else if (type.equalsIgnoreCase("store")) - { - Triple nodeInfo = getFileNode(source); - contentUrl = nodeInfo.getMiddle(); - } - else if (type.equalsIgnoreCase("app")) - { - Triple nodeInfo = getFileNodeFrom(storeSource, source); - contentUrl = nodeInfo.getMiddle(); - } - else - { - throw new InternalExpressionException("Incorrect source type, expecting 'url', 'store' or 'app', got "+type); - } + if (!carpetScriptHost.applyActionForResource(target, shared, p -> { try { writeUrlToFile(contentUrl, p); } catch (IOException e) { - throw new InternalExpressionException("Unable to write resource "+target+": "+e.getMessage()); + throw new InternalExpressionException("Unable to write resource "+target+": "+e.toString()); } })) { @@ -391,4 +398,56 @@ public class AppStoreManager } CarpetScriptServer.LOG.info("Downloaded resource "+target+" from "+contentUrl); } + + /** + * Gets a new StoreNode for an app's dependency with proper relativeness. Will be null if it comes from an external URL + * @param originalSource The StoreNode from the container's app + * @param sourceString The string the app specified as source + * @param contentUrl The full content URL, from {@link #getFullContentUrl(String, StoreNode)} + * @return A {@link StoreNode} that can be used in an app that came from the provided source + */ + private static StoreNode getNewStoreNode(StoreNode originalSource, String sourceString, String contentUrl) + { + StoreNode next = originalSource; + if (sourceString == contentUrl) // External URL (check getFullUrlContent) + return null; + if (sourceString.charAt(0) == '/') // Absolute URL + { + next = APP_STORE_ROOT; + sourceString = sourceString.substring(1); + } + String[] dirs = sourceString.split("/"); + try + { + for (int i = 0; i < dirs.length - 1; i++) + next = next.drillDown(dirs[i]); + } + catch (IOException e) + { + return null; // Should never happen, but let's not give a potentially incorrect node just in case + } + return next; + } + + public static void addLibrary(CarpetScriptHost carpetScriptHost, StoreNode storeSource, Value library) { + if (!(library instanceof MapValue)) + throw new InternalExpressionException("This is not a valid library map: "+library.getString()); + Map libraryMap = ((MapValue) library).getMap().entrySet().stream().collect(Collectors.toMap(e -> e.getKey().getString(), e -> e.getValue().getString())); + String source = libraryMap.get("source"); + String contentUrl = getFullContentUrl(source, storeSource); + String target = libraryMap.computeIfAbsent("target", k -> contentUrl.substring(contentUrl.lastIndexOf('/') + 1)); + if (!(contentUrl.endsWith(".sc") || contentUrl.endsWith(".scl"))) + throw new InternalExpressionException("App resource type must download a scarpet app or library."); + if (target.indexOf('/') != -1) + throw new InternalExpressionException("App resource tried to leave script reserved space"); + try + { + downloadScript(carpetScriptHost.responsibleSource, target, Triple.of(target, contentUrl, getNewStoreNode(storeSource, source, contentUrl)), true); + } + catch (CommandException e) + { + throw new InternalExpressionException("Error when installing app dependencies: " + e.toString()); + } + CarpetScriptServer.LOG.info("Downloaded app "+target+" from "+contentUrl); + } }