diff --git a/docs/scarpet/api/Auxiliary.md b/docs/scarpet/api/Auxiliary.md index 9148fb78..466e9ec6 100644 --- a/docs/scarpet/api/Auxiliary.md +++ b/docs/scarpet/api/Auxiliary.md @@ -298,7 +298,8 @@ 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, 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. +based on the `type` of the file. A path can have one `'.zip'` component indicating a zip folder allowing to read / write to and from +zip files, although you cannot nest zip files in other zip files. Resources can be located in the app specific space, or a shared space for all the apps. Accessing of app-specific resources is guaranteed to be isolated from other apps. Shared resources are... well, shared across all apes, meaning @@ -312,7 +313,7 @@ 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, list and delete operations). Returns `true` +Functions return `null` if 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. @@ -336,6 +337,10 @@ third argument. Throws: - `nbt_read_error`: When failed to read NBT file. - `json_read_error`: When failed to read JSON file. The exception data will contain details about the problem. +- `io_exception`: For all other errors when handling data on disk not related to encoding issues + +All other errors resulting of improper use of input arguments should result in `null` returned from the function, rather than exception +thrown.
 write_file('foo', 'shared_text, ['one', 'two']);
@@ -616,7 +621,13 @@ Available options in the scarpet app space:
   * `game_view_distance` - the view distance
   * `game_mod_name` - the name of the base mod. Expect `'fabric'`
   * `game_version` - base version of the game
+  * `game_target` - target release version
+  * `game_major_target` - major release target. For 1.12.2, that would be 12
+  * `game_minor_reease` - minor release target. For 1.12.2, that woudl be 2
+  * `game_protocol` - protocol version number
+  * `game_pack_version` - datapack version number
   * `game_data_version` - data version of the game. Returns an integer, so it can be compared.
+  * `game_stable` - indicating if its a production release or a snapshot
   
  Server related properties
  * `server_motd` - the motd of the server visible when joining
diff --git a/docs/scarpet/language/FunctionsAndControlFlow.md b/docs/scarpet/language/FunctionsAndControlFlow.md
index a02c2d51..a0a867e0 100644
--- a/docs/scarpet/language/FunctionsAndControlFlow.md
+++ b/docs/scarpet/language/FunctionsAndControlFlow.md
@@ -265,8 +265,8 @@ but like everywhere else, doing that sounds like a bad idea.
     `unknown_poi_type`, `unknown_dimension`, `unknown_structure`, `unknown_criterion`: Specific 
     errors thrown when a specified internal name does not exist or is invalid.
   - `io_exception`: This is the parent for any exception that occurs due to an error handling external data.
-    - `nbt_read_error`: Incorrect input NBT file.
-    - `json_read_error`: Incorrect input JSON data.
+    - `nbt_error`: Incorrect input/output NBT file.
+    - `json_error`: Incorrect input/output JSON data.
   - `user_exception`: Exception thrown by default with `throw` function.
   
 Synopsis:
diff --git a/src/main/java/carpet/mixins/PlayerManager_scarpetEventsMixin.java b/src/main/java/carpet/mixins/PlayerManager_scarpetEventsMixin.java
index 9da0d4e9..a4b6a6bd 100644
--- a/src/main/java/carpet/mixins/PlayerManager_scarpetEventsMixin.java
+++ b/src/main/java/carpet/mixins/PlayerManager_scarpetEventsMixin.java
@@ -1,11 +1,13 @@
 package carpet.mixins;
 
+import carpet.CarpetServer;
 import carpet.fakes.ServerPlayerEntityInterface;
 import net.minecraft.server.PlayerManager;
 import net.minecraft.server.network.ServerPlayerEntity;
 import org.spongepowered.asm.mixin.Mixin;
 import org.spongepowered.asm.mixin.injection.At;
 import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
 import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
 
 import static carpet.script.CarpetEventServer.Event.PLAYER_RESPAWNS;
@@ -29,4 +31,9 @@ public class PlayerManager_scarpetEventsMixin
         ((ServerPlayerEntityInterface)player).invalidateEntityObjectReference();
     }
 
+    @Inject(method = "onDataPacksReloaded", at = @At("HEAD"))
+    private void reloadCommands(CallbackInfo ci)
+    {
+        CarpetServer.scriptServer.reAddCommands();
+    }
 }
diff --git a/src/main/java/carpet/script/CarpetScriptHost.java b/src/main/java/carpet/script/CarpetScriptHost.java
index 4f600896..6c2fcac8 100644
--- a/src/main/java/carpet/script/CarpetScriptHost.java
+++ b/src/main/java/carpet/script/CarpetScriptHost.java
@@ -3,6 +3,7 @@ package carpet.script;
 import carpet.CarpetServer;
 import carpet.CarpetSettings;
 import carpet.script.api.Auxiliary;
+import carpet.script.argument.FileArgument;
 import carpet.script.argument.FunctionArgument;
 import carpet.script.bundled.Module;
 import carpet.script.command.CommandArgument;
@@ -20,6 +21,7 @@ import carpet.script.value.Value;
 import carpet.utils.Messenger;
 
 import com.google.gson.JsonElement;
+import com.mojang.brigadier.arguments.StringArgumentType;
 import com.mojang.brigadier.builder.ArgumentBuilder;
 import com.mojang.brigadier.builder.LiteralArgumentBuilder;
 import com.mojang.brigadier.context.CommandContext;
@@ -32,6 +34,7 @@ import net.minecraft.server.command.ServerCommandSource;
 import net.minecraft.server.network.ServerPlayerEntity;
 import net.minecraft.server.world.ServerWorld;
 import net.minecraft.text.LiteralText;
+import net.minecraft.text.Text;
 import net.minecraft.util.math.BlockPos;
 import org.apache.commons.lang3.tuple.Pair;
 
@@ -43,11 +46,13 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.function.Consumer;
 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.argument;
 import static net.minecraft.server.command.CommandManager.literal;
 
 public class CarpetScriptHost extends ScriptHost
@@ -311,6 +316,107 @@ public class CarpetScriptHost extends ScriptHost
         }
     }
 
