diff --git a/docs/scarpet/Full.md b/docs/scarpet/Full.md index 9ff30fca..9478e09e 100644 --- a/docs/scarpet/Full.md +++ b/docs/scarpet/Full.md @@ -151,9 +151,10 @@ have memory and act like objects so to speak. Check `outer(var)` for details. ## Code delivery, line indicators Note that this should only apply to pasting your code to execute with commandblock. Scarpet recommends placing -your code in apps (files with `.sc` extension that can be placed inside "/scripts" folder in the world files and -loaded as a scarpet app with command `/script load [app_name]`. Scarpet apps loaded from disk should only -contain code, no need to start with "/script run" prefix +your code in apps (files with `.sc` extension that can be placed inside `/scripts` folder in the world files +or as a globally available app in singleplayer in the `.minecraft/config/carpet/scripts` folder and loaded +as a Scarpet app with the command `/script load [app_name]`. Scarpet apps loaded from disk should only +contain code, no need to start with `/script run` prefix. The following is the code that could be provided in a `foo.sc` app file located in world `/scripts` folder @@ -1705,7 +1706,8 @@ Here is the gist of the Minecraft related functions. Otherwise the CarpetScript ## App structure The main delivery method for scarpet programs into the game is in the form of apps in `*.sc` files located in the world `scripts` -folder. When loaded (via `/script load` command, etc.), the game will run the content of the app once, regardless of its scope +folder. In singleplayer, you can also save apps in `.minecraft/config/carpet/scripts` for them to be available in any world. +When loaded (via `/script load` command, etc.), the game will run the content of the app once, regardless of its scope (more about the app scopes below), without executing of any functions, unless called directly, and with the exception of the `__config()` function, if present, which will be executed once. Loading the app will also bind specific events to the event system (check Events section for details). @@ -1739,7 +1741,7 @@ running anything in the global scope for a `'player'` scoped app is not recommen stay loaded after startup. Otherwise, after reading the app the first time, and fetching the config, server will drop them down. This is to allow to store multiple apps on the server/world and selectively decide which one should be running at startup. WARNING: all apps will run once at startup anyways, so be aware that their actions that are called -statically, will be performed once anyways. +statically, will be performed once anyways. Only apps present in the world's `scripts` folder will be autoloaded. * `'legacy_command_type_support'` - if `true`, and the app defines the legacy command system via `__command()` function, all parameters of command functions will be interpreted and used using brigadier / vanilla style argument parser and their type will be inferred from their names, otherwise @@ -1904,6 +1906,20 @@ Here is a list of built-in types, with their return value formats, as well as a * `'pos'`: block position as a triple of coordinates. Customized with `'loaded'`, if true requiring the position to be loaded. * `'block'`: a valid block state wrapped in a block value (including block properties and data) + * `'blockpredicate`': returns a 4-tuple indicating conditions of a block to match: block name, block tag, + map of required state properties, and tag to match. Either block name or block tag are `null` but not both. + Property map is always specified, but its empty for no conditions, and matching nbt tag can be `null` indicating + no requirements. Technically the 'all-matching' predicate would be `[null, null, {}, null]`, but + block name or block tag is always specified. One can use the following routine to match a block agains this predicate: + ``` + block_to_match = block(x,y,z); + [block_name, block_tag, properties, nbt_tag] = block_predicate; + + (block_name == null || block_name == block_to_match) && + (block_tag == null || block_tags(block_to_match, block_tag)) && + all(properties, block_state(block_to_match, _) == properties:_) && + (!tag || tag_matches(block_data(block_to_match), tag)) + ``` * `'teamcolor'`: name of a team, and an integer color value of one of 16 valid team colors. * `'columnpos'`: a pair of x and z coordinates. * `'dimension'`: string representing a valid dimension in the world. @@ -4795,13 +4811,15 @@ to simulate running commands in a different scope. # `/script load / unload (global?)`, `/script in ` commands -`load / unload` commands allow for very conventient way of writing your code, providing it to the game and -distribute with your worlds without the need of use of commandblocks. Just place your scarpet code in the -/scripts folder of your world files and make sure it ends with `.sc` extension. The good thing about editing that -code is that you can no only use normal editing without the need of marking of newlines, +`load / unload` commands allow for very convenient way of writing your code, providing it to the game and +distribute with your worlds without the need of use of commandblocks. Just place your Scarpet code in the +`/scripts` folder of your world files and make sure it ends with `.sc` extension. In singleplayer, you can +also save your scripts in `.minecraft/config/carpet/scripts` to make them available in any world. + +The good thing about editing that code is that you can not only use normal editing without the need of marking of newlines, but you can also use comments in your code. -a comment is anything that starts with a double slash, and continues to the end of the line: +A comment is anything that starts with a double slash, and continues to the end of the line:
 foo = 1;
