Merge branch 'master' into 1.17

# Conflicts:
#	src/main/java/carpet/script/utils/AppStoreManager.java
This commit is contained in:
gnembon committed 2021-06-30 13:33:10 -03:00
commit 4f77327b6c
7 files changed
+194 -83

No files matched your search

+38 -18
View File
@@ -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
+30 -13
View File
@@ -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 `/<app>` command, see below.
@@ -314,6 +330,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.
@@ -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
+2
View File
@@ -845,6 +845,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;
@@ -860,6 +861,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;
@@ -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<Suggestions> suggestDownloadableApps(
@@ -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 NbtElement 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;
}
@@ -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<String> 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<String> 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<String, String, StoreNode> nodeInfo = getFileNode(path);
return downloadScript(source, path, nodeInfo, false);
}
private static int downloadScript(ServerCommandSource source, String path, Triple<String, String, StoreNode> 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<String, String, StoreNode> getFileNode(String appPath)
{
return getFileNodeFrom(appStoreRoot, appPath);
return getFileNodeFrom(APP_STORE_ROOT, appPath);
}
public static Triple<String, String, StoreNode> 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.getServer().isDedicated())
public static boolean saveScriptToFile(ServerCommandSource source, String path, String appFileName, String code, boolean globalSavePath, boolean useTrash)
{
Path scriptLocation;
if (globalSavePath && !source.getServer().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.getServer().getSavePath(WorldSavePath.ROOT).resolve("scripts").toAbsolutePath()+"/"+appFileName;
location = "world scripts folder";
scriptLocation = source.getServer().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;
@@ -349,41 +366,31 @@ public class AppStoreManager
}
}
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<String, Value> 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<String, String, StoreNode> nodeInfo = getFileNode(source);
contentUrl = nodeInfo.getMiddle();
}
else if (type.equalsIgnoreCase("app"))
{
Triple<String, String, StoreNode> 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<String, String> 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);
}
}