+    public Boolean addAppCommands(Consumer notifier)
+    {
+        if (appConfig.get(StringValue.of("commands")) != null)
+        {
+            try
+            {
+                LiteralArgumentBuilder command = readCommands(commandValidator);
+                if (command != null)
+                {
+                    scriptServer.server.getCommandManager().getDispatcher().register(command);
+                    return true;
+                }
+                else {
+                    return false;
+                }
+            }
+            catch (CommandSyntaxException cse)
+            {
+                // failed
+                notifier.accept(Messenger.c("r Failed to build command system for "+getName()+" thus failed to load the app ", cse.getRawMessage()));
+                return null;
+            }
+
+        }
+        return addLegacyCommand(notifier);
+    }
+
+    private Boolean addLegacyCommand(Consumer notifier)
+    {
+        if (main == null) return false;
+        if (getFunction("__command") == null) return false;
+
+        if (scriptServer.isInvalidCommandRoot(getName()))
+        {
+            notifier.accept(Messenger.c("gi Tried to mask vanilla command."));
+            return null;
+        }
+
+        Function configValidator;
+        try
+        {
+            configValidator = getCommandConfigPermissions();
+        }
+        catch (CommandSyntaxException e)
+        {
+            notifier.accept(Messenger.c("rb "+e.getMessage()));
+            return null;
+        }
+        String hostName = getName();
+        LiteralArgumentBuilder command = literal(hostName).
+                requires((player) -> scriptServer.modules.containsKey(hostName) && commandValidator.apply(player) && configValidator.apply(player)).
+                executes( (c) ->
+                {
+                    CarpetScriptHost targetHost = scriptServer.modules.get(hostName).retrieveOwnForExecution(c.getSource());
+                    Value response = targetHost.handleCommandLegacy(c.getSource(),"__command", null, "");
+                    if (!response.isNull()) Messenger.m(c.getSource(), "gi "+response.getString());
+                    return (int)response.readInteger();
+                });
+
+        for (String function : globaFunctionNames(main, s ->  !s.startsWith("_")).sorted().collect(Collectors.toList()))
+        {
+            if (appConfig.getOrDefault(StringValue.of("legacy_command_type_support"), Value.FALSE).getBoolean())
+            {
+                try
+                {
+                    FunctionValue functionValue = getFunction(function);
+                    command = addPathToCommand(
+                            command,
+                            CommandToken.parseSpec(CommandToken.specFromSignature(functionValue), this),
+                            FunctionArgument.fromCommandSpec(this, functionValue)
+                    );
+                }
+                catch (CommandSyntaxException e)
+                {
+                    return false;
+                }
+            }
+            else
+            {
+                command = command.
+                        then(literal(function).
+                                requires((player) -> scriptServer.modules.containsKey(hostName) && scriptServer.modules.get(hostName).getFunction(function) != null).
+                                executes((c) -> {
+                                    CarpetScriptHost targetHost = scriptServer.modules.get(hostName).retrieveOwnForExecution(c.getSource());
+                                    Value response = targetHost.handleCommandLegacy(c.getSource(),function, null, "");
+                                    if (!response.isNull()) Messenger.m(c.getSource(), "gi " + response.getString());
+                                    return (int) response.readInteger();
+                                }).
+                                then(argument("args...", StringArgumentType.greedyString()).
+                                        executes( (c) -> {
+                                            CarpetScriptHost targetHost = scriptServer.modules.get(hostName).retrieveOwnForExecution(c.getSource());
+                                            Value response = targetHost.handleCommandLegacy(c.getSource(),function, null, StringArgumentType.getString(c, "args..."));
+                                            if (!response.isNull()) Messenger.m(c.getSource(), "gi "+response.getString());
+                                            return (int)response.readInteger();
+                                        })));
+            }
+        }
+        scriptServer.server.getCommandManager().getDispatcher().register(command);
+        return true;
+    }
+
     public LiteralArgumentBuilder readCommands(Function useValidator) throws CommandSyntaxException
     {
         Value commands = appConfig.get(StringValue.of("commands"));
@@ -680,31 +786,31 @@ public class CarpetScriptHost extends ScriptHost
 
     private void dumpState()
     {
-        Module.saveData(main, null, globalState, false);
+        Module.saveData(main, globalState);
     }
 
     private Tag loadState()
     {
-        return Module.getData(main, null, false);
+        return Module.getData(main);
     }
 
-    public Tag readFileTag(String file, boolean isShared)
+    public Tag readFileTag(FileArgument fdesc)
     {
-        if (getName() == null && !isShared) return null;
-        if (file != null)
-            return Module.getData(main, file, isShared);
+        if (getName() == null && !fdesc.isShared) return null;
+        if (fdesc.resource != null)
+            return fdesc.getNbtData(main);
         if (parent == null)
             return globalState;
         return ((CarpetScriptHost)parent).globalState;
     }
 
-    public boolean writeTagFile(Tag tag, String file, boolean isShared)
+    public boolean writeTagFile(Tag tag, FileArgument fdesc)
     {
-        if (getName() == null && !isShared) return false; // if belongs to an app, cannot be default host.
+        if (getName() == null && !fdesc.isShared) return false; // if belongs to an app, cannot be default host.
 
-        if (file!= null)
+        if (fdesc.resource != null)
         {
-            return Module.saveData(main, file, tag, isShared);
+            return fdesc.saveNbtData(main, tag);
         }
 
         CarpetScriptHost responsibleHost = (parent != null)?(CarpetScriptHost) parent:this;
@@ -717,34 +823,34 @@ public class CarpetScriptHost extends ScriptHost
         return true;
     }
 
-    public boolean removeResourceFile(String resource, boolean isShared, String type)
+    public boolean removeResourceFile(FileArgument fdesc)
     {
-        if (getName() == null && !isShared) return false; //
-        return Module.dropExistingFile(main, resource, type, isShared);
+        if (getName() == null && !fdesc.isShared) return false; //
+        return fdesc.dropExistingFile(main);
     }
 
-    public boolean appendLogFile(String resource, boolean isShared, String type, List data)
+    public boolean appendLogFile(FileArgument fdesc, List data)
     {
-        if (getName() == null && !isShared) return false; // if belongs to an app, cannot be default host.
-        return Module.appendToTextFile(main, resource, type, isShared, data);
+        if (getName() == null && !fdesc.isShared) return false; // if belongs to an app, cannot be default host.
+        return fdesc.appendToTextFile(main, data);
     }
 
-    public List readTextResource(String resource, String type, boolean isShared)
+    public List readTextResource(FileArgument fdesc)
     {
-        if (getName() == null && !isShared) return null;
-        return Module.listFile(main, resource, type, isShared);
+        if (getName() == null && !fdesc.isShared) return null;
+        return fdesc.listFile(main);
     }
     
-    public JsonElement readJsonFile(String resource, String type, boolean isShared)
+    public JsonElement readJsonFile(FileArgument fdesc)
     {
-        if (getName() == null && !isShared) return null;
-        return Module.readJsonFile(main, resource, type, isShared);
+        if (getName() == null && !fdesc.isShared) return null;
+        return fdesc.readJsonFile(main);
     }
 
-    public Stream listFolder(String resource, String type, boolean isShared)
+    public Stream listFolder(FileArgument fdesc)
     {
-        if (getName() == null && !isShared) return null; //
-        return Module.listFolder(main, resource, type, isShared);
+        if (getName() == null && !fdesc.isShared) return null; //
+        return fdesc.listFolder(main);
     }
 
 
diff --git a/src/main/java/carpet/script/CarpetScriptServer.java b/src/main/java/carpet/script/CarpetScriptServer.java
index fc9eb5e2..cd02173b 100644
--- a/src/main/java/carpet/script/CarpetScriptServer.java
+++ b/src/main/java/carpet/script/CarpetScriptServer.java
@@ -272,35 +272,23 @@ public class CarpetScriptServer
         }
         //addEvents(source, name);
         String action = reload?"reloaded":"loaded";
-        if (newHost.appConfig.get(StringValue.of("commands")) != null)
-        {
-            try
-            {
-                LiteralArgumentBuilder command = newHost.readCommands(commandValidator);
-                if (command != null)
-                {
-                    if (!isRuleApp) Messenger.m(source, "gi "+name+" app "+action+" with /"+name+" command");
-                    server.getCommandManager().getDispatcher().register(command);
-                    CarpetServer.settingsManager.notifyPlayersCommandsChanged();
-                }
-                else {
-                    if (!isRuleApp) Messenger.m(source, "gi "+name+" app "+action);
-                }
-            }
-            catch (CommandSyntaxException cse)
-            {
-                removeScriptHost(source, name, false, false);
-                Messenger.m(source, "r Failed to build command system for "+name+" thus failed to load the app ", cse.getRawMessage());
-                return false;
-            }
-
-        }
-        else if (!addLegacyCommand(source, name, reload, !isRuleApp, commandValidator)) // this needs to be moved to config reader, only supporting legacy command here
+        Boolean isCommandAdded = newHost.addAppCommands(s -> {
+            if (!isRuleApp) Messenger.m(source, s);
+        });
+        if (isCommandAdded == null) // error should be dispatched
         {
             removeScriptHost(source, name, false, false);
-            Messenger.m(source, "r Failed to build command system for "+name+" thus failed to load the app");
-            return false;
         }
