From 259bdc2c94a298a2a0705a6e4af406aab53acf01 Mon Sep 17 00:00:00 2001 From: gnembon Date: Wed, 2 Dec 2020 22:36:20 -0400 Subject: [PATCH 1/5] ability to query client brand and server scarpet version. --- docs/scarpet/api/Auxiliary.md | 6 ++++-- docs/scarpet/api/Entities.md | 5 +++++ src/main/java/carpet/network/ServerNetworkHandler.java | 7 +++++++ src/main/java/carpet/script/utils/SystemInfo.java | 10 ++-------- src/main/java/carpet/script/value/EntityValue.java | 9 +++++++++ 5 files changed, 27 insertions(+), 10 deletions(-) diff --git a/docs/scarpet/api/Auxiliary.md b/docs/scarpet/api/Auxiliary.md index e5e9209b..f759b4fd 100644 --- a/docs/scarpet/api/Auxiliary.md +++ b/docs/scarpet/api/Auxiliary.md @@ -500,7 +500,6 @@ Available options in the scarpet app space: * `game_version` - base version of the game Server related properties - * `server_motd` - the motd of the server visible when joining * `server_ip` - IP adress of the game hosted * `server_whitelisted` - boolean indicating whether the access to the server is only for whitelisted players @@ -516,4 +515,7 @@ Available options in the scarpet app space: * `java_version` - version of Java * `java_bits` - number indicating how many bits the Java has, 32 or 64 * `java_system_cpu_load` - current percentage of CPU used by the system - * `java_process_cpu_load` - current percentage of CPU used by JVM \ No newline at end of file + * `java_process_cpu_load` - current percentage of CPU used by JVM + + Scarpet related properties + * `scarpet_version` - returns the version of the carpet your scarpet comes with. \ No newline at end of file diff --git a/docs/scarpet/api/Entities.md b/docs/scarpet/api/Entities.md index a60d426c..e73c0697 100644 --- a/docs/scarpet/api/Entities.md +++ b/docs/scarpet/api/Entities.md @@ -366,6 +366,11 @@ Player's ping in milliseconds, or `null` if its not a player. Player's permission level, or `null` if not applicable for this entity. +### `query(e, 'client_brand')` + +Returns recognized type of client of the client connected. Possible results include `'vanilla'`, or `'carpet '` where +version indicates the version of the connected carpet client. + ### `query(e, 'effect', name?)` Without extra arguments, it returns list of effect active on a living entity. Each entry is a triple of short diff --git a/src/main/java/carpet/network/ServerNetworkHandler.java b/src/main/java/carpet/network/ServerNetworkHandler.java index 5971e93d..72ffc14f 100644 --- a/src/main/java/carpet/network/ServerNetworkHandler.java +++ b/src/main/java/carpet/network/ServerNetworkHandler.java @@ -217,6 +217,13 @@ public class ServerNetworkHandler } + public static String getPlayerStatus(ServerPlayerEntity player) + { + if (remoteCarpetPlayers.containsKey(player)) return "carpet "+remoteCarpetPlayers.get(player); + if (validCarpetPlayers.contains(player)) return "carpet "+CarpetSettings.carpetVersion; + return "vanilla"; + } + private static class DataBuilder { private CompoundTag tag; diff --git a/src/main/java/carpet/script/utils/SystemInfo.java b/src/main/java/carpet/script/utils/SystemInfo.java index 2dc048db..86b13735 100644 --- a/src/main/java/carpet/script/utils/SystemInfo.java +++ b/src/main/java/carpet/script/utils/SystemInfo.java @@ -1,6 +1,7 @@ package carpet.script.utils; import carpet.CarpetServer; +import carpet.CarpetSettings; import carpet.script.CarpetContext; import carpet.script.CarpetScriptHost; import carpet.script.value.ListValue; @@ -123,14 +124,7 @@ public class SystemInfo { }); return carpetRules; }); - - - - - - - - + put("scarpet_version", c -> StringValue.of(CarpetSettings.carpetVersion)); }}; public static Value get(String what, CarpetContext cc) diff --git a/src/main/java/carpet/script/value/EntityValue.java b/src/main/java/carpet/script/value/EntityValue.java index e1e6d897..cb6aa894 100644 --- a/src/main/java/carpet/script/value/EntityValue.java +++ b/src/main/java/carpet/script/value/EntityValue.java @@ -9,6 +9,7 @@ import carpet.fakes.MobEntityInterface; import carpet.fakes.HungerManagerInterface; import carpet.fakes.ServerPlayerInteractionManagerInterface; import carpet.helpers.Tracer; +import carpet.network.ServerNetworkHandler; import carpet.patches.EntityPlayerMPFake; import carpet.script.CarpetContext; import carpet.script.EntityEventsGroup; @@ -579,6 +580,14 @@ public class EntityValue extends Value return Value.NULL; }); + put("client_brand", (e, a) -> { + if (e instanceof ServerPlayerEntity) + { + return StringValue.of(ServerNetworkHandler.getPlayerStatus((ServerPlayerEntity) e)); + } + return Value.NULL; + }); + put("team", (e, a) -> e.getScoreboardTeam()==null?Value.NULL:new StringValue(e.getScoreboardTeam().getName())); put("ping", (e, a) -> { From 6a687177ab809270a3b732a19ca206af400ce5cb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 3 Dec 2020 02:36:49 +0000 Subject: [PATCH 2/5] Merge docs for 'ability to query client brand and server scarpet version.' --- docs/scarpet/Full.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/scarpet/Full.md b/docs/scarpet/Full.md index c2ae997a..eb470336 100644 --- a/docs/scarpet/Full.md +++ b/docs/scarpet/Full.md @@ -3170,6 +3170,11 @@ Player's ping in milliseconds, or `null` if its not a player. Player's permission level, or `null` if not applicable for this entity. +### `query(e, 'client_brand')` + +Returns recognized type of client of the client connected. Possible results include `'vanilla'`, or `'carpet '` where +version indicates the version of the connected carpet client. + ### `query(e, 'effect', name?)` Without extra arguments, it returns list of effect active on a living entity. Each entry is a triple of short @@ -4791,7 +4796,6 @@ Available options in the scarpet app space: * `game_version` - base version of the game Server related properties - * `server_motd` - the motd of the server visible when joining * `server_ip` - IP adress of the game hosted * `server_whitelisted` - boolean indicating whether the access to the server is only for whitelisted players @@ -4807,7 +4811,10 @@ Available options in the scarpet app space: * `java_version` - version of Java * `java_bits` - number indicating how many bits the Java has, 32 or 64 * `java_system_cpu_load` - current percentage of CPU used by the system - * `java_process_cpu_load` - current percentage of CPU used by JVM# `/script run` command + * `java_process_cpu_load` - current percentage of CPU used by JVM + + Scarpet related properties + * `scarpet_version` - returns the version of the carpet your scarpet comes with.# `/script run` command Primary way to input commands. The command executes in the context, position, and dimension of the executing player, commandblock, etc... The command receives 4 variables, `x`, `y`, `z` and `p` indicating position and From 5a15c115ac3fb0b9cd0e57dca7192fc13e11a90e Mon Sep 17 00:00:00 2001 From: gnembon Date: Thu, 3 Dec 2020 00:44:27 -0400 Subject: [PATCH 3/5] compound tags from scarpet are saved compressed and allowing to read compressed compounds as well. --- .../java/carpet/script/api/Auxiliary.java | 14 ++-- .../carpet/script/bundled/FileModule.java | 69 ++++++++++++++----- 2 files changed, 57 insertions(+), 26 deletions(-) diff --git a/src/main/java/carpet/script/api/Auxiliary.java b/src/main/java/carpet/script/api/Auxiliary.java index 2105605c..1ec278f8 100644 --- a/src/main/java/carpet/script/api/Auxiliary.java +++ b/src/main/java/carpet/script/api/Auxiliary.java @@ -88,12 +88,12 @@ public class Auxiliary { public static final String MARKER_STRING = "__scarpet_marker"; private static final Map mixerMap = Arrays.stream(SoundCategory.values()).collect(Collectors.toMap(SoundCategory::getName, k -> k)); - public static String recognizeResource(Value value) + public static String recognizeResource(Value value, boolean isFloder) { String origfile = value.getString(); String file = origfile.toLowerCase(Locale.ROOT).replaceAll("[^A-Za-z0-9\\-+_/]", ""); file = Arrays.stream(file.split("/+")).filter(s -> !s.isEmpty()).collect(Collectors.joining("/")); - if (file.isEmpty()) + if (file.isEmpty() && !isFloder) { throw new InternalExpressionException("Cannot use "+origfile+" as resource name - must have some letters and numbers"); } @@ -786,7 +786,7 @@ public class Auxiliary { }); expression.addLazyFunction("read_file", 2, (c, t, lv) -> { - String resource = recognizeResource(lv.get(0).evalValue(c)); + String resource = recognizeResource(lv.get(0).evalValue(c), false); String origtype = lv.get(1).evalValue(c).getString().toLowerCase(Locale.ROOT); boolean shared = origtype.startsWith("shared_"); String type = shared ? origtype.substring(7) : origtype; //len(shared_) @@ -809,7 +809,7 @@ public class Auxiliary { }); expression.addLazyFunction("delete_file", 2, (c, t, lv) -> { - String resource = recognizeResource(lv.get(0).evalValue(c)); + String resource = recognizeResource(lv.get(0).evalValue(c), false); String origtype = lv.get(1).evalValue(c).getString().toLowerCase(Locale.ROOT); boolean shared = origtype.startsWith("shared_"); String type = shared ? origtype.substring(7) : origtype; //len(shared_) @@ -821,7 +821,7 @@ public class Auxiliary { expression.addLazyFunction("write_file", -1, (c, t, lv) -> { if (lv.size() < 3) throw new InternalExpressionException("'write_file' requires three or more arguments"); - String resource = recognizeResource(lv.get(0).evalValue(c)); + String resource = recognizeResource(lv.get(0).evalValue(c), false); String origtype = lv.get(1).evalValue(c).getString().toLowerCase(Locale.ROOT); boolean shared = origtype.startsWith("shared_"); String type = shared ? origtype.substring(7) : origtype; //len(shared_) @@ -874,7 +874,7 @@ public class Auxiliary { if (lv.size()>0) { c.host.issueDeprecation("load_app_data(...) with arguments"); - file = recognizeResource(lv.get(0).evalValue(c)); + file = recognizeResource(lv.get(0).evalValue(c), false); if (lv.size() > 1) { shared = lv.get(1).evalValue(c).getBoolean(); @@ -897,7 +897,7 @@ public class Auxiliary { if (lv.size()>1) { c.host.issueDeprecation("store_app_data(...) with more than one argument"); - file = recognizeResource(lv.get(1).evalValue(c)); + file = recognizeResource(lv.get(1).evalValue(c), false); if (lv.size() > 2) { shared = lv.get(2).evalValue(c).getBoolean(); diff --git a/src/main/java/carpet/script/bundled/FileModule.java b/src/main/java/carpet/script/bundled/FileModule.java index cacc2e12..4528a17f 100644 --- a/src/main/java/carpet/script/bundled/FileModule.java +++ b/src/main/java/carpet/script/bundled/FileModule.java @@ -1,5 +1,7 @@ package carpet.script.bundled; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; import net.minecraft.nbt.PositionTracker; import net.minecraft.nbt.Tag; import net.minecraft.nbt.TagReaders; @@ -60,25 +62,40 @@ public class FileModule extends Module } //copied private method from net.minecraft.nbt.NbtIo.read() + // to read non-compound tags - these won't be compressed public static Tag read(File file) { - try (DataInputStream dataInput_1 = new DataInputStream(new FileInputStream(file))) + try { - byte byte_1 = dataInput_1.readByte(); - if (byte_1 == 0) + return NbtIo.readCompressed(file); + } + catch (IOException e) + { + try { + return NbtIo.read(file); + } + catch (IOException ioException) + { + try (DataInputStream dataInput_1 = new DataInputStream(new FileInputStream(file))) + { + byte byte_1 = dataInput_1.readByte(); + if (byte_1 == 0) + { + return null; + } + else + { + dataInput_1.readUTF(); + return TagReaders.of(byte_1).read(dataInput_1, 0, PositionTracker.DEFAULT); + } + } + catch (IOException ignored) + { + } return null; } - else - { - dataInput_1.readUTF(); - return TagReaders.of(byte_1).read(dataInput_1, 0, PositionTracker.DEFAULT); - } } - catch (IOException ignored) - { - } - return null; } //copied private method from net.minecraft.nbt.NbtIo.write() and client method safe_write @@ -87,18 +104,32 @@ public class FileModule extends Module File file_2 = new File(file.getAbsolutePath() + "_tmp"); if (file_2.exists()) file_2.delete(); - try(DataOutputStream dataOutputStream_1 = new DataOutputStream(new FileOutputStream(file_2))) + if (tag_1 instanceof CompoundTag) { - dataOutputStream_1.writeByte(tag_1.getType()); - if (tag_1.getType() != 0) + try { - dataOutputStream_1.writeUTF(""); - tag_1.write(dataOutputStream_1); + NbtIo.writeCompressed((CompoundTag) tag_1, file_2); + } + catch (IOException e) + { + return false; } } - catch (IOException e) + else { - return false; + try (DataOutputStream dataOutputStream_1 = new DataOutputStream(new FileOutputStream(file_2))) + { + dataOutputStream_1.writeByte(tag_1.getType()); + if (tag_1.getType() != 0) + { + dataOutputStream_1.writeUTF(""); + tag_1.write(dataOutputStream_1); + } + } + catch (IOException e) + { + return false; + } } if (file.exists()) file.delete(); if (!file.exists()) file_2.renameTo(file); From 6e5af5bfc899db887f54670ab345bc2d031022bb Mon Sep 17 00:00:00 2001 From: gnembon Date: Thu, 3 Dec 2020 04:05:11 -0400 Subject: [PATCH 4/5] added list_files, fixes #589 --- docs/scarpet/api/Auxiliary.md | 33 ++++---- docs/scarpet/resources/editors/idea/2.txt | 1 + .../scarpet/resources/editors/npp/scarpet.xml | 2 +- .../java/carpet/script/CarpetScriptHost.java | 7 ++ .../carpet/script/CarpetScriptServer.java | 4 +- .../java/carpet/script/api/Auxiliary.java | 76 ++++++++++++------- .../carpet/script/bundled/FileModule.java | 15 ++++ .../java/carpet/script/bundled/Module.java | 62 +++++++++------ 8 files changed, 126 insertions(+), 74 deletions(-) diff --git a/docs/scarpet/api/Auxiliary.md b/docs/scarpet/api/Auxiliary.md index f759b4fd..6362b82d 100644 --- a/docs/scarpet/api/Auxiliary.md +++ b/docs/scarpet/api/Auxiliary.md @@ -275,13 +275,15 @@ Available output types: ### `read_file(resource, type)` ### `delete_file(resource, type)` ### `write_file(resource, type, data, ...)` +### `list_files(resource, type)` With the specified `resource` in the scripts folder, of a specific `type`, writes/appends `data` to it, reads its - content, or deletes the resource. + content, deletes the resource, or lists other files under this resource. Resource is identified by a path to the file. A path can contain letters, numbers, characters `-`, `+`, or `_`, and a folder separator: `'/'`. Any other characters are stripped -from the name. Empty descriptors are invalid. Do not add file extensions to the descriptor - extensions are inferred +from the name. Empty descriptors are invalid, except for `list_files` where it means the root folder. + Do not add file extensions to the descriptor - extensions are inferred based on the `type` of the file. Resources can be located in the app specific space, or a shared space for all the apps. Accessing of app-specific @@ -296,33 +298,26 @@ specific data directory is under `world/scripts/foo.data/...`, and shared data s The default no-name app, via `/script run` command can only save/load/read files from the shared space. -Functions return `null` if an error is encounter or no file is present (for read and delete operations). Returns `true` -for success writes and deletes, and requested data, based on the file type, for read operations. - -NBT files can be written once as they an store one tag at a time. Consecutive writes will overwrite previous data. - -Write operations to text files always result in appending to the existing file, so consecutive writes will increase -the size of the file and add data to it. Since files are closed after each write, sending multiple lines of data to -write is beneficial for writing speed. To send multiple packs of data, either provide them flat or as a list in the -third argument. - * `write_file('temp', 'text', 'foo', 'bar', 'baz')` or - * write_file('temp', 'text', l('foo', 'bar', 'baz')) +Functions return `null` if an error is encounter or no file is present (for read, list and delete operations). Returns `true` +for success writes and deletes, and requested data, based on the file type, for read operations. It returns list of files +for folder listing. -To log a single line of string - -Supported values for resource `type` is: +Supported values for resource `type` are: * `nbt` - NBT tag * `text` - text resource with automatic newlines added * `raw` - text resource without implied newlines - * `shared_nbt`, `shared_text`, `shared_raw` - shared versions of the above + * `folder` - for `list_files` only - indicting folder listing instead of files + * `shared_nbt`, `shared_text`, `shared_raw`, `shared_folder` - shared versions of the above NBT files have extension `.nbt`, store one NBT tag, and return a NBT type value. Text files have `.txt` extension, stores multiple lines of text and returns lists of all lines from the file. With `write_file`, multiple lines can be sent to the file at once. The only difference between `raw` and `text` types are automatic newlines added after each -record to the file. +record to the file. Since files are closed after each write, sending multiple lines of data to +write is beneficial for writing speed. To send multiple packs of data, either provide them flat or as a list in the +third argument.
-write_file('foo', 'shared_text, l('one', 'two'));
+write_file('foo', 'shared_text, ['one', 'two']);
 write_file('foo', 'shared_text', 'three\n', 'four\n');
 write_file('foo', 'shared_raw', 'five\n', 'six\n');
 
diff --git a/docs/scarpet/resources/editors/idea/2.txt b/docs/scarpet/resources/editors/idea/2.txt
index 8f826f39..e7f1f8f6 100644
--- a/docs/scarpet/resources/editors/idea/2.txt
+++ b/docs/scarpet/resources/editors/idea/2.txt
@@ -55,6 +55,7 @@ item_category
 last_tick_times
 light
 liquid
+list_files
 load_app_data
 loaded
 loaded_status
diff --git a/docs/scarpet/resources/editors/npp/scarpet.xml b/docs/scarpet/resources/editors/npp/scarpet.xml
index 5dba63e7..46f3a139 100644
--- a/docs/scarpet/resources/editors/npp/scarpet.xml
+++ b/docs/scarpet/resources/editors/npp/scarpet.xml
@@ -82,7 +82,7 @@
 
                 plop harvest destroy schedule nbt draw_shape create_marker remove_all_markers
 
-                load_app_data store_app_data read_file write_file delete_file
+                load_app_data store_app_data read_file write_file delete_file list_files
                 escape_nbt parse_nbt encode_nbt tag_matches statistic
             
             euler pi null true false
diff --git a/src/main/java/carpet/script/CarpetScriptHost.java b/src/main/java/carpet/script/CarpetScriptHost.java
index f4cf8844..4a5a02ef 100644
--- a/src/main/java/carpet/script/CarpetScriptHost.java
+++ b/src/main/java/carpet/script/CarpetScriptHost.java
@@ -41,6 +41,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.function.Function;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import static java.lang.Math.max;
 import static net.minecraft.server.command.CommandManager.literal;
@@ -673,6 +674,12 @@ public class CarpetScriptHost extends ScriptHost
         return Module.listFile(main, resource, "txt", isShared);
     }
 
+    public Stream listFolder(String resource, String ext, boolean isShared)
+    {
+        if (getName() == null && !isShared) return null; //
+        return Module.listFolder(main, resource, ext, isShared);
+    }
+
 
     public void tick()
     {
diff --git a/src/main/java/carpet/script/CarpetScriptServer.java b/src/main/java/carpet/script/CarpetScriptServer.java
index 17cf32f8..d7090999 100644
--- a/src/main/java/carpet/script/CarpetScriptServer.java
+++ b/src/main/java/carpet/script/CarpetScriptServer.java
@@ -137,7 +137,7 @@ public class CarpetScriptServer
             Path folder = server.getSavePath(WorldSavePath.ROOT).resolve("scripts");
             Files.createDirectories(folder);
             Optional
-            scriptPath = Files.walk(folder)
+            scriptPath = Files.list(folder)
                 .filter(script -> 
                     script.getFileName().toString().equalsIgnoreCase(name+".sc") || 
                     (allowLibraries && script.getFileName().toString().equalsIgnoreCase(name+".scl"))
@@ -149,7 +149,7 @@ public class CarpetScriptServer
             {
                 Path globalFolder = FabricLoader.getInstance().getConfigDir().resolve("carpet/scripts");
                 Files.createDirectories(globalFolder);
-                scriptPath = Files.walk(globalFolder)
+                scriptPath = Files.list(globalFolder)
                         .filter(script -> script.getFileName().toString().equalsIgnoreCase(name + ".sc") ||
                                 (allowLibraries && script.getFileName().toString().equalsIgnoreCase(name + ".scl")))
                         .findFirst();
diff --git a/src/main/java/carpet/script/api/Auxiliary.java b/src/main/java/carpet/script/api/Auxiliary.java
index 1ec278f8..1c8503ef 100644
--- a/src/main/java/carpet/script/api/Auxiliary.java
+++ b/src/main/java/carpet/script/api/Auxiliary.java
@@ -32,6 +32,7 @@ import carpet.script.value.StringValue;
 import carpet.script.value.Value;
 import carpet.script.value.ValueConversions;
 import carpet.utils.Messenger;
+import com.google.common.collect.ImmutableMap;
 import net.minecraft.block.BlockState;
 import net.minecraft.command.argument.EntityAnchorArgumentType;
 import net.minecraft.entity.Entity;
@@ -50,7 +51,6 @@ import net.minecraft.particle.ParticleEffect;
 import net.minecraft.server.MinecraftServer;
 import net.minecraft.server.command.CommandOutput;
 import net.minecraft.server.command.ServerCommandSource;
-import net.minecraft.server.network.ServerPlayNetworkHandler;
 import net.minecraft.server.network.ServerPlayerEntity;
 import net.minecraft.server.world.ServerWorld;
 import net.minecraft.sound.SoundCategory;
@@ -69,6 +69,7 @@ import net.minecraft.util.registry.DynamicRegistryManager;
 import net.minecraft.util.registry.Registry;
 import net.minecraft.world.World;
 import org.apache.commons.lang3.tuple.Pair;
+import org.apache.commons.lang3.tuple.Triple;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -87,6 +88,12 @@ import static java.lang.Math.min;
 public class Auxiliary {
     public static final String MARKER_STRING = "__scarpet_marker";
     private static final Map mixerMap = Arrays.stream(SoundCategory.values()).collect(Collectors.toMap(SoundCategory::getName, k -> k));
+    private static final Map supportedTypes = ImmutableMap.of(
+            "raw", ".txt",
+            "text", ".txt",
+            "nbt", ".nbt",
+            "folder", "folder"
+    );
 
     public static String recognizeResource(Value value, boolean isFloder)
     {
@@ -100,6 +107,20 @@ public class Auxiliary {
         return file;
     }
 
+    public static Triple getFileDescriptor(List lv, Context c, boolean isFloder)
+    {
+        if (lv.size() < 2) throw new InternalExpressionException("File functions require path and type as first two arguments");
+        String resource = recognizeResource(lv.get(0).evalValue(c), isFloder);
+        String origtype = lv.get(1).evalValue(c).getString().toLowerCase(Locale.ROOT);
+        boolean shared = origtype.startsWith("shared_");
+        String type = shared ? origtype.substring(7) : origtype; //len(shared_)
+        if (!supportedTypes.containsKey(type))
+            throw new InternalExpressionException("Unsupported file type: "+origtype);
+        if (type.equals("folder") && !isFloder)
+            throw new InternalExpressionException("Folder types are no supported for this IO function");
+        return Triple.of(resource, type, shared);
+    }
+
     public static void apply(Expression expression)
     {
         expression.addLazyFunction("sound", -1, (c, t, lv) -> {
@@ -785,23 +806,29 @@ public class Auxiliary {
             return (_c, _t) -> res; // pass through for variables
         });
 
-        expression.addLazyFunction("read_file", 2, (c, t, lv) -> {
-            String resource = recognizeResource(lv.get(0).evalValue(c), false);
-            String origtype = lv.get(1).evalValue(c).getString().toLowerCase(Locale.ROOT);
-            boolean shared = origtype.startsWith("shared_");
-            String type = shared ? origtype.substring(7) : origtype; //len(shared_)
-            if (!type.equals("raw") && !type.equals("text") && !type.equals("nbt"))
-                throw new InternalExpressionException("Unsupported file type: "+origtype);
+        expression.addLazyFunction("list_files", 2, (c, t, lv) ->
+        {
+            Triple fdesc = getFileDescriptor(lv, c, true);
+            Stream files = ((CarpetScriptHost) c.host).listFolder(fdesc.getLeft(), supportedTypes.get(fdesc.getMiddle()), fdesc.getRight());
+            if (files == null) return LazyValue.NULL;
+            Value ret = ListValue.wrap(files.map(StringValue::of).collect(Collectors.toList()));
+            return (cc, tt) -> ret;
+        });
+
+
+        expression.addLazyFunction("read_file", 2, (c, t, lv) ->
+        {
+            Triple fdesc = getFileDescriptor(lv, c, false);
             Value retVal;
-            if (type.equals("nbt"))
+            if (fdesc.getMiddle().equals("nbt"))
             {
-                Tag state = ((CarpetScriptHost)((CarpetContext)c).host).readFileTag(resource, shared);
+                Tag state = ((CarpetScriptHost) c.host).readFileTag(fdesc.getLeft(), fdesc.getRight());
                 if (state == null) return LazyValue.NULL;
                 retVal = new NBTSerializableValue(state);
             }
             else
             {
-                List content = ((CarpetScriptHost) ((CarpetContext) c).host).readTextResource(resource, shared);
+                List content = ((CarpetScriptHost) c.host).readTextResource(fdesc.getLeft(), fdesc.getRight());
                 if (content == null) return LazyValue.NULL;
                 retVal = ListValue.wrap(content.stream().map(StringValue::new).collect(Collectors.toList()));
             }
@@ -809,33 +836,26 @@ public class Auxiliary {
         });
 
         expression.addLazyFunction("delete_file", 2, (c, t, lv) -> {
-            String resource = recognizeResource(lv.get(0).evalValue(c), false);
-            String origtype = lv.get(1).evalValue(c).getString().toLowerCase(Locale.ROOT);
-            boolean shared = origtype.startsWith("shared_");
-            String type = shared ? origtype.substring(7) : origtype; //len(shared_)
-            if (!type.equals("raw") && !type.equals("text") && !type.equals("nbt"))
-                throw new InternalExpressionException("Unsupported file type: "+origtype);
-            boolean success = ((CarpetScriptHost)((CarpetContext)c).host).removeResourceFile(resource, shared, type);
-            return success?LazyValue.TRUE:LazyValue.FALSE;
+            Triple fdesc = getFileDescriptor(lv, c, false);
+            return ((CarpetScriptHost) c.host).removeResourceFile(fdesc.getLeft(), fdesc.getRight(), fdesc.getMiddle())
+                    ? LazyValue.TRUE
+                    : LazyValue.FALSE;
         });
 
         expression.addLazyFunction("write_file", -1, (c, t, lv) -> {
             if (lv.size() < 3) throw new InternalExpressionException("'write_file' requires three or more arguments");
-            String resource = recognizeResource(lv.get(0).evalValue(c), false);
-            String origtype = lv.get(1).evalValue(c).getString().toLowerCase(Locale.ROOT);
-            boolean shared = origtype.startsWith("shared_");
-            String type = shared ? origtype.substring(7) : origtype; //len(shared_)
-            if (!type.equals("raw") && !type.equals("text") && !type.equals("nbt"))
-                throw new InternalExpressionException("Unsupported file type: "+origtype);
+            Triple fdesc = getFileDescriptor(lv, c, false);
+
+
             boolean success;
-            if (type.equals("nbt"))
+            if (fdesc.getMiddle().equals("nbt"))
             {
                 Value val = lv.get(2).evalValue(c);
                 NBTSerializableValue tagValue =  (val instanceof NBTSerializableValue)
                         ? (NBTSerializableValue) val
                         : new NBTSerializableValue(val.getString());
                 Tag tag = tagValue.getTag();
-                success = ((CarpetScriptHost)((CarpetContext)c).host).writeTagFile(tag, resource, shared);
+                success = ((CarpetScriptHost) c.host).writeTagFile(tag, fdesc.getLeft(), fdesc.getRight());
             }
             else
             {
@@ -860,7 +880,7 @@ public class Auxiliary {
                         data.add(lv.get(i).evalValue(c).getString());
                     }
                 }
-                success = ((CarpetScriptHost) ((CarpetContext) c).host).appendLogFile(resource, shared, type, data);
+                success = ((CarpetScriptHost) c.host).appendLogFile(fdesc.getLeft(), fdesc.getRight(), fdesc.getMiddle(), data);
             }
             return success?LazyValue.TRUE:LazyValue.FALSE;
         });
diff --git a/src/main/java/carpet/script/bundled/FileModule.java b/src/main/java/carpet/script/bundled/FileModule.java
index 4528a17f..b710b376 100644
--- a/src/main/java/carpet/script/bundled/FileModule.java
+++ b/src/main/java/carpet/script/bundled/FileModule.java
@@ -23,6 +23,7 @@ import java.nio.file.StandardOpenOption;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
+import java.util.stream.Stream;
 
 public class FileModule extends Module
 {
@@ -175,4 +176,18 @@ public class FileModule extends Module
             return null;
         }
     }
+
+    public static Stream listFiles(Path dir, String ext)
+    {
+        boolean folderListing = ext.equals("folder");
+        try
+        {
+            return Files.list(dir).
+                    filter(path -> folderListing?Files.isDirectory(path):path.toString().endsWith(ext));
+        }
+        catch (IOException ignored)
+        {
+            return null;
+        }
+    }
 }
diff --git a/src/main/java/carpet/script/bundled/Module.java b/src/main/java/carpet/script/bundled/Module.java
index 1916e0ba..3bf5f8ce 100644
--- a/src/main/java/carpet/script/bundled/Module.java
+++ b/src/main/java/carpet/script/bundled/Module.java
@@ -3,10 +3,12 @@ package carpet.script.bundled;
 import carpet.CarpetServer;
 import net.minecraft.nbt.Tag;
 import net.minecraft.util.WorldSavePath;
+import org.apache.commons.io.FilenameUtils;
 
-import java.io.File;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.List;
+import java.util.stream.Stream;
 
 public abstract class Module
 {
@@ -19,61 +21,73 @@ public abstract class Module
     {
         if (isShared)
         {
-            return "shared/"+file;
+            return file.isEmpty()?"shared":"shared/"+file;
         }
-        else if (module != null && module.getName() != null) // appdata
+        if (module != null && module.getName() != null) // appdata
         {
-            return module.getName()+".data"+(file==null?"":"/"+file);
-        }
-        else
-        {
-            throw  new RuntimeException("Invalid file descriptor: "+file);
+            return module.getName()+".data"+(file==null || file.isEmpty()?"":"/"+file);
         }
+        throw  new RuntimeException("Invalid file descriptor: "+file);
     }
 
     public static Tag getData(Module module, String file, boolean isShared)
     {
-        File dataFile = resolveResource(module, file, "nbt", isShared);
+        Path dataFile = resolveResource(module, file, "nbt", isShared);
         if (dataFile == null) return null;
-        if (!Files.exists(dataFile.toPath()) || !(dataFile.isFile())) return null;
-        synchronized (writeIOSync) { return FileModule.read(dataFile); }
+        if (!Files.exists(dataFile) || !(dataFile.toFile().isFile())) return null;
+        synchronized (writeIOSync) { return FileModule.read(dataFile.toFile()); }
     }
 
     public static boolean saveData(Module module, String file, Tag globalState, boolean isShared)
     {
-        File dataFile = resolveResource(module, file, "nbt", isShared);
+        Path dataFile = resolveResource(module, file, "nbt", isShared);
         if (dataFile == null) return false;
-        if (!Files.exists(dataFile.toPath().getParent()) && !dataFile.getParentFile().mkdirs()) return false;
-        synchronized (writeIOSync) { return FileModule.write(globalState, dataFile); }
+        if (!Files.exists(dataFile.getParent()) && !dataFile.toFile().getParentFile().mkdirs()) return false;
+        synchronized (writeIOSync) { return FileModule.write(globalState, dataFile.toFile()); }
     }
 
     public static boolean appendToTextFile(Module module, String resourceName, String type, boolean isShared, List message)
     {
-        File dataFile = resolveResource(module, resourceName, "txt", isShared);
+        Path dataFile = resolveResource(module, resourceName, "txt", isShared);
         if (dataFile == null) return false;
-        if (!Files.exists(dataFile.toPath().getParent()) && !dataFile.getParentFile().mkdirs()) return false;
-        synchronized (writeIOSync) { return FileModule.appendText(dataFile, type.equals("text"), message); }
+        if (!Files.exists(dataFile.getParent()) && !dataFile.toFile().getParentFile().mkdirs()) return false;
+        synchronized (writeIOSync) { return FileModule.appendText(dataFile.toFile(), type.equals("text"), message); }
     }
 
     public static boolean dropExistingFile(Module module, String resourceName, String ext, boolean isShared)
     {
-        File dataFile = resolveResource(module, resourceName, ext, isShared);
+        Path dataFile = resolveResource(module, resourceName, ext, isShared);
         if (dataFile == null) return false;
-        synchronized (writeIOSync) { return dataFile.delete(); }
+        synchronized (writeIOSync) { return dataFile.toFile().delete(); }
     }
 
     public static List listFile(Module module, String resourceName, String ext, boolean isShared)
     {
-        File dataFile = resolveResource(module, resourceName, ext, isShared);
+        Path dataFile = resolveResource(module, resourceName, ext, isShared);
         if (dataFile == null) return null;
-        if (!dataFile.exists()) return null;
-        synchronized (writeIOSync) { return FileModule.listFileContent(dataFile); }
+        if (!dataFile.toFile().exists()) return null;
+        synchronized (writeIOSync) { return FileModule.listFileContent(dataFile.toFile()); }
     }
 
-    private static File resolveResource(Module module, String resourceName, String ext, boolean isShared)
+    public static Stream listFolder(Module module, String resourceName, String ext, boolean isShared)
+    {
+        Path dir = resolveResource(module, resourceName, null, isShared);
+        if (dir == null) return null;
+        if (!Files.exists(dir)) return null;
+        Stream result;
+        synchronized (writeIOSync) { result = FileModule.listFiles(dir, ext); }
+        if (result == null) return null;
+        Path rootPath = resolveResource(module, "", null, isShared);
+        Stream strings = result.map(p -> rootPath.relativize(p).toString().replaceAll("[\\\\/]","/"));
+        if (!ext.equals("folder"))
+            return strings.map(FilenameUtils::removeExtension);
+        return strings;
+    }
+
+    private static Path resolveResource(Module module, String resourceName, String ext, boolean isShared)
     {
         if (!isShared && (module == null || module.getName() == null)) return null;
-        return CarpetServer.minecraft_server.getSavePath(WorldSavePath.ROOT).resolve("scripts/"+getDescriptor(module, resourceName, isShared)+"."+ext).toFile();
+        return CarpetServer.minecraft_server.getSavePath(WorldSavePath.ROOT).resolve("scripts/"+getDescriptor(module, resourceName, isShared)+(ext==null?"":"."+ext));
     }
 
     @Override

From 33bf24fa860ae93b2498b22270bc7ea106a9579b Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" 
Date: Thu, 3 Dec 2020 08:05:37 +0000
Subject: [PATCH 5/5] Merge docs for 'added list_files, fixes #589'

---
 docs/scarpet/Full.md | 33 ++++++++++++++-------------------
 1 file changed, 14 insertions(+), 19 deletions(-)

diff --git a/docs/scarpet/Full.md b/docs/scarpet/Full.md
index eb470336..bd8c1ae3 100644
--- a/docs/scarpet/Full.md
+++ b/docs/scarpet/Full.md
@@ -4571,13 +4571,15 @@ Available output types:
 ### `read_file(resource, type)`
 ### `delete_file(resource, type)`
 ### `write_file(resource, type, data, ...)`
+### `list_files(resource, type)`
 
 With the specified `resource` in the scripts folder, of a specific `type`, writes/appends `data` to it, reads its
- content, or deletes the resource.
+ content, deletes the resource, or lists other files under this resource.
 
 Resource is identified by a path to the file.  
 A path can contain letters, numbers, characters `-`, `+`, or `_`, and a folder separator: `'/'`. Any other characters are stripped
-from the name. Empty descriptors are invalid. Do not add file extensions to the descriptor - extensions are inferred
+from the name. Empty descriptors are invalid, except for `list_files` where it means the root folder.
+ Do not add file extensions to the descriptor - extensions are inferred
 based on the `type` of the file.
  
 Resources can be located in the app specific space, or a shared space for all the apps. Accessing of app-specific
@@ -4592,33 +4594,26 @@ specific data directory is under `world/scripts/foo.data/...`, and shared data s
 
 The default no-name app, via `/script run` command can only save/load/read files from the shared space.
 
-Functions return `null` if an error is encounter or no file is present (for read and delete operations). Returns `true`
-for success writes and deletes, and requested data, based on the file type, for read operations.
-
-NBT files can be written once as they an store one tag at a time. Consecutive writes will overwrite previous data.
-
-Write operations to text files always result in appending to the existing file, so consecutive writes will increase
-the size of the file and add data to it. Since files are closed after each write, sending multiple lines of data to
-write is beneficial for writing speed. To send multiple packs of data, either provide them flat or as a list in the
-third argument.
- * `write_file('temp', 'text', 'foo', 'bar', 'baz')` or
- * write_file('temp', 'text', l('foo', 'bar', 'baz'))
+Functions return `null` if an error is encounter or no file is present (for read, list and delete operations). Returns `true`
+for success writes and deletes, and requested data, based on the file type, for read operations. It returns list of files 
+for folder listing.
  
-To log a single line of string
- 
-Supported values for resource `type` is:
+Supported values for resource `type` are:
  * `nbt` - NBT tag
  * `text` - text resource with automatic newlines added
  * `raw` - text resource without implied newlines
- * `shared_nbt`, `shared_text`, `shared_raw` - shared versions of the above
+ * `folder` - for `list_files` only - indicting folder listing instead of files
+ * `shared_nbt`, `shared_text`, `shared_raw`, `shared_folder` - shared versions of the above
  
 NBT files have extension `.nbt`, store one NBT tag, and return a NBT type value. Text files have `.txt` extension, 
 stores multiple lines of text and returns lists of all lines from the file. With `write_file`, multiple lines can be
 sent to the file at once. The only difference between `raw` and `text` types are automatic newlines added after each
-record to the file.
+record to the file. Since files are closed after each write, sending multiple lines of data to
+write is beneficial for writing speed. To send multiple packs of data, either provide them flat or as a list in the
+third argument.
 
 
-write_file('foo', 'shared_text, l('one', 'two'));
+write_file('foo', 'shared_text, ['one', 'two']);
 write_file('foo', 'shared_text', 'three\n', 'four\n');
 write_file('foo', 'shared_raw', 'five\n', 'six\n');