diff --git a/docs/scarpet/api/Overview.md b/docs/scarpet/api/Overview.md
index 9e9fc5bd..f81da3a5 100644
--- a/docs/scarpet/api/Overview.md
+++ b/docs/scarpet/api/Overview.md
@@ -6,7 +6,8 @@ Here is the gist of the Minecraft related functions. Otherwise the CarpetScript
 ## App structure
 
 The main delivery method for scarpet programs into the game is in the form of apps in `*.sc` files located in the world `scripts` 
-folder. When loaded (via `/script load` command, etc.), the game will run the content of the app once, regardless of its scope
+folder. In singleplayer, you can also save apps in `.minecraft/config/carpet/scripts` for them to be available in any world. 
+When loaded (via `/script load` command, etc.), the game will run the content of the app once, regardless of its scope
 (more about the app scopes below), without executing of any functions, unless called directly, and with the exception of the
 `__config()` function, if present, which will be executed once. Loading the app will also bind specific 
 events to the event system (check Events section for details).
@@ -40,7 +41,7 @@ running anything in the global scope for a `'player'` scoped app is not recommen
 stay loaded after startup. Otherwise, after reading the app the first time, and fetching the config, server will drop them down. 
 This is to allow to store multiple apps on the server/world and selectively decide which one should be running at 
 startup. WARNING: all apps will run once at startup anyways, so be aware that their actions that are called 
-statically, will be performed once anyways.
+statically, will be performed once anyways. Only apps present in the world's `scripts` folder will be autoloaded.
 *   `'legacy_command_type_support'` - if `true`, and the app defines the legacy command system via `__command()` function,
 all parameters of command functions will be interpreted and used using brigadier / vanilla style argument parser and their type
 will be inferred from their names, otherwise
@@ -205,6 +206,20 @@ Here is a list of built-in types, with their return value formats, as well as a
   * `'pos'`: block position as a triple of coordinates. Customized with `'loaded'`, if true requiring the position 
   to be loaded.
   * `'block'`: a valid block state wrapped in a block value (including block properties and data)
+  * `'blockpredicate`': returns a 4-tuple indicating conditions of a block to match: block name, block tag,
+  map of required state properties, and tag to match. Either block name or block tag are `null` but not both.
+  Property map is always specified, but its empty for no conditions, and matching nbt tag can be `null` indicating
+  no requirements. Technically the 'all-matching' predicate would be `[null, null, {}, null]`, but 
+  block name or block tag is always specified. One can use the following routine to match a block agains this predicate:
+  ```
+    block_to_match = block(x,y,z);
+    [block_name, block_tag, properties, nbt_tag] = block_predicate;
+   
+    (block_name == null || block_name == block_to_match) &&
+    (block_tag == null || block_tags(block_to_match, block_tag)) &&
+    all(properties, block_state(block_to_match, _) == properties:_) &&
+    (!tag || tag_matches(block_data(block_to_match), tag))
+  ```
   * `'teamcolor'`: name of a team, and an integer color value of one of 16 valid team colors.
   * `'columnpos'`: a pair of x and z coordinates.
   * `'dimension'`: string representing a valid dimension in the world.