+        else if (isCommandAdded)
+        {
+            CarpetServer.settingsManager.notifyPlayersCommandsChanged();
+            if (!isRuleApp) Messenger.m(source, "gi "+name+" app "+action+" with /"+name+" command");
+        }
+        else
+        {
+            if (!isRuleApp) Messenger.m(source, "gi "+name+" app "+action);
+        }
+
         if (newHost.isPerUser())
         {
             // that will provide player hosts right at the startup
@@ -317,89 +305,9 @@ public class CarpetScriptServer
         return true;
     }
 
-    private boolean addLegacyCommand(ServerCommandSource source, String hostName, boolean isReload, boolean notifySource, Function useValidator)
+    public boolean isInvalidCommandRoot(String appName)
     {
-        CarpetScriptHost host = modules.get(hostName);
-        String loaded = isReload?"reloaded":"loaded";
-        if (host == null)
-        {
-            return true;
-        }
-        if (host.getFunction("__command") == null)
-        {
-            if (notifySource) Messenger.m(source, "gi "+hostName+" app "+loaded+".");
-            return true;
-        }
-        if (holyMoly.contains(hostName))
-        {
-            Messenger.m(source, "gi "+hostName+" app "+loaded+" with no command.");
-            Messenger.m(source, "gi Tried to mask vanilla command.");
-            return true;
-        }
-
-        Function configValidator;
-        try
-        {
-            configValidator = host.getCommandConfigPermissions();
-        }
-        catch (CommandSyntaxException e)
-        {
-            Messenger.m(source, "rb "+e.getMessage());
-            return false;
-        }
-
-        LiteralArgumentBuilder command = literal(hostName).
-                requires((player) -> modules.containsKey(hostName) && useValidator.apply(player) && configValidator.apply(player)).
-                executes( (c) ->
-                {
-                    CarpetScriptHost targetHost = modules.get(hostName).retrieveOwnForExecution(c.getSource());
-                    Value response = targetHost.handleCommandLegacy(c.getSource(),"__command", null, "");
-                    if (!response.isNull()) Messenger.m(c.getSource(), "gi "+response.getString());
-                    return (int)response.readInteger();
-                });
-
-        for (String function : host.globaFunctionNames(host.main, s ->  !s.startsWith("_")).sorted().collect(Collectors.toList()))
-        {
-            if (host.appConfig.getOrDefault(StringValue.of("legacy_command_type_support"), Value.FALSE).getBoolean())
-            {
-                try
-                {
-                    FunctionValue functionValue = host.getFunction(function);
-                    command = host.addPathToCommand(
-                            command,
-                            CommandToken.parseSpec(CommandToken.specFromSignature(functionValue), host),
-                            FunctionArgument.fromCommandSpec(host, functionValue)
-                    );
-                }
-                catch (CommandSyntaxException e)
-                {
-                    return false;
-                }
-            }
-            else
-            {
-                command = command.
-                        then(literal(function).
-                                requires((player) -> modules.containsKey(hostName) && modules.get(hostName).getFunction(function) != null).
-                                executes((c) -> {
-                                    CarpetScriptHost targetHost = modules.get(hostName).retrieveOwnForExecution(c.getSource());
-                                    Value response = targetHost.handleCommandLegacy(c.getSource(),function, null, "");
-                                    if (!response.isNull()) Messenger.m(c.getSource(), "gi " + response.getString());
-                                    return (int) response.readInteger();
-                                }).
-                                then(argument("args...", StringArgumentType.greedyString()).
-                                        executes( (c) -> {
-                                            CarpetScriptHost targetHost = modules.get(hostName).retrieveOwnForExecution(c.getSource());
-                                            Value response = targetHost.handleCommandLegacy(c.getSource(),function, null, StringArgumentType.getString(c, "args..."));
-                                            if (!response.isNull()) Messenger.m(c.getSource(), "gi "+response.getString());
-                                            return (int)response.readInteger();
-                                        })));
-            }
-        }
-        if (notifySource) Messenger.m(source, "gi "+hostName+" app "+loaded+" with /"+hostName+" command");
-        server.getCommandManager().getDispatcher().register(command);
-        CarpetServer.settingsManager.notifyPlayersCommandsChanged();
-        return true;
+        return holyMoly.contains(appName);
     }
 
 
@@ -558,4 +466,9 @@ public class CarpetScriptServer
         init();
         apps.forEach((s, data) -> addScriptHost(server.getCommandSource(), s,data.commandValidator, data.perUser,false, data.isRuleApp));
     }
+
+    public void reAddCommands()
+    {
+        modules.values().forEach(host -> host.addAppCommands(s -> {}));
+    }
 }
diff --git a/src/main/java/carpet/script/api/Auxiliary.java b/src/main/java/carpet/script/api/Auxiliary.java
index 0537e532..c7c2ac35 100644
--- a/src/main/java/carpet/script/api/Auxiliary.java
+++ b/src/main/java/carpet/script/api/Auxiliary.java
@@ -7,7 +7,7 @@ import carpet.fakes.ServerWorldInterface;
 import carpet.fakes.StatTypeInterface;
 import carpet.fakes.ThreadedAnvilChunkStorageInterface;
 import carpet.helpers.FeatureGenerator;
-import carpet.script.bundled.Module;
+import carpet.script.argument.FileArgument;
 import carpet.script.CarpetContext;
 import carpet.script.CarpetEventServer;
 import carpet.script.CarpetScriptHost;
@@ -105,7 +105,6 @@ import net.minecraft.world.level.UnmodifiableLevelProperties;
 import net.minecraft.world.level.storage.LevelStorage;
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.lang3.tuple.Pair;
-import org.apache.commons.lang3.tuple.Triple;
 
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
@@ -140,6 +139,7 @@ public class Auxiliary {
     private static final Map mixerMap = Arrays.stream(SoundCategory.values()).collect(Collectors.toMap(SoundCategory::getName, k -> k));
     public static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().registerTypeAdapter(Value.class, new ScarpetJsonDeserializer()).create();
 
+    @Deprecated
     public static String recognizeResource(Value value, boolean isFloder)
     {
         String origfile = value.getString();
@@ -152,20 +152,6 @@ 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 (!Module.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) -> {
@@ -878,49 +864,27 @@ public class Auxiliary {
 
         expression.addLazyFunction("list_files", 2, (c, t, lv) ->
         {
-            Triple fdesc = getFileDescriptor(lv, c, true);
-            Stream files = ((CarpetScriptHost) c.host).listFolder(fdesc.getLeft(), fdesc.getMiddle(), fdesc.getRight());
+            FileArgument fdesc = FileArgument.from(lv, c, true, FileArgument.Reason.READ);
+            Stream files = ((CarpetScriptHost) c.host).listFolder(fdesc);
             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);
+            FileArgument fdesc = FileArgument.from(lv, c, false, FileArgument.Reason.READ);
             Value retVal;
-            if (fdesc.getMiddle().equals("nbt"))
+            if (fdesc.type == FileArgument.Type.NBT)
             {
-                Tag state;
-                try
-                {
-                    state = ((CarpetScriptHost) c.host).readFileTag(fdesc.getLeft(), fdesc.getRight());
-                } catch (CrashException e)
-                {
-                    throw new ThrowStatement((fdesc.getRight()?"shared/":"")+ fdesc.getLeft(), Throwables.NBT_READ);
-                }
-
+                Tag state = ((CarpetScriptHost) c.host).readFileTag(fdesc);
                 if (state == null) return LazyValue.NULL;
                 retVal = new NBTSerializableValue(state);
             }
-            else if (fdesc.getMiddle().equals("json"))
+            else if (fdesc.type == FileArgument.Type.JSON)
             {
                 JsonElement json;
-                try
-                {
-                    json = ((CarpetScriptHost) c.host).readJsonFile(fdesc.getLeft(), fdesc.getMiddle(), fdesc.getRight());
-                }
-                catch (JsonParseException e)
-                {
-                    Throwable exc = e;
-                    if(e.getCause() != null)
-                        exc = e.getCause();
-                    throw new ThrowStatement(MapValue.wrap(ImmutableMap.of(
-                            StringValue.of("error"), StringValue.of(exc.getMessage()),
-                            StringValue.of("path"), StringValue.of((fdesc.getRight()?"shared/":"")+ fdesc.getLeft())
-                    )), Throwables.JSON_READ);
-                }
+                json = ((CarpetScriptHost) c.host).readJsonFile(fdesc);
                 Value parsedJson = GSON.fromJson(json, Value.class);
                 if (parsedJson == null)
                     retVal = Value.NULL;
@@ -929,7 +893,7 @@ public class Auxiliary {
             }
             else
             {
-                List content = ((CarpetScriptHost) c.host).readTextResource(fdesc.getLeft(), fdesc.getMiddle(), fdesc.getRight());
+                List content = ((CarpetScriptHost) c.host).readTextResource(fdesc);
                 if (content == null) return LazyValue.NULL;
                 retVal = ListValue.wrap(content.stream().map(StringValue::new).collect(Collectors.toList()));
             }
@@ -937,32 +901,29 @@ public class Auxiliary {
         });
 
         expression.addLazyFunction("delete_file", 2, (c, t, lv) -> {
-            Triple fdesc = getFileDescriptor(lv, c, false);
-            return ((CarpetScriptHost) c.host).removeResourceFile(fdesc.getLeft(), fdesc.getRight(), fdesc.getMiddle())
-                    ? LazyValue.TRUE
-                    : LazyValue.FALSE;
+            FileArgument fdesc = FileArgument.from(lv, c, false, FileArgument.Reason.DELETE);
+            return ((CarpetScriptHost) c.host).removeResourceFile(fdesc) ? 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");
-            Triple fdesc = getFileDescriptor(lv, c, false);
-
+            FileArgument fdesc = FileArgument.from(lv, c, false, FileArgument.Reason.CREATE);
 
             boolean success;
-            if (fdesc.getMiddle().equals("nbt"))
+            if (fdesc.type == FileArgument.Type.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) c.host).writeTagFile(tag, fdesc.getLeft(), fdesc.getRight());
+                success = ((CarpetScriptHost) c.host).writeTagFile(tag, fdesc);
             }
-            else if (fdesc.getMiddle().equals("json"))
+            else if (fdesc.type == FileArgument.Type.JSON)
             {
                 List data = Collections.singletonList(GSON.toJson(lv.get(2).evalValue(c).toJson()));
-                ((CarpetScriptHost) c.host).removeResourceFile(fdesc.getLeft(), fdesc.getRight(), fdesc.getMiddle());
-                success = ((CarpetScriptHost) c.host).appendLogFile(fdesc.getLeft(), fdesc.getRight(), fdesc.getMiddle(), data);
+                ((CarpetScriptHost) c.host).removeResourceFile(fdesc);
+                success = ((CarpetScriptHost) c.host).appendLogFile(fdesc, data);
             }
             else
             {
@@ -987,34 +948,22 @@ public class Auxiliary {
                         data.add(lv.get(i).evalValue(c).getString());
                     }
                 }
-                success = ((CarpetScriptHost) c.host).appendLogFile(fdesc.getLeft(), fdesc.getRight(), fdesc.getMiddle(), data);
+                success = ((CarpetScriptHost) c.host).appendLogFile(fdesc, data);
             }
             return success?LazyValue.TRUE:LazyValue.FALSE;
         });
 
-        //write_file
-
         expression.addLazyFunction("load_app_data", -1, (c, t, lv) ->
         {
-            String file = null;
-            boolean shared = false;
+            FileArgument fdesc = new FileArgument(null, FileArgument.Type.NBT, null, false, false, FileArgument.Reason.READ);
             if (lv.size()>0)
             {
                 c.host.issueDeprecation("load_app_data(...) with arguments");
-                file = recognizeResource(lv.get(0).evalValue(c), false);
-                if (lv.size() > 1)
-                {
-                    shared = lv.get(1).evalValue(c).getBoolean();
-                }
-            }
-            Tag state;
-            try
-            {
-                state = ((CarpetScriptHost)((CarpetContext)c).host).readFileTag(file, shared);
-            } catch (CrashException e)
-            {
-                throw new ThrowStatement("app data", Throwables.NBT_READ);
+                String resource = recognizeResource(lv.get(0).evalValue(c), false);
+                boolean shared = lv.size() > 1 && lv.get(1).evalValue(c).getBoolean();
+                fdesc = new FileArgument(resource, FileArgument.Type.NBT, null, false, shared, FileArgument.Reason.READ);
             }
+            Tag state = ((CarpetScriptHost) c.host).readFileTag(fdesc);
             if (state == null)
                 return (cc, tt) -> Value.NULL;
             Value retVal = new NBTSerializableValue(state);
@@ -1026,22 +975,19 @@ public class Auxiliary {
             if (lv.size() == 0)
                 throw new InternalExpressionException("'store_app_data' needs NBT tag and an optional file");
             Value val = lv.get(0).evalValue(c);
-            String file = null;
-            boolean shared = false;
+            FileArgument fdesc = new FileArgument(null, FileArgument.Type.NBT, null, false, false, FileArgument.Reason.CREATE);
             if (lv.size()>1)
             {
                 c.host.issueDeprecation("store_app_data(...) with more than one argument");
-                file = recognizeResource(lv.get(1).evalValue(c), false);
-                if (lv.size() > 2)
-                {
-                    shared = lv.get(2).evalValue(c).getBoolean();
-                }
+                String resource = recognizeResource(lv.get(1).evalValue(c), false);
+                boolean shared = lv.size() > 2 && lv.get(2).evalValue(c).getBoolean();
+                fdesc = new FileArgument(resource, FileArgument.Type.NBT, null, false, shared, FileArgument.Reason.CREATE);
             }
             NBTSerializableValue tagValue =  (val instanceof NBTSerializableValue)
                     ? (NBTSerializableValue) val
                     : new NBTSerializableValue(val.getString());
             Tag tag = tagValue.getTag();
-            boolean success = ((CarpetScriptHost)((CarpetContext)c).host).writeTagFile(tag, file, shared);
+            boolean success = ((CarpetScriptHost) c.host).writeTagFile(tag, fdesc);
             return success?LazyValue.TRUE:LazyValue.FALSE;
         });
 
diff --git a/src/main/java/carpet/script/argument/FileArgument.java b/src/main/java/carpet/script/argument/FileArgument.java
index c3800fa0..037ba75d 100644
--- a/src/main/java/carpet/script/argument/FileArgument.java
+++ b/src/main/java/carpet/script/argument/FileArgument.java
@@ -5,8 +5,14 @@ import carpet.script.Context;
 import carpet.script.LazyValue;
 import carpet.script.bundled.Module;
 import carpet.script.exception.InternalExpressionException;
+import carpet.script.exception.ThrowStatement;
+import carpet.script.exception.Throwables;
+import carpet.script.value.MapValue;
+import carpet.script.value.StringValue;
 import carpet.script.value.Value;
+import com.google.common.collect.ImmutableMap;
 import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
 import com.google.gson.JsonParser;
 import com.google.gson.stream.JsonReader;
 import net.minecraft.nbt.CompoundTag;
@@ -15,24 +21,30 @@ import net.minecraft.nbt.NbtTagSizeTracker;
 import net.minecraft.nbt.Tag;
 import net.minecraft.nbt.TagReaders;
 import net.minecraft.util.WorldSavePath;
+import net.minecraft.util.crash.CrashException;
 import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.tuple.Pair;
 
+import java.io.BufferedInputStream;
 import java.io.BufferedReader;
 import java.io.BufferedWriter;
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
+import java.net.URI;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystemNotFoundException;
+import java.nio.file.FileSystems;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -47,9 +59,22 @@ public class FileArgument
     public boolean isFolder;
     public boolean isShared;
     public Reason reason;
+    private FileSystem zfs;
+    private Path zipPath;
 
     public final static Object writeIOSync = new Object();
 
+    public void close() {
+        if (zfs != null && zfs.isOpen()) {
+            try {
+                zfs.close();
+            } catch (IOException e) {
+                throw new InternalExpressionException("Unable to close zip container: "+zipContainer);
+            }
+            zfs = null;
+        }
+    }
+
     public enum Type
     {
         RAW("raw", ".txt"),
@@ -75,7 +100,7 @@ public class FileArgument
         READ, CREATE, DELETE
     }
 
-    private FileArgument(String resource, Type type, String zipContainer, boolean isFolder, boolean isShared, Reason reason)
+    public FileArgument(String resource, Type type, String zipContainer, boolean isFolder, boolean isShared, Reason reason)
     {
         this.resource = resource;
         this.type = type;
@@ -83,47 +108,103 @@ public class FileArgument
         this.isFolder = isFolder;
         this.isShared = isShared;
         this.reason = reason;
+        this.zfs = null;
+        this.zipPath = null;
+    }
+
+    @Override
+    public String toString() {
+        return "path: "+resource+" zip: "+zipContainer+" type: "+type.id+" folder: "+isFolder+" shared: "+isShared+" reason: "+reason.toString();
     }
 
     public static FileArgument from(List lv, Context c, boolean isFolder, Reason reason)
     {
         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), isFolder);
+        Pair resource = recognizeResource(lv.get(0).evalValue(c), isFolder);
         String origtype = lv.get(1).evalValue(c).getString().toLowerCase(Locale.ROOT);
         boolean shared = origtype.startsWith("shared_");
         String typeString = shared ? origtype.substring(7) : origtype; //len(shared_)
         Type type = Type.of.get(typeString);
-        if (!Type.of.containsKey(typeString))
+        if (type==null)
             throw new InternalExpressionException("Unsupported file type: "+origtype);
-
         if (type==Type.FOLDER && !isFolder)
             throw new InternalExpressionException("Folder types are no supported for this IO function");
-        return  new FileArgument(resource, type,null, isFolder, shared, reason);
+        return  new FileArgument(resource.getLeft(), type,resource.getRight(), isFolder, shared, reason);
 
     }
 
-    public static String recognizeResource(Value value, boolean isFolder)
+    public static Pair recognizeResource(Value value, boolean isFolder)
     {
         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() && !isFolder)
+        String[] pathElements = origfile.toLowerCase(Locale.ROOT).split("[/\\\\]+");
+        List path = new ArrayList<>();
+        String zipPath = null;
+        for (String token : pathElements)
         {
-            throw new InternalExpressionException("Cannot use "+origfile+" as resource name - must have some letters and numbers");
+            boolean isZip = token.endsWith(".zip");
+            if (zipPath != null && isZip) throw new InternalExpressionException(token+" indicates zip access in an already zipped location "+zipPath);
+            if (isZip) token = token.substring(0, token.length()-4);
+            token = token.replaceAll("[^A-Za-z0-9\\-+_]", "");
+            if (token.isEmpty()) continue;
+            if (isZip) token = token + ".zip";
+            path.add(token);
+            if (isZip)
+            {
+                zipPath = String.join("/", path);
+                path.clear();
+            }
         }
-        return file;
+        if (path.isEmpty() && !isFolder)
+            throw new InternalExpressionException(
+                    "Cannot use "+origfile+" as resource name: indicated path is empty"+((zipPath==null)?"":" in zip container "+zipPath)
+            );
+        return Pair.of(String.join("/", path), zipPath);
+    }
+
+    private Path resolve(String suffix)
+    {
+        if (CarpetServer.minecraft_server == null)
+            throw new InternalExpressionException("Accessing world files without server running");
+        return CarpetServer.minecraft_server.getSavePath(WorldSavePath.ROOT).resolve("scripts/"+suffix);
     }
 
     private Path toPath(Module module)
     {
         if (!isShared && (module == null || module.getName() == null)) return null;
-        return CarpetServer.minecraft_server.getSavePath(WorldSavePath.ROOT).resolve("scripts/"+getDescriptor(module, resource)+type.extension);
+        if (zipContainer == null)
+            return resolve(getDescriptor(module, resource)+(isFolder?"":type.extension));
+        else
+        {
+            if (zfs == null)
+            {
+                Map env = new HashMap<>();
+                if (reason == Reason.CREATE) env.put("create", "true");
+                zipPath = resolve(getDescriptor(module, zipContainer));
+                try {
+                    zfs = FileSystems.newFileSystem(URI.create("jar:"+ zipPath.toUri().toString()), env);
+                }
+                catch (FileSystemNotFoundException fsnfe)
+                {
+                    return null;
+                }
+                catch (IOException e)
+                {
+                    throw new ThrowStatement("Unable to open zip file: "+zipContainer, Throwables.IO_EXCEPTION);
+                }
+            }
+            return zfs.getPath(resource+(isFolder?"/":type.extension));
+        }
     }
 
     private Path moduleRootPath(Module module)
     {
         if (!isShared && (module == null || module.getName() == null)) return null;
-        return CarpetServer.minecraft_server.getSavePath(WorldSavePath.ROOT).resolve("scripts/"+getDescriptor(module, ""));
+        return resolve(isShared?"shared":module.getName()+".data");
+    }
+
+    public String getDisplayPath()
+    {
+        return (isShared?"shared/":"")+(zipContainer!=null?zipContainer+"/":"")+resource+type.extension;
     }
 
     private String getDescriptor(Module module, String res)