diff --git a/docs/scarpet/api/ScriptCommand.md b/docs/scarpet/api/ScriptCommand.md
index b10b5dc6..f2f2aa6a 100644
--- a/docs/scarpet/api/ScriptCommand.md
+++ b/docs/scarpet/api/ScriptCommand.md
@@ -8,13 +8,15 @@ to simulate running commands in a different scope.
 
 # `/script load / unload  (global?)`, `/script in ` commands
 
-`load / unload` commands allow for very conventient way of writing your code, providing it to the game and 
-distribute with your worlds without the need of use of commandblocks. Just place your scarpet code in the 
-/scripts folder of your world files and make sure it ends with `.sc` extension. The good thing about editing that 
-code is that you can no only use normal editing without the need of marking of newlines, 
+`load / unload` commands allow for very convenient way of writing your code, providing it to the game and 
+distribute with your worlds without the need of use of commandblocks. Just place your Scarpet code in the 
+`/scripts` folder of your world files and make sure it ends with `.sc` extension. In singleplayer, you can 
+also save your scripts in `.minecraft/config/carpet/scripts` to make them available in any world.
+
+The good thing about editing that code is that you can not only use normal editing without the need of marking of newlines,  
 but you can also use comments in your code.
 
-a comment is anything that starts with a double slash, and continues to the end of the line:
+A comment is anything that starts with a double slash, and continues to the end of the line:
 
 
 foo = 1;
diff --git a/docs/scarpet/language/Overview.md b/docs/scarpet/language/Overview.md
index 64c3d81d..0030ad73 100644
--- a/docs/scarpet/language/Overview.md
+++ b/docs/scarpet/language/Overview.md
@@ -151,9 +151,10 @@ have memory and act like objects so to speak. Check `outer(var)` for details.
 ## Code delivery, line indicators
 
 Note that this should only apply to pasting your code to execute with commandblock. Scarpet recommends placing 
-your code in apps (files with `.sc` extension that can be placed inside "/scripts" folder in the world files and 
-loaded as a scarpet app with command `/script load [app_name]`. Scarpet apps loaded from disk should only 
-contain code, no need to start with "/script run" prefix
+your code in apps (files with `.sc` extension that can be placed inside `/scripts` folder in the world files 
+or as a globally available app in singleplayer in the `.minecraft/config/carpet/scripts` folder and loaded 
+as a Scarpet app with the command `/script load [app_name]`. Scarpet apps loaded from disk should only 
+contain code, no need to start with `/script run` prefix.
 
 The following is the code that could be provided in a `foo.sc` app file located in world `/scripts` folder
 
diff --git a/src/main/java/carpet/patches/EntityPlayerMPFake.java b/src/main/java/carpet/patches/EntityPlayerMPFake.java
index 82c29d17..911a4234 100644
--- a/src/main/java/carpet/patches/EntityPlayerMPFake.java
+++ b/src/main/java/carpet/patches/EntityPlayerMPFake.java
@@ -120,4 +120,10 @@ public class EntityPlayerMPFake extends ServerPlayerEntity
         this.hungerManager = new HungerManager();
         kill();
     }
+
+    @Override
+    public String getIp()
+    {
+        return "127.0.0.1";
+    }
 }
diff --git a/src/main/java/carpet/script/CarpetScriptServer.java b/src/main/java/carpet/script/CarpetScriptServer.java
index 69fd0327..e450460b 100644
--- a/src/main/java/carpet/script/CarpetScriptServer.java
+++ b/src/main/java/carpet/script/CarpetScriptServer.java
@@ -16,19 +16,25 @@ import com.mojang.brigadier.arguments.StringArgumentType;
 import com.mojang.brigadier.builder.LiteralArgumentBuilder;
 import com.mojang.brigadier.exceptions.CommandSyntaxException;
 import com.mojang.brigadier.tree.CommandNode;