@@ -136,7 +217,7 @@ public class FileArgument
         {
             return module.getName()+".data"+(res==null || res.isEmpty()?"":"/"+res);
         }
-        throw  new RuntimeException("Invalid file descriptor: "+res);
+        throw new InternalExpressionException("Invalid file descriptor: "+res);
     }
 
     public Stream listFiles(Module module)
@@ -146,8 +227,7 @@ public class FileArgument
         String ext = type.extension;
         try
         {
-            return Files.list(dir).
-                    filter(path -> isFolder?Files.isDirectory(path):path.toString().endsWith(ext));
+            return Files.list(dir).filter(path -> (type==Type.FOLDER)?path.toString().endsWith("/"):path.toString().endsWith(ext));
         }
         catch (IOException ignored)
         {
@@ -157,73 +237,106 @@ public class FileArgument
 
     public Stream listFolder(Module module)
     {
-        Stream result;
-        synchronized (writeIOSync) { result = listFiles(module); }
-        if (result == null) return null;
-        Path rootPath = moduleRootPath(module);
-        Stream strings = result.map(p -> rootPath.relativize(p).toString().replaceAll("[\\\\/]","/"));
-        if (type != Type.FOLDER)
-            return strings.map(FilenameUtils::removeExtension);
-        return strings;
+        Stream strings;
+        try { synchronized (writeIOSync)
+        {
+            Stream result;
+            result = listFiles(module);
+            if (result == null) return null;
+            Path rootPath = moduleRootPath(module);
+            if (rootPath == null) return null;
+            String zipComponent = (zipContainer != null) ? rootPath.relativize(zipPath).toString() : null;
+            strings = result.map(p -> {
+                if (zipContainer == null)
+                    return rootPath.relativize(p).toString().replaceAll("[\\\\/]+", "/");
+                return (zipComponent + p.toString()).replaceAll("[\\\\/]+", "/");
+            });
+        } }
+        finally
+        {
+            close();
+        }
+        if (type == Type.FOLDER)
+            return strings.map(s -> s.endsWith("/")?s.substring(0, s.length()-1):s);
+        return strings.map(FilenameUtils::removeExtension);
+    }
+
+    private void createPaths(Path file)
+    {
+        try {
+            if ((zipContainer == null || file.getParent() != null) &&
+                    !Files.exists(file.getParent()) &&
+                    Files.createDirectories(file.getParent()) == null
+            ) throw new IOException();
+        } catch (IOException e) {
+            throw new ThrowStatement("Unable to create paths for "+file.toString(), Throwables.IO_EXCEPTION);
+        }
+
     }
 
     public boolean appendToTextFile(Module module, List message)
     {
-        Path dataFile = toPath(module);//, resourceName, supportedTypes.get(type), isShared);
-        if (dataFile == null) return false;
-        if (!Files.exists(dataFile.getParent()) && !dataFile.toFile().getParentFile().mkdirs()) return false;
-        synchronized (writeIOSync)
+        try { synchronized (writeIOSync)
         {
-            try
+            Path dataFile = toPath(module);//, resourceName, supportedTypes.get(type), isShared);
+            if (dataFile == null) return false;
+            createPaths(dataFile);
+            OutputStream out = Files.newOutputStream(dataFile, StandardOpenOption.APPEND, StandardOpenOption.CREATE);
+            try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)))
             {
-                OutputStream out = Files.newOutputStream(dataFile, StandardOpenOption.APPEND, StandardOpenOption.CREATE);
-                try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)))
+                for (String line: message)
                 {
-                    for (String line: message)
-                    {
-                        writer.append(line);
-                        if (type == Type.TEXT) writer.newLine();
-                    }
+                    writer.append(line);
+                    if (type == Type.TEXT) writer.newLine();
                 }
             }
-            catch (IOException e)
-            {
-                return false;
-            }
-            return true;
+        } }
+        catch (IOException e)
+        {
+            throw new ThrowStatement("Error when writing to the file: "+e, Throwables.IO_EXCEPTION);
         }
+        finally
+        {
+            close();
+        }
+        return true;
     }
 
     public Tag getNbtData(Module module) // aka getData
     {
-        Path dataFile = toPath(module);
-        if (dataFile == null) return null;
-        if (!Files.exists(dataFile) || !(dataFile.toFile().isFile())) return null;
-        synchronized (writeIOSync) { return readTag(dataFile.toFile()); }
+        try { synchronized (writeIOSync) {
+            Path dataFile = toPath(module);
+            if (dataFile == null || !Files.exists(dataFile)) return null;
+            return readTag(dataFile);
+        } }
+        finally {
+            close();
+        }
     }
 
     //copied private method from net.minecraft.nbt.NbtIo.read()
     // to read non-compound tags - these won't be compressed
-    public static Tag readTag(File file)
+    public static Tag readTag(Path path)
     {
         try
         {
-            return NbtIo.readCompressed(file);
+            return NbtIo.readCompressed(Files.newInputStream(path));
         }
         catch (IOException e)
         {
             // Copy of NbtIo.read(File) because that's now client-side only
-            if (!file.exists())
+            if (!Files.exists(path))
             {
                 return null;
             }
-            try (DataInputStream in = new DataInputStream(new FileInputStream(file)))
+            try (DataInputStream in = new DataInputStream(new BufferedInputStream(Files.newInputStream(path))))
             {
                 return NbtIo.read(in);
             }
             catch (IOException ioException)
             {
-                try (DataInputStream dataInput_1 = new DataInputStream(new FileInputStream(file)))
+                // not compressed compound tag neither uncompressed compound tag - trying any type of a tag
+                try (DataInputStream dataInput_1 = new DataInputStream(new BufferedInputStream(Files.newInputStream(path))))
                 {
                     byte byte_1 = dataInput_1.readByte();
                     if (byte_1 == 0)
@@ -238,71 +351,101 @@ public class FileArgument
                 }
                 catch (IOException ignored)
                 {
+                    throw new ThrowStatement("Not a valid NBT tag in "+path.toString(), Throwables.NBT_ERROR);
                 }
-                return null;
             }
         }
+        catch (CrashException e)
+        {
+            throw new ThrowStatement("Error when reading NBT file "+path.toString(), Throwables.NBT_ERROR);
+        }
     }
 
     public boolean saveNbtData(Module module, Tag tag) // aka saveData
     {
-        Path dataFile = toPath(module);
-        if (dataFile == null) return false;
-        if (!Files.exists(dataFile.getParent()) && !dataFile.toFile().getParentFile().mkdirs()) return false;
-        synchronized (writeIOSync) { return writeTag(tag, dataFile.toFile()); }
+        try { synchronized (writeIOSync) {
+            Path dataFile = toPath(module);
+            if (dataFile == null) return false;
+            createPaths(dataFile);
+            return writeTagDisk(tag, dataFile, zipContainer != null);
+        } }
+        finally
+        {
+            close();
+        }
     }
 
     //copied private method from net.minecraft.nbt.NbtIo.write() and client method safe_write
-    public static boolean writeTag(Tag tag_1, File file)
+    public static boolean writeTagDisk(Tag tag_1, Path path, boolean zipped)
     {
-        File file_2 = new File(file.getAbsolutePath() + "_tmp");
-        if (file_2.exists()) file_2.delete();
+        Path original = path;
+        try
+        {
+            if (!zipped)
+            {
+                path = new File(path.toFile().getAbsolutePath() + "_tmp").toPath();
+                Files.deleteIfExists(path);
+            }
 
-        if (tag_1 instanceof CompoundTag)
-        {
-            try
+            if (tag_1 instanceof CompoundTag)
             {
-                NbtIo.writeCompressed((CompoundTag) tag_1, file_2);
+                NbtIo.writeCompressed((CompoundTag) tag_1, Files.newOutputStream(path));
             }
-            catch (IOException e)
+            else
             {
-                return false;
-            }
-        }
-        else
-        {
-            try (DataOutputStream dataOutputStream_1 = new DataOutputStream(new FileOutputStream(file_2)))
-            {
-                dataOutputStream_1.writeByte(tag_1.getType());
-                if (tag_1.getType() != 0)
+                try (DataOutputStream dataOutputStream_1 = new DataOutputStream(Files.newOutputStream(path)))
                 {
-                    dataOutputStream_1.writeUTF("");
-                    tag_1.write(dataOutputStream_1);
+                    dataOutputStream_1.writeByte(tag_1.getType());
+                    if (tag_1.getType() != 0)
+                    {
+                        dataOutputStream_1.writeUTF("");
+                        tag_1.write(dataOutputStream_1);
+                    }
                 }
             }
-            catch (IOException e)
+            if (!zipped)
             {
-                return false;
+                Files.deleteIfExists(original);
+                Files.move(path, original);
             }
+            return true;
+        }
+        catch (IOException e)
+        {
+            throw new ThrowStatement("Unable to write tag to "+original.toString(), Throwables.IO_EXCEPTION);
         }
-        if (file.exists()) file.delete();
-        if (!file.exists()) file_2.renameTo(file);
-        return true;
     }
 
     public boolean dropExistingFile(Module module)
     {
-        Path dataFile = toPath(module);
-        if (dataFile == null) return false;
-        synchronized (writeIOSync) { return dataFile.toFile().delete(); }
+
+        try { synchronized (writeIOSync)
+        {
+            Path dataFile = toPath(module);
+            if (dataFile == null) return false;
+            return Files.deleteIfExists(dataFile);
+        } }
+        catch (IOException e)
+        {
+            throw new ThrowStatement("Error while removing file: "+getDisplayPath(), Throwables.IO_EXCEPTION);
+        }
+        finally
+        {
+            close();
+        }
     }
 
     public List listFile(Module module)
     {
-        Path dataFile = toPath(module);
-        if (dataFile == null) return null;
-        if (!dataFile.toFile().exists()) return null;
-        synchronized (writeIOSync) { return listFileContent(dataFile); }
+        try { synchronized (writeIOSync) {
+            Path dataFile = toPath(module);
+            if (dataFile == null) return null;
+            if (!Files.exists(dataFile)) return null;
+            return listFileContent(dataFile);
+        } }
+        finally {
+            close();
+        }
     }
 
     public static List listFileContent(Path filePath)
@@ -319,15 +462,19 @@ public class FileArgument
         }
         catch (IOException e)
         {
-            return null;
+            throw new ThrowStatement("Failed to read text file "+filePath.toString(), Throwables.IO_EXCEPTION);
         }
     }
 
     public JsonElement readJsonFile(Module module) {
-        Path dataFile = toPath(module);
-        if (dataFile == null) return null;
-        if (!dataFile.toFile().exists()) return null;
-        synchronized (writeIOSync) { return readJsonContent(dataFile); }
+        try { synchronized (writeIOSync) {
+            Path dataFile = toPath(module);
+            if (dataFile == null || !Files.exists(dataFile)) return null;
+            return readJsonContent(dataFile);
+        } }
+        finally {
+            close();
+        }
     }
 
     public static JsonElement readJsonContent(Path filePath)