+
+import net.fabricmc.api.EnvType;
+import net.fabricmc.loader.api.FabricLoader;
 import net.minecraft.server.MinecraftServer;
 import net.minecraft.server.command.ServerCommandSource;
 import net.minecraft.server.network.ServerPlayerEntity;
 import net.minecraft.util.WorldSavePath;
 import net.minecraft.util.math.BlockPos;
 
-import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.Collectors;
@@ -127,20 +133,32 @@ public class CarpetScriptServer
 
     public Module getModule(String name, boolean allowLibraries)
     {
-        File folder = server.getSavePath(WorldSavePath.ROOT).resolve("scripts").toFile();
-        File[] listOfFiles = folder.listFiles();
-        if (listOfFiles != null)
-            for (File script : listOfFiles)
+        try {
+            Path folder = server.getSavePath(WorldSavePath.ROOT).resolve("scripts");
+            Files.createDirectories(folder);
+            Optional
+            scriptPath = Files.walk(folder)
+                .filter(script -> 
+                    script.getFileName().toString().equalsIgnoreCase(name+".sc") || 
+                    (allowLibraries && script.getFileName().toString().equalsIgnoreCase(name+".scl"))
+                ).findFirst();
+            if (scriptPath.isPresent())
+                return new FileModule(scriptPath.get());
+
+            if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT)
             {
-                if (script.getName().equalsIgnoreCase(name+".sc"))
-                {
-                    return new FileModule(script);
-                }
-                if (allowLibraries && script.getName().equalsIgnoreCase(name+".scl"))
-                {
-                    return new FileModule(script);
-                }
+                Path globalFolder = FabricLoader.getInstance().getConfigDir().resolve("carpet/scripts");
+                Files.createDirectories(globalFolder);
+                scriptPath = Files.walk(globalFolder)
+                        .filter(script -> script.getFileName().toString().equalsIgnoreCase(name + ".sc") ||
+                                (allowLibraries && script.getFileName().toString().equalsIgnoreCase(name + ".scl")))
+                        .findFirst();
+                if (scriptPath.isPresent())
+                    return new FileModule(scriptPath.get());
             }
+        } catch (IOException e) {
+            CarpetSettings.LOG.error("Exception while loading the app: ", e);
+        }
         for (Module moduleData : bundledModuleData)
         {
             if (moduleData.getName().equalsIgnoreCase(name) && (allowLibraries || !moduleData.isLibrary()))
@@ -173,17 +191,23 @@ public class CarpetScriptServer
                 if (!mi.isLibrary() && !mi.getName().endsWith("_beta")) moduleNames.add(mi.getName());
             }
         }
-        File folder = server.getSavePath(WorldSavePath.ROOT).resolve("scripts").toFile();
-        File[] listOfFiles = folder.listFiles();
-        if (listOfFiles == null)
-            return moduleNames;
-        for (File script : listOfFiles)
-        {
-            if (script.getName().endsWith(".sc"))
+        try {
+            Path worldScripts = server.getSavePath(WorldSavePath.ROOT).resolve("scripts");
+            Files.createDirectories(worldScripts);
+            Files.walk(worldScripts)
+                .filter(f -> f.toString().endsWith(".sc"))
+                .forEach(f -> moduleNames.add(f.getFileName().toString().replaceFirst("\\.sc$","").toLowerCase(Locale.ROOT)));
+
+            if (includeBuiltIns && (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT))
             {
-                String name = script.getName().replaceFirst("\\.sc","").toLowerCase(Locale.ROOT);
-                moduleNames.add(name);
+                Path globalScripts = FabricLoader.getInstance().getConfigDir().resolve("carpet/scripts");
+                Files.createDirectories(globalScripts);
+                Files.walk(globalScripts)
+                    .filter(f -> f.toString().endsWith(".sc"))
+                    .forEach(f -> moduleNames.add(f.getFileName().toString().replaceFirst("\\.sc$","").toLowerCase(Locale.ROOT)));
             }
+        } catch (IOException e) {
+            CarpetSettings.LOG.error("Exception while searching for apps: ", e);
         }
         return moduleNames;
     }