@@ -336,9 +483,19 @@ public class FileArgument
         {
             return new JsonParser().parse(new JsonReader(reader));
         }
+        catch (JsonParseException e)
+        {
+            Throwable exc = e;
+            if(e.getCause() != null)
+                exc = e.getCause();
+            throw new ThrowStatement(MapValue.wrap(ImmutableMap.of(
+                    StringValue.of("error"), StringValue.of(exc.getMessage()),
+                    StringValue.of("path"), StringValue.of(filePath.toString())
+            )), Throwables.JSON_ERROR);
+        }
         catch (IOException e)
         {
-            return null;
+            throw new ThrowStatement("Failed to read json file content "+filePath.toString(), Throwables.IO_EXCEPTION);
         }
     }
 
diff --git a/src/main/java/carpet/script/bundled/FileModule.java b/src/main/java/carpet/script/bundled/FileModule.java
index d6bb5ae5..23d558f5 100644
--- a/src/main/java/carpet/script/bundled/FileModule.java
+++ b/src/main/java/carpet/script/bundled/FileModule.java
@@ -1,33 +1,10 @@
 package carpet.script.bundled;
 
-import net.minecraft.nbt.CompoundTag;
-import net.minecraft.nbt.NbtIo;
-import net.minecraft.nbt.NbtTagSizeTracker;
-import net.minecraft.nbt.Tag;
-import net.minecraft.nbt.TagReaders;
-
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
-import java.util.ArrayList;
-import java.util.List;
 import java.util.Locale;
-import java.util.stream.Stream;
-
-import com.google.gson.JsonElement;
-import com.google.gson.JsonParser;
-import com.google.gson.stream.JsonReader;
 
 public class FileModule extends Module
 {
@@ -66,155 +43,4 @@ public class FileModule extends Module
         return library;
     }
 
-    //copied private method from net.minecraft.nbt.NbtIo.read()
-    // to read non-compound tags - these won't be compressed
-    @Deprecated
-    public static Tag read(File file)
-    {
-        try
-        {
-            return NbtIo.readCompressed(file);
-        }
-        catch (IOException e)
-        {
-            // Copy of NbtIo.read(File) because that's now client-side only
-            if (!file.exists())
-            {
-                return null;
-            }
-            try (DataInputStream in = new DataInputStream(new FileInputStream(file)))
-            {
-                return NbtIo.read(in);
-            }
-            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, NbtTagSizeTracker.EMPTY);
-                    }
-                }
-                catch (IOException ignored)
-                {
-                }
-                return null;
-            }
-        }
-    }
-
-    //copied private method from net.minecraft.nbt.NbtIo.write() and client method safe_write
-    @Deprecated
-    public static boolean write(Tag tag_1, File file)
-    {
-        File file_2 = new File(file.getAbsolutePath() + "_tmp");
-        if (file_2.exists()) file_2.delete();
-
-        if (tag_1 instanceof CompoundTag)
-        {
-            try
-            {
-                NbtIo.writeCompressed((CompoundTag) tag_1, file_2);
-            }
-            catch (IOException e)
-            {
-                return false;
-            }
-        }
-        else
-        {
-            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);
-        return true;
-    }
-
-    @Deprecated
-    public static boolean appendText(Path filePath, boolean addNewLines, List data)
-    {
-        try
-        {
-            OutputStream out = Files.newOutputStream(filePath, StandardOpenOption.APPEND, StandardOpenOption.CREATE);
-            try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)))
-            {
-                for (String line: data)
-                {
-                    writer.append(line);
-                    if (addNewLines) writer.newLine();
-                }
-            }
-        }
-        catch (IOException e)
-        {
-            return false;
-        }
-        return true;
-    }
-
-
-    @Deprecated
-    public static List listFileContent(Path filePath)
-    {
-        try (BufferedReader reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8)) {
-            List result = new ArrayList<>();
-            for (;;) {
-                String line = reader.readLine();
-                if (line == null)
-                    break;
-                result.add(line.replaceAll("[\n\r]+",""));
-            }
-            return result;
-        }
-        catch (IOException e)
-        {
-            return null;
-        }
-    }
-
-    @Deprecated
-    public static JsonElement readJsonContent(Path filePath)
-    {
-    	try (BufferedReader reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8))
-        {
-            return new JsonParser().parse(new JsonReader(reader));
-        }
-        catch (IOException e)
-        {
-            return null;
-        }
-    }
-
-    @Deprecated
-    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 ea6231b5..a6d29d85 100644
--- a/src/main/java/carpet/script/bundled/Module.java
+++ b/src/main/java/carpet/script/bundled/Module.java
@@ -1,123 +1,39 @@
 package carpet.script.bundled;
 
 import carpet.CarpetServer;
+import carpet.script.argument.FileArgument;
 import net.minecraft.nbt.Tag;
 import net.minecraft.util.WorldSavePath;
-import org.apache.commons.io.FilenameUtils;
-
-import com.google.common.collect.ImmutableMap;
-import com.google.gson.JsonElement;
 
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Stream;
 
 public abstract class Module
 {
     public abstract String getName();
     public abstract String getCode();
     public abstract boolean isLibrary();
-    @Deprecated
-    public final static Object writeIOSync = new Object();
-    @Deprecated
-    public final static Map supportedTypes = ImmutableMap.of(
-            "raw", ".txt",
-            "text", ".txt",
-            "nbt", ".nbt",
-            "json", ".json",
-            "folder", "folder"
-    );
 
-    @Deprecated
-    private static String getDescriptor(Module module, String file, boolean isShared)
+    public static Tag getData(Module module)
     {
-        if (isShared)
-        {
-            return file.isEmpty()?"shared":"shared/"+file;
-        }
-        if (module != null && module.getName() != null) // appdata
-        {
-            return module.getName()+".data"+(file==null || file.isEmpty()?"":"/"+file);
-        }
-        throw  new RuntimeException("Invalid file descriptor: "+file);
-    }
-
-    @Deprecated
-    public static Tag getData(Module module, String file, boolean isShared)
-    {
-        Path dataFile = resolveResource(module, file, ".nbt", isShared);
+        Path dataFile = resolveResource(module);
         if (dataFile == null) return null;
         if (!Files.exists(dataFile) || !(dataFile.toFile().isFile())) return null;
-        synchronized (writeIOSync) { return FileModule.read(dataFile.toFile()); }
+        synchronized (FileArgument.writeIOSync) { return FileArgument.readTag(dataFile); }
     }
 
-    @Deprecated
-    public static boolean saveData(Module module, String file, Tag globalState, boolean isShared)
+    public static void saveData(Module module, Tag globalState)
     {
-        Path dataFile = resolveResource(module, file, ".nbt", isShared);
-        if (dataFile == null) return false;
-        if (!Files.exists(dataFile.getParent()) && !dataFile.toFile().getParentFile().mkdirs()) return false;
-        synchronized (writeIOSync) { return FileModule.write(globalState, dataFile.toFile()); }
+        Path dataFile = resolveResource(module);
+        if (dataFile == null) return;
+        if (!Files.exists(dataFile.getParent()) && !dataFile.toFile().getParentFile().mkdirs()) return;
+        synchronized (FileArgument.writeIOSync) { FileArgument.writeTagDisk(globalState, dataFile, false); }
     }
 
-    @Deprecated
-    public static boolean appendToTextFile(Module module, String resourceName, String type, boolean isShared, List message)
+    private static Path resolveResource(Module module)
     {
-        Path dataFile = resolveResource(module, resourceName, supportedTypes.get(type), isShared);
-        if (dataFile == null) return false;
-        if (!Files.exists(dataFile.getParent()) && !dataFile.toFile().getParentFile().mkdirs()) return false;
-        synchronized (writeIOSync) { return FileModule.appendText(dataFile, type.equals("text"), message); }
-    }
-
-    @Deprecated
-    public static boolean dropExistingFile(Module module, String resourceName, String type, boolean isShared)
-    {
-        Path dataFile = resolveResource(module, resourceName, supportedTypes.get(type), isShared);
-        if (dataFile == null) return false;
-        synchronized (writeIOSync) { return dataFile.toFile().delete(); }
-    }
-
-    @Deprecated
-    public static List listFile(Module module, String resourceName, String type, boolean isShared)
-    {
-        Path dataFile = resolveResource(module, resourceName, supportedTypes.get(type), isShared);
-        if (dataFile == null) return null;
-        if (!dataFile.toFile().exists()) return null;
-        synchronized (writeIOSync) { return FileModule.listFileContent(dataFile); }
-    }
-
-    @Deprecated
-    public static JsonElement readJsonFile(Module module, String resourceName, String type, boolean isShared) {
-        Path dataFile = resolveResource(module, resourceName, supportedTypes.get(type), isShared);
-        if (dataFile == null) return null;
-        if (!dataFile.toFile().exists()) return null;
-        synchronized (writeIOSync) { return FileModule.readJsonContent(dataFile); }
-    }
-
-    @Deprecated
-    public static Stream listFolder(Module module, String resourceName, String type, boolean isShared)
-    {
-        Path dir = resolveResource(module, resourceName, null, isShared);
-        String ext = supportedTypes.get(type);
-        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;
-    }
-
-    @Deprecated
-    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==null?"":ext));
+        if (module == null || module.getName() == null) return null; // commandline app
+        return CarpetServer.minecraft_server.getSavePath(WorldSavePath.ROOT).resolve("scripts/"+module.getName()+".data.nbt");
     }
 
     @Override
diff --git a/src/main/java/carpet/script/exception/Throwables.java b/src/main/java/carpet/script/exception/Throwables.java
index 22857a41..3f811cfe 100644
--- a/src/main/java/carpet/script/exception/Throwables.java
+++ b/src/main/java/carpet/script/exception/Throwables.java
@@ -25,8 +25,8 @@ public class Throwables {
     public static final Throwables UNKNOWN_STRUCTURE     = register("unknown_structure", VALUE_EXCEPTION);
     public static final Throwables UNKNOWN_CRITERION     = register("unknown_criterion", VALUE_EXCEPTION);
     public static final Throwables IO_EXCEPTION          = register("io_exception", THROWN_EXCEPTION_TYPE);
-    public static final Throwables NBT_READ              = register("nbt_read_error", IO_EXCEPTION);
-    public static final Throwables JSON_READ             = register("json_read_error", IO_EXCEPTION);
+    public static final Throwables NBT_ERROR             = register("nbt_error", IO_EXCEPTION);
+    public static final Throwables JSON_ERROR            = register("json_error", IO_EXCEPTION);
     public static final Throwables USER_DEFINED          = register("user_exception", THROWN_EXCEPTION_TYPE);
 
     /**
diff --git a/src/main/java/carpet/script/language/Loops.java b/src/main/java/carpet/script/language/Loops.java
index 4908a853..ad041cd2 100644
--- a/src/main/java/carpet/script/language/Loops.java
+++ b/src/main/java/carpet/script/language/Loops.java
@@ -119,7 +119,7 @@ public class Loops {
                 c.setVariable("_i", (cc, tt) -> new NumericValue(doYouReally).bindTo("_i"));
                 try
                 {
-                    result.add(expr.evalValue(c));
+                    result.add(expr.evalValue(c, t));
                 }
                 catch (BreakStatement | ContinueStatement stmt)
                 {
diff --git a/src/main/java/carpet/script/utils/SystemInfo.java b/src/main/java/carpet/script/utils/SystemInfo.java
index 79dd6b1a..18c69d3e 100644
--- a/src/main/java/carpet/script/utils/SystemInfo.java
+++ b/src/main/java/carpet/script/utils/SystemInfo.java
@@ -4,6 +4,7 @@ import carpet.CarpetServer;
 import carpet.CarpetSettings;
 import carpet.script.CarpetContext;
 import carpet.script.CarpetScriptHost;
+import carpet.script.value.BooleanValue;
 import carpet.script.value.ListValue;
 import carpet.script.value.MapValue;
 import carpet.script.value.NumericValue;
@@ -59,7 +60,19 @@ public class SystemInfo {
         put("game_view_distance", c -> new NumericValue(c.s.getMinecraftServer().getPlayerManager().getViewDistance()));
         put("game_mod_name", c -> StringValue.of(c.s.getMinecraftServer().getServerModName()));
         put("game_version", c -> StringValue.of(c.s.getMinecraftServer().getVersion()));
+        put("game_target", c -> StringValue.of(SharedConstants.getGameVersion().getReleaseTarget()));
+        put("game_protocol", c -> NumericValue.of(SharedConstants.method_31372()));
+        put("game_major_target", c -> {
+            String [] vers = SharedConstants.getGameVersion().getReleaseTarget().split("\\.");
+            return NumericValue.of((vers.length > 1)?Integer.parseInt(vers[1]):0);
+        });
+        put("game_minor_target", c -> {
+            String [] vers = SharedConstants.getGameVersion().getReleaseTarget().split("\\.");
+            return NumericValue.of((vers.length > 2)?Integer.parseInt(vers[2]):0);
+        });
+        put("game_stable", c -> BooleanValue.of(SharedConstants.getGameVersion().isStable()));
         put("game_data_version", c->NumericValue.of(SharedConstants.getGameVersion().getWorldVersion()));
+        put("game_pack_version", c->NumericValue.of(SharedConstants.getGameVersion().getPackVersion()));
 
         put("server_ip", c -> StringValue.of(c.s.getMinecraftServer().getServerIp()));
         put("server_whitelisted", c -> new NumericValue(c.s.getMinecraftServer().isEnforceWhitelist()));
diff --git a/src/main/java/carpet/script/value/BooleanValue.java b/src/main/java/carpet/script/value/BooleanValue.java
index ea67f11b..19a9bc05 100644
--- a/src/main/java/carpet/script/value/BooleanValue.java
+++ b/src/main/java/carpet/script/value/BooleanValue.java
@@ -16,6 +16,11 @@ public class BooleanValue extends NumericValue
         boolValue = boolval;
     }
 
+    public static BooleanValue of(boolean value)
+    {
+        return value?TRUE:FALSE;
+    }
+
     @Override
     public String getString() {
         return boolValue?"true":"false";