diff --git a/src/main/java/carpet/script/bundled/FileModule.java b/src/main/java/carpet/script/bundled/FileModule.java
index 031c862c..cacc2e12 100644
--- a/src/main/java/carpet/script/bundled/FileModule.java
+++ b/src/main/java/carpet/script/bundled/FileModule.java
@@ -16,6 +16,7 @@ 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;
@@ -26,13 +27,13 @@ public class FileModule extends Module
     private String name;
     private String code;
     private boolean library;
-    public FileModule(File sourceFile)
+    public FileModule(Path sourcePath)
     {
-        library = sourceFile.getName().endsWith(".scl");
+        library = sourcePath.getFileName().toString().endsWith(".scl");
         try
         {
-            name = sourceFile.getName().replaceFirst("\\.scl?","").toLowerCase(Locale.ROOT);
-            code = new String(Files.readAllBytes(sourceFile.toPath()), StandardCharsets.UTF_8);
+            name = sourcePath.getFileName().toString().replaceFirst("\\.scl?","").toLowerCase(Locale.ROOT);
+            code = new String(Files.readAllBytes(sourcePath), StandardCharsets.UTF_8);
         }
         catch ( IOException e)
         {
diff --git a/src/main/java/carpet/script/command/CommandArgument.java b/src/main/java/carpet/script/command/CommandArgument.java
index 97234e4d..029a6008 100644
--- a/src/main/java/carpet/script/command/CommandArgument.java
+++ b/src/main/java/carpet/script/command/CommandArgument.java
@@ -63,6 +63,7 @@ import net.minecraft.command.argument.RotationArgumentType;
 import net.minecraft.command.argument.ScoreHolderArgumentType;
 import net.minecraft.command.argument.ScoreboardSlotArgumentType;
 import net.minecraft.command.argument.SwizzleArgumentType;
+import net.minecraft.command.argument.TeamArgumentType;
 import net.minecraft.command.argument.TimeArgumentType;
 import net.minecraft.command.argument.UuidArgumentType;
 import net.minecraft.command.argument.Vec2ArgumentType;
@@ -130,7 +131,6 @@ public abstract class CommandArgument
             new VanillaUnconfigurableArgument( "blockpredicate", BlockPredicateArgumentType::blockPredicate,
                     (c, p) -> ValueConversions.ofBlockPredicate(c.getSource().getMinecraftServer().getTagManager(), BlockPredicateArgumentType.getBlockPredicate(c, p)), false
             ),
-            // block_predicate todo - not sure about the returned format. Needs to match block tags used in the API (future)
             new VanillaUnconfigurableArgument("teamcolor", ColorArgumentType::color,
                     (c, p) -> {
                         Formatting format = ColorArgumentType.getColor(c, p);
@@ -165,7 +165,6 @@ public abstract class CommandArgument
                     (c, p) -> ValueConversions.of(Registry.ENCHANTMENT.getId(ItemEnchantmentArgumentType.getEnchantment(c, p))), false
             ),
             // item_predicate  ?? //same as item but accepts tags, not sure right now
-            // slot // item_slot
             new SlotArgument(),
             new VanillaUnconfigurableArgument("item", ItemStackArgumentType::itemStack,
                     (c, p) -> ValueConversions.of(ItemStackArgumentType.getItemStackArgument(c, p).createStack(1, false)), false
@@ -241,7 +240,9 @@ public abstract class CommandArgument
             new VanillaUnconfigurableArgument("swizzle", SwizzleArgumentType::swizzle,
                     (c, p) -> StringValue.of(SwizzleArgumentType.getSwizzle(c, p).stream().map(Direction.Axis::asString).collect(Collectors.joining())), true
             ),
-            // team
+            new VanillaUnconfigurableArgument("team", TeamArgumentType::team,
+                    (c, p) -> StringValue.of(TeamArgumentType.getTeam(c, p).getName()), false
+            ),
             new VanillaUnconfigurableArgument("time", TimeArgumentType::time,
                     (c, p) -> new NumericValue(IntegerArgumentType.getInteger(c, p)), false
             ),
@@ -437,7 +438,7 @@ public abstract class CommandArgument
             if (config.containsKey("options"))
             {
                 Value optionsValue = config.get("options");
-                if (!(optionsValue instanceof ListValue)) throw new InternalExpressionException("Custom sting type requires options passed as a list");
+                if (!(optionsValue instanceof ListValue)) throw new InternalExpressionException("Custom string type requires options passed as a list");
                 validOptions = ((ListValue) optionsValue).getItems().stream()
                         .map(v -> caseSensitive?v.getString():(v.getString().toLowerCase(Locale.ROOT)))
                         .collect(Collectors.toSet());
diff --git a/src/main/resources/assets/carpet/scripts/distance_beta.sc b/src/main/resources/assets/carpet/scripts/distance_beta.sc
index b1b3bc6d..1865885a 100644
--- a/src/main/resources/assets/carpet/scripts/distance_beta.sc
+++ b/src/main/resources/assets/carpet/scripts/distance_beta.sc
@@ -1,91 +1,158 @@
 import('math','_euclidean','_manhattan','_round');
 
+global_display_modes = ['chat', 'line', 'centered_sphere', 'box'];
+
 __config()->{
     'commands'->{
         'from '->'set_start',
-        'from  to '->'_calculate',
-        'to '->_(end_pos)->(_calculate(null,end_pos)),
-        'toggle_line'->'_toggle_line'
+        'from  to '-> _(start, end) -> (global_current_start = start; calculate(end)),
+        'to '-> 'calculate',
+        'clear' -> _() -> global_display_shapes = [],
+        'clear last' -> _() -> if(global_display_shapes, delete(global_display_shapes, -1)),
+        'display ' -> 'set_mode',
+        'assist ' -> 'set_assist',
+        'assist none' -> _() -> set_assist(null),
     },
     'arguments' -> {
         'start' -> {'type' -> 'location'},
-        'end' -> {'type' -> 'location'}
+        'end' -> {'type' -> 'location'},
+        'mode' -> {'type' -> 'term', 'options' -> global_display_modes },
+        'assist' -> {'type' -> 'block', 'suggest' -> ['brown_carpet']},
     }
 };
 
-global_start=null;
-global_end=null;
+global_current_start = null;
+global_display_shapes = [];
+global_renderer_running = false;
+global_current_mode = 'chat';
 
-global_show_line=false;
+global_assist_block = null;
+
+set_assist(block) ->
+(
+
+   if (block == null,
+      global_assist_block = null;
+      handle_event('player_places_block', null);
+      handle_event('player_uses_item', null);
+
+   ,
+      global_assist_block = str(block);
+      handle_event('player_places_block', 'on_player_places_block');
+      handle_event('player_uses_item', 'on_player_uses_item');
+   )
+);
+
+global_generators = {
+    'line' -> _(from, to) -> [
+        ['line', 'from',from,'to',to]
+    ],
+    'centered_sphere' -> _(from, to) -> [
+        ['line', 'from',from,'to',to, 'line', 5, 'color', 0x88888888],
+        ['sphere', 'center',from,'radius',_euclidean(from,to)]
+    ],
+    'box' -> _(from, to) -> [
+        ['line', 'from',from,'to',to, 'line', 5, 'color', 0x88888888],
+        ['box', 'from',from,'to',to]
+    ],
+};
+
+_create_shapes(mode, from, to) ->
+(
+    shapes = call(global_generators:mode, from+[0, 0.07, 0], to+[0, 0.07, 0]);
+    shapes += ['label', 'pos',to+[0,0.2,0],             'align', 'right', 'indent', -1.5, 'text',format('rb Cylindrical:')];
+    shapes += ['label', 'pos',to+[0,0.2,0],             'align', 'left',                  'text',_round(_euclidean(from, [to:0, from:1, to:2]), 0.001)];
+    shapes += ['label', 'pos',to+[0,0.2,0], 'height', 1, 'align', 'right', 'indent', -1.5, 'text',format('rb Manhattan:')];
+    shapes += ['label', 'pos',to+[0,0.2,0], 'height', 1, 'align', 'left',                  'text',_round(_manhattan(from,to),0.001)];
+    shapes += ['label', 'pos',to+[0,0.2,0], 'height', 2, 'align', 'right', 'indent', -1.5, 'text',format('rb Euclidian:')];
+    shapes += ['label', 'pos',to+[0,0.2,0], 'height', 2, 'align', 'left',                  'text',_round(_euclidean(from,to),0.001)];
+    map(shapes, put(_, 1, [20, 'player', player()], 'extend'); _);
+);
 
 __mktp(pos) -> str('!/tp %.6f %.6f %.6f', pos);
 
 set_start(pos)->(
-    global_start = pos;
-    global_end = null;
+    global_current_start = pos;
     print(player(),format('gi Initial point set to: ', 'g '+pos, __mktp(pos)))
 );
 
-_toggle_line()->(
-    global_show_line= !global_show_line;
-    if(global_show_line,
-        print('Toggled lines on');
-        _draw_line()
-    , // else
-        print('Toggled lines off')
-    );
-);
-
-_calculate(start_pos, end_pos)->(
-    if(start_pos!=null,
-        set_start(start_pos);
-        calculate(end_pos)
-    , // else
-        if(global_start,
-            calculate(end_pos),
-            print(player(),format('r There was no initial point for '+player()));
-            set_start(end_pos)
-        )
+set_mode(mode)->(
+    if (global_display_modes ~ mode,
+        global_current_mode = mode
     )
 );
 
-__on_player_places_block(player, item_tuple, hand, block) ->(
-    if(block=='brown_carpet' && system_info('world_carpet_rules'):'carpets'=='true',
+calculate(end)->(
+    if(global_current_start != null,
+        measure(end)
+    , // else
+        print(player(),format('r No initial point selected'));
+    )
+);
+
+_carpets() -> system_info('world_carpet_rules'):'carpets'=='true';
+
+
+on_player_places_block(player, item_tuple, hand, block) ->
+(
+    if(block==global_assist_block && _carpets(),
         effective_pos = pos(block)+[0.5, 0, 0.5];
-        if(global_start==null||player~'sneaking',//wont complain for first carpet
-            set_start(effective_pos),
-            _calculate(null, effective_pos)
+        if(global_current_start==null||player~'sneaking',//wont complain for first carpet
+            set_start(effective_pos)
+        , // else
+            calculate(effective_pos)
         )
     )
 );
 
-calculate(end_pos)->(
-    global_end = end_pos;
-    manhattan = _round(_manhattan(global_start, global_end),0.001);//rounding to nearest 0.01 for precision, but not ridiculous decimal places
-    spherical = _round(_euclidean(global_start, global_end), 0.001);
-    //useful for chunk distances etc.
-    cylindrical = _round(_euclidean(global_start, [global_end:0, global_start:1, global_end:2]), 0.001);
-    
-
-    print(player(),format(
-        'w Distance between ',
-        'c '+str('[%.1f, %.1f, %.1f]', global_start), __mktp(global_start),
-        'w  and ',
-        'c '+str('[%.1f, %.1f, %.1f]', global_end), __mktp(global_end), 'w :\n',
-        'w  - Spherical: ', 'wb '+spherical+'\n',
-        'w  - Cylindrical: ', 'wb '+cylindrical+'\n',
-        'w  - Manhattan: ', 'wb '+manhattan
-    ));
-
-    schedule(0,'_draw_line');
-);
-
-_draw_line()->(
-    if(global_show_line,//It will always draw from the global positions, so it will turn on if there is a line
-        if ( global_start && global_end,
-            draw_shape('line',20,'from',global_start,'to',global_end, 'player',player());
-            draw_shape('label',20,'pos',global_end,'text',_round(_euclidean(global_start,global_end),0.001));
-        );
-        schedule(20,'_draw_line')
+on_player_uses_item(player, item_tuple, hand) ->
+(
+    if (item_tuple:0 == global_assist_block && hand == 'mainhand' && _carpets(),
+       if (player ~'sneaking',
+        global_current_mode = global_display_modes:(global_display_modes ~ global_current_mode + 1);
+        display_title(player, 'actionbar', format('w Distance mode: ','e '+global_current_mode));
+      ,
+         if(global_display_shapes, delete(global_display_shapes, -1))
+      )
     )
 );
+
+measure(end)->
+(
+    if (global_current_start == null, return());
+    start = global_current_start;
+    if (global_current_mode == 'chat',
+        manhattan = _round(_manhattan(start, end),0.001);//rounding to nearest 0.01 for precision, but not ridiculous decimal places
+        spherical = _round(_euclidean(start, end), 0.001);
+        //useful for chunk distances etc.
+        cylindrical = _round(_euclidean(start, [end:0, start:1, end:2]), 0.001);
+
+        print(player(),format(
+            'w Distance between ',
+            'c '+str('[%.1f, %.1f, %.1f]', start), __mktp(start),
+            'w  and ',
+            'c '+str('[%.1f, %.1f, %.1f]', end), __mktp(end), 'w :\n',
+            'w  - Spherical: ', 'wb '+spherical+'\n',
+            'w  - Cylindrical: ', 'wb '+cylindrical+'\n',
+            'w  - Manhattan: ', 'wb '+manhattan
+        ));
+    , // else
+        global_display_shapes += _create_shapes(global_current_mode, start, end);
+        for (global_display_shapes, draw_shape(_) );
+        if (!global_renderer_running,
+            global_renderer_running = true;
+            render()
+        )
+    )
+);
+
+render()->(
+    if (!global_display_shapes,
+        global_renderer_running = false;
+        return();
+    );
+    for (global_display_shapes,
+        draw_shape(_);
+    );
+    schedule(20,'render')
+);
diff --git a/src/main/resources/assets/carpet/scripts/draw_beta.sc b/src/main/resources/assets/carpet/scripts/draw_beta.sc
index ff819431..c88739d1 100644
--- a/src/main/resources/assets/carpet/scripts/draw_beta.sc
+++ b/src/main/resources/assets/carpet/scripts/draw_beta.sc
@@ -20,7 +20,7 @@ __config() -> {
     'arguments'->{
         'center'->{'type'->'pos', 'loaded'->'true'},
         'radius'->{'type'->'int', 'suggest'->[], 'min'->0},//to avoid default suggestions
-        'replacement'->{'type'->'block'},
+        'replacement'->{'type'->'blockpredicate'},
         'height'->{'type'->'int', 'suggest'->[],'min'->0},
         'orientation'->{'type'->'term', 'suggest'->['x','y','z']},
         'pointing'->{'type'->'term','suggest'->['up','down']},
@@ -32,9 +32,20 @@ __config() -> {
 
 //"Boilerplate" code
 
+_block_matches(existing, block_predicate) ->
+(
+    [name, block_tag, properties, nbt] = block_predicate;
+
+    (name == null || name == existing) &&
+    (block_tag == null || block_tags(existing, block_tag)) &&
+    all(properties, block_state(existing, _) == properties:_) &&
+    (!tag || tag_matches(block_data(existing), tag))
+);
+
 set_block(x, y, z, block, replacement)-> (
-    if(block != block(x, y, z),
-        without_updates( if (set([x, y, z],block),
+    existing = block(x, y, z);
+    if(block != existing && (replacement && _block_matches(existing, replacement) ),
+        without_updates( if (set(existing,block),
             global_affected += [x, y, z];
         ));
     );