diff --git a/src/main/java/carpet/CarpetServer.java b/src/main/java/carpet/CarpetServer.java index 99ebe115..0c4cb056 100644 --- a/src/main/java/carpet/CarpetServer.java +++ b/src/main/java/carpet/CarpetServer.java @@ -1,24 +1,23 @@ package carpet; + +import java.util.Random; + import carpet.commands.CameraModeCommand; import carpet.commands.CarpetCommand; import carpet.commands.CounterCommand; -//import carpet.commands.DistanceCommand; +import carpet.commands.DistanceCommand; import carpet.commands.DrawCommand; -//import carpet.commands.ScriptCommand; -//import carpet.commands.InfoCommand; +import carpet.commands.InfoCommand; +import carpet.commands.LogCommand; import carpet.commands.PerimeterInfoCommand; import carpet.commands.PlayerCommand; +import carpet.commands.ScriptCommand; import carpet.commands.SpawnCommand; -import carpet.commands.LogCommand; -import carpet.commands.TestCommand; import carpet.commands.TickCommand; -//import carpet.script.ExpressionInspector; - -import java.util.Random; - -//import carpet.helpers.TickSpeed; +import carpet.helpers.TickSpeed; import carpet.logging.LoggerRegistry; +import carpet.script.CarpetScriptServer; import carpet.utils.HUDController; import com.mojang.brigadier.CommandDispatcher; import net.minecraft.server.command.ServerCommandSource; @@ -28,6 +27,7 @@ public class CarpetServer // static for now - easier to handle all around the co { public static final Random rand = new Random((int)((2>>16)*Math.random())); public static MinecraftServer minecraft_server; + public static CarpetScriptServer scriptServer; public static void init(MinecraftServer server) //aka constructor of this static singleton class { CarpetServer.minecraft_server = server; @@ -35,7 +35,7 @@ public class CarpetServer // static for now - easier to handle all around the co public static void onServerLoaded(MinecraftServer server) { CarpetSettings.apply_settings_from_conf(server); - //ExpressionInspector.CarpetExpression_resetExpressionEngine(); + scriptServer = new CarpetScriptServer(); } // Separate from onServerLoaded, because a server can be loaded multiple times in singleplayer public static void onGameStarted() { @@ -44,7 +44,7 @@ public class CarpetServer // static for now - easier to handle all around the co public static void tick(MinecraftServer server) { - //TickSpeed.tick(server); + TickSpeed.tick(server); HUDController.update_hud(server); } @@ -57,13 +57,13 @@ public class CarpetServer // static for now - easier to handle all around the co SpawnCommand.register(dispatcher); PlayerCommand.register(dispatcher); CameraModeCommand.register(dispatcher); - //InfoCommand.register(dispatcher); - //DistanceCommand.register(dispatcher); + InfoCommand.register(dispatcher); + DistanceCommand.register(dispatcher); PerimeterInfoCommand.register(dispatcher); DrawCommand.register(dispatcher); - //ScriptCommand.register(dispatcher); + ScriptCommand.register(dispatcher); - TestCommand.register(dispatcher); + //TestCommand.register(dispatcher); } } diff --git a/src/main/java/carpet/CarpetSettings.java b/src/main/java/carpet/CarpetSettings.java index 7ba57e65..81e854ba 100644 --- a/src/main/java/carpet/CarpetSettings.java +++ b/src/main/java/carpet/CarpetSettings.java @@ -198,7 +198,7 @@ public class CarpetSettings } } - private static void notifyPlayersCommandsChanged() + public static void notifyPlayersCommandsChanged() { if (CarpetServer.minecraft_server == null || CarpetServer.minecraft_server.getPlayerManager() == null) { diff --git a/src/main/java/carpet/commands/ScriptCommand.java b/src/main/java/carpet/commands/ScriptCommand.java new file mode 100644 index 00000000..d86ff744 --- /dev/null +++ b/src/main/java/carpet/commands/ScriptCommand.java @@ -0,0 +1,491 @@ +package carpet.commands; + +import carpet.CarpetServer; +import carpet.CarpetSettings; +import carpet.script.CarpetExpression; +import carpet.script.Expression; +import carpet.script.ExpressionInspector; +import carpet.script.ScriptHost; +import carpet.script.Tokenizer; +import carpet.script.exception.CarpetExpressionException; +import carpet.utils.Messenger; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import net.minecraft.block.Block; +import net.minecraft.block.pattern.CachedBlockPosition; +import net.minecraft.command.arguments.BlockPosArgumentType; +import net.minecraft.command.arguments.BlockPredicateArgumentType; +import net.minecraft.command.arguments.BlockStateArgument; +import net.minecraft.command.arguments.BlockStateArgumentType; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.util.Clearable; +import net.minecraft.util.math.BlockPos; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.MutableIntBoundingBox; +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.math.Vec3i; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import static net.minecraft.server.command.CommandManager.argument; +import static net.minecraft.server.command.CommandManager.literal; +import static net.minecraft.server.command.CommandSource.suggestMatching; + +public class ScriptCommand +{ + public static void register(CommandDispatcher dispatcher) + { + LiteralArgumentBuilder b = literal("globals"). + executes(ScriptCommand::listGlobals); + LiteralArgumentBuilder o = literal("stop"). + executes( (cc) -> { CarpetServer.scriptServer.stopAll = true; return 1;}); + LiteralArgumentBuilder u = literal("resume"). + executes( (cc) -> { CarpetServer.scriptServer.stopAll = false; return 1;}); + LiteralArgumentBuilder l = literal("run"). + requires((player) -> player.hasPermissionLevel(2)). + then(argument("expr", StringArgumentType.greedyString()). + executes((cc) -> compute( + cc, + StringArgumentType.getString(cc, "expr")))); + LiteralArgumentBuilder s = literal("invoke"). + then(argument("call", StringArgumentType.word()).suggests( (cc, bb)->suggestMatching(suggestFunctionCalls(cc),bb)). + executes( (cc) -> invoke( + cc, + StringArgumentType.getString(cc, "call"), + null, + null, + "" + )). + then(argument("arguments", StringArgumentType.greedyString()). + executes( (cc) -> invoke( + cc, + StringArgumentType.getString(cc, "call"), + null, + null, + StringArgumentType.getString(cc, "arguments") + )))); + LiteralArgumentBuilder c = literal("invokepoint"). + then(argument("call", StringArgumentType.word()).suggests( (cc, bb)->suggestMatching(suggestFunctionCalls(cc),bb)). + then(argument("origin", BlockPosArgumentType.create()). + executes( (cc) -> invoke( + cc, + StringArgumentType.getString(cc, "call"), + BlockPosArgumentType.getBlockPos(cc, "origin"), + null, + "" + )). + then(argument("arguments", StringArgumentType.greedyString()). + executes( (cc) -> invoke( + cc, + StringArgumentType.getString(cc, "call"), + BlockPosArgumentType.getBlockPos(cc, "origin"), + null, + StringArgumentType.getString(cc, "arguments") + ))))); + LiteralArgumentBuilder h = literal("invokearea"). + then(argument("call", StringArgumentType.word()).suggests( (cc, bb)->suggestMatching(suggestFunctionCalls(cc),bb)). + then(argument("from", BlockPosArgumentType.create()). + then(argument("to", BlockPosArgumentType.create()). + executes( (cc) -> invoke( + cc, + StringArgumentType.getString(cc, "call"), + BlockPosArgumentType.getBlockPos(cc, "from"), + BlockPosArgumentType.getBlockPos(cc, "to"), + "" + )). + then(argument("arguments", StringArgumentType.greedyString()). + executes( (cc) -> invoke( + cc, + StringArgumentType.getString(cc, "call"), + BlockPosArgumentType.getBlockPos(cc, "from"), + BlockPosArgumentType.getBlockPos(cc, "to"), + StringArgumentType.getString(cc, "arguments") + )))))); + LiteralArgumentBuilder i = literal("scan").requires((player) -> player.hasPermissionLevel(2)). + then(argument("origin", BlockPosArgumentType.create()). + then(argument("from", BlockPosArgumentType.create()). + then(argument("to", BlockPosArgumentType.create()). + then(argument("expr", StringArgumentType.greedyString()). + executes( (cc) -> scriptScan( + cc, + BlockPosArgumentType.getBlockPos(cc, "origin"), + BlockPosArgumentType.getBlockPos(cc, "from"), + BlockPosArgumentType.getBlockPos(cc, "to"), + StringArgumentType.getString(cc, "expr") + )))))); + LiteralArgumentBuilder e = literal("fill").requires((player) -> player.hasPermissionLevel(2)). + then(argument("origin", BlockPosArgumentType.create()). + then(argument("from", BlockPosArgumentType.create()). + then(argument("to", BlockPosArgumentType.create()). + then(argument("expr", StringArgumentType.string()). + then(argument("block", BlockStateArgumentType.create()). + executes((cc) -> scriptFill( + cc, + BlockPosArgumentType.getBlockPos(cc, "origin"), + BlockPosArgumentType.getBlockPos(cc, "from"), + BlockPosArgumentType.getBlockPos(cc, "to"), + StringArgumentType.getString(cc, "expr"), + BlockStateArgumentType.getBlockState(cc, "block"), + null, "solid" + )). + then(literal("replace"). + then(argument("filter", BlockPredicateArgumentType.create()) + .executes((cc) -> scriptFill( + cc, + BlockPosArgumentType.getBlockPos(cc, "origin"), + BlockPosArgumentType.getBlockPos(cc, "from"), + BlockPosArgumentType.getBlockPos(cc, "to"), + StringArgumentType.getString(cc, "expr"), + BlockStateArgumentType.getBlockState(cc, "block"), + BlockPredicateArgumentType.getBlockPredicate(cc, "filter"), + "solid" + ))))))))); + LiteralArgumentBuilder t = literal("outline").requires((player) -> player.hasPermissionLevel(2)). + then(argument("origin", BlockPosArgumentType.create()). + then(argument("from", BlockPosArgumentType.create()). + then(argument("to", BlockPosArgumentType.create()). + then(argument("expr", StringArgumentType.string()). + then(argument("block", BlockStateArgumentType.create()). + executes((cc) -> scriptFill( + cc, + BlockPosArgumentType.getBlockPos(cc, "origin"), + BlockPosArgumentType.getBlockPos(cc, "from"), + BlockPosArgumentType.getBlockPos(cc, "to"), + StringArgumentType.getString(cc, "expr"), + BlockStateArgumentType.getBlockState(cc, "block"), + null, "outline" + )). + then(literal("replace"). + then(argument("filter", BlockPredicateArgumentType.create()) + .executes((cc) -> scriptFill( + cc, + BlockPosArgumentType.getBlockPos(cc, "origin"), + BlockPosArgumentType.getBlockPos(cc, "from"), + BlockPosArgumentType.getBlockPos(cc, "to"), + StringArgumentType.getString(cc, "expr"), + BlockStateArgumentType.getBlockState(cc, "block"), + BlockPredicateArgumentType.getBlockPredicate(cc, "filter"), + "outline" + ))))))))); + LiteralArgumentBuilder a = literal("load").requires( (player) -> player.hasPermissionLevel(2) ). + then(argument("package", StringArgumentType.word()). + suggests( (cc, bb) -> suggestMatching(CarpetServer.scriptServer.listAvailableModules(),bb)). + executes((cc) -> + { + boolean success = CarpetServer.scriptServer.addScriptHost(cc.getSource(), StringArgumentType.getString(cc, "package")); + Messenger.m(cc.getSource(), success?"w Successfully added a module":"r Failed to add a module"); + return success?1:0; + }) + ); + LiteralArgumentBuilder f = literal("unload").requires( (player) -> player.hasPermissionLevel(2) ). + then(argument("package", StringArgumentType.word()). + suggests( (cc, bb) -> suggestMatching(CarpetServer.scriptServer.modules.keySet(),bb)). + executes((cc) -> + { + boolean success =CarpetServer.scriptServer.removeScriptHost(StringArgumentType.getString(cc, "package")); + Messenger.m(cc.getSource(), success?"w Successfully added a module":"r Failed to add a module"); + return success?1:0; + })); + + dispatcher.register(literal("script"). + requires((player) -> CarpetSettings.getBool("commandScript")). + then(b).then(u).then(o).then(l).then(s).then(c).then(h).then(i).then(e).then(t).then(a).then(f)); + dispatcher.register(literal("script"). + requires((player) -> CarpetSettings.getBool("commandScript")). + then(literal("in"). + then(argument("package", StringArgumentType.word()). + suggests( (cc, bb) -> suggestMatching(CarpetServer.scriptServer.modules.keySet(), bb)). + then(b).then(u).then(o).then(l).then(s).then(c).then(h).then(i).then(e).then(t)))); + } + private static ScriptHost getHost(CommandContext context) + { + try + { + String name = StringArgumentType.getString(context, "package").toLowerCase(Locale.ROOT); + return CarpetServer.scriptServer.modules.getOrDefault(name, CarpetServer.scriptServer.globalHost); + + } + catch (IllegalArgumentException ignored) + { + return CarpetServer.scriptServer.globalHost; + } + } + private static Collection suggestFunctionCalls(CommandContext c) + { + ServerCommandSource s = c.getSource(); + ScriptHost host = getHost(c); + return host.getPublicFunctions(); + } + private static int listGlobals(CommandContext context) + { + ScriptHost host = getHost(context); + ServerCommandSource source = context.getSource(); + + Messenger.m(source, "w Global functions"+((host == CarpetServer.scriptServer.globalHost)?":":" in "+host.getName()+":")); + for (String fname : host.getAvailableFunctions()) + { + Expression expr = host.getExpressionForFunction(fname); + Tokenizer.Token tok = host.getTokenForFunction(fname); + List snippet = ExpressionInspector.Expression_getExpressionSnippet(tok, expr); + Messenger.m(source, "w Function "+fname+" defined at: line "+(tok.lineno+1)+" pos "+(tok.linepos+1)); + for (String snippetLine: snippet) + { + Messenger.m(source, "li "+snippetLine); + } + Messenger.m(source, "gi ----------------"); + } + //Messenger.m(source, "w "+code); + Messenger.m(source, "w Global Variables:"); + + for (String vname : host.globalVariables.keySet()) + { + Messenger.m(source, "w Variable "+vname+": ", "wb "+ host.globalVariables.get(vname).evalValue(null).getPrettyString()); + } + return 1; + } + + public static void handleCall(ServerCommandSource source, Supplier call) + { + try + { + CarpetServer.scriptServer.setChatErrorSnooper(source); + long start = System.nanoTime(); + String result = call.get(); + long time = ((System.nanoTime()-start)/1000); + String metric = "\u00B5s"; + if (time > 5000) + { + time /= 1000; + metric = "ms"; + } + if (time > 10000) + { + time /= 1000; + metric = "s"; + } + Messenger.m(source, "wi = ", "wb "+result, "gi ("+time+metric+")"); + } + catch (CarpetExpressionException e) + { + Messenger.m(source, "r Exception white evaluating expression at "+new BlockPos(source.getPosition())+": "+e.getMessage()); + } + CarpetServer.scriptServer.resetErrorSnooper(); + } + + private static int invoke(CommandContext context, String call, BlockPos pos1, BlockPos pos2, String args) + { + ServerCommandSource source = context.getSource(); + ScriptHost host = getHost(context); + if (call.startsWith("__")) + { + Messenger.m(source, "r Hidden functions are only callable in scripts"); + return 0; + } + List positions = new ArrayList<>(); + if (pos1 != null) + { + positions.add(pos1.getX()); + positions.add(pos1.getY()); + positions.add(pos1.getZ()); + } + if (pos2 != null) + { + positions.add(pos2.getX()); + positions.add(pos2.getY()); + positions.add(pos2.getZ()); + } + //if (!(args.trim().isEmpty())) + // arguments.addAll(Arrays.asList(args.trim().split("\\s+"))); + handleCall(source, () -> host.call(source, call, positions, args)); + return 1; + } + + + private static int compute(CommandContext context, String expr) + { + ServerCommandSource source = context.getSource(); + ScriptHost host = getHost(context); + handleCall(source, () -> { + CarpetExpression ex = new CarpetExpression(expr, source, new BlockPos(0, 0, 0)); + return ex.scriptRunCommand(host, new BlockPos(source.getPosition())); + }); + return 1; + } + + private static int scriptScan(CommandContext context, BlockPos origin, BlockPos a, BlockPos b, String expr) + { + ServerCommandSource source = context.getSource(); + ScriptHost host = getHost(context); + MutableIntBoundingBox area = new MutableIntBoundingBox(a, b); + CarpetExpression cexpr = new CarpetExpression(expr, source, origin); + int int_1 = area.getBlockCountX() * area.getBlockCountY() * area.getBlockCountZ(); + if (int_1 > CarpetSettings.getInt("fillLimit")) + { + Messenger.m(source, "r too many blocks to evaluate: " + int_1); + return 1; + } + int successCount = 0; + try + { + for (int x = area.minX; x <= area.maxX; x++) + { + for (int y = area.minY; y <= area.maxY; y++) + { + for (int z = area.minZ; z <= area.maxZ; z++) + { + try + { + if (cexpr.fillAndScanCommand(host, x, y, z)) successCount++; + } + catch (ArithmeticException ignored) + { + } + } + } + } + } + catch (CarpetExpressionException exc) + { + Messenger.m(source, "r Error while processing command: "+exc); + return 0; + } + Messenger.m(source, "w Expression successful in " + successCount + " out of " + int_1 + " blocks"); + return successCount; + + } + + + private static int scriptFill(CommandContext context, BlockPos origin, BlockPos a, BlockPos b, String expr, + BlockStateArgument block, Predicate replacement, String mode) + { + ServerCommandSource source = context.getSource(); + ScriptHost host = getHost(context); + MutableIntBoundingBox area = new MutableIntBoundingBox(a, b); + CarpetExpression cexpr = new CarpetExpression(expr, source, origin); + int int_1 = area.getBlockCountX() * area.getBlockCountY() * area.getBlockCountZ(); + if (int_1 > CarpetSettings.getInt("fillLimit")) + { + Messenger.m(source, "r too many blocks to evaluate: "+ int_1); + return 1; + } + + boolean[][][] volume = new boolean[area.getBlockCountX()][area.getBlockCountY()][area.getBlockCountZ()]; + + BlockPos.Mutable mbpos = new BlockPos.Mutable(origin); + ServerWorld world = source.getWorld(); + + + for (int x = area.minX; x <= area.maxX; x++) + { + for (int y = area.minY; y <= area.maxY; y++) + { + for (int z = area.minZ; z <= area.maxZ; z++) + { + try + { + if (cexpr.fillAndScanCommand(host, x, y, z)) + { + volume[x-area.minX][y-area.minY][z-area.minZ]=true; + } + } + catch (CarpetExpressionException e) + { + Messenger.m(source, "r Exception while filling the area:\n","l "+e.getMessage()); + return 0; + } + catch (ArithmeticException e) + { + } + } + } + } + final int maxx = area.getBlockCountX()-1; + final int maxy = area.getBlockCountY()-1; + final int maxz = area.getBlockCountZ()-1; + if ("outline".equalsIgnoreCase(mode)) + { + boolean[][][] newVolume = new boolean[area.getBlockCountX()][area.getBlockCountY()][area.getBlockCountZ()]; + for (int x = 0; x <= maxx; x++) + { + for (int y = 0; y <= maxy; y++) + { + for (int z = 0; z <= maxz; z++) + { + if (volume[x][y][z]) + { + if ( ( (x != 0 && !volume[x-1][y ][z ]) || + (x != maxx && !volume[x+1][y ][z ]) || + (y != 0 && !volume[x ][y-1][z ]) || + (y != maxy && !volume[x ][y+1][z ]) || + (z != 0 && !volume[x ][y ][z-1]) || + (z != maxz && !volume[x ][y ][z+1]) + )) + { + newVolume[x][y][z] = true; + } + } + } + } + } + volume = newVolume; + } + int affected = 0; + for (int x = 0; x <= maxx; x++) + { + for (int y = 0; y <= maxy; y++) + { + for (int z = 0; z <= maxz; z++) + { + if (volume[x][y][z]) + { + mbpos.set(x+area.minX, y+area.minY, z+area.minZ); + if (replacement == null || replacement.test( + new CachedBlockPosition( world, mbpos, true))) + { + BlockEntity tileentity = world.getBlockEntity(mbpos); + Clearable.clear(tileentity); + + if (block.setBlockState( + world, + mbpos, + 2 | (CarpetSettings.getBool("fillUpdates") ?0:1024) + )) + { + ++affected; + } + } + } + } + } + } + if (CarpetSettings.getBool("fillUpdates") && block != null) + { + for (int x = 0; x <= maxx; x++) + { + for (int y = 0; y <= maxy; y++) + { + for (int z = 0; z <= maxz; z++) + { + if (volume[x][y][z]) + { + mbpos.set(x+area.minX, y+area.minY, z+area.minZ); + Block blokc = world.getBlockState(mbpos).getBlock(); + world.updateNeighbors(mbpos, blokc); + } + } + } + } + } + Messenger.m(source, "gi Affected "+affected+" blocks in "+area.getBlockCountX() * area.getBlockCountY() * area.getBlockCountZ()+" block volume"); + return 1; + } +} + diff --git a/src/main/java/carpet/fakes/MinecraftServerInterface.java b/src/main/java/carpet/fakes/MinecraftServerInterface.java new file mode 100644 index 00000000..40900379 --- /dev/null +++ b/src/main/java/carpet/fakes/MinecraftServerInterface.java @@ -0,0 +1,8 @@ +package carpet.fakes; + +import java.util.function.BooleanSupplier; + +public interface MinecraftServerInterface +{ + void forceTick(BooleanSupplier sup); +} diff --git a/src/main/java/carpet/mixins/MinecraftServer_scarpetMixin.java b/src/main/java/carpet/mixins/MinecraftServer_scarpetMixin.java new file mode 100644 index 00000000..2540b068 --- /dev/null +++ b/src/main/java/carpet/mixins/MinecraftServer_scarpetMixin.java @@ -0,0 +1,34 @@ +package carpet.mixins; + +import carpet.fakes.MinecraftServerInterface; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.ServerTask; +import net.minecraft.util.NonBlockingThreadExecutor; +import net.minecraft.util.SystemUtil; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.function.BooleanSupplier; + +@Mixin(MinecraftServer.class) +public abstract class MinecraftServer_scarpetMixin extends NonBlockingThreadExecutor implements MinecraftServerInterface +{ + public MinecraftServer_scarpetMixin(String string_1) + { + super(string_1); + } + + @Shadow protected abstract void tick(BooleanSupplier booleanSupplier_1); + + @Shadow private long timeReference; + + @Shadow private long field_4557; + + @Override + public void forceTick(BooleanSupplier isAhead) + { + timeReference = field_4557 = SystemUtil.getMeasuringTimeMs(); + tick(isAhead); + executeTaskQueue(); + } +} diff --git a/src/main/java/carpet/script/CarpetContext.java b/src/main/java/carpet/script/CarpetContext.java new file mode 100644 index 00000000..bb1cdc80 --- /dev/null +++ b/src/main/java/carpet/script/CarpetContext.java @@ -0,0 +1,23 @@ +package carpet.script; + +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.util.math.BlockPos; + +public class CarpetContext extends Context +{ + public ServerCommandSource s; + public BlockPos origin; + public CarpetContext(ScriptHost host, ServerCommandSource source, BlockPos origin) + { + super(host); + s = source; + this.origin = origin; + } + + @Override + public Context recreate() + { + return new CarpetContext(this.host, this.s, this.origin); + } + +} diff --git a/src/main/java/carpet/script/CarpetExpression.java b/src/main/java/carpet/script/CarpetExpression.java new file mode 100644 index 00000000..05ba9ebb --- /dev/null +++ b/src/main/java/carpet/script/CarpetExpression.java @@ -0,0 +1,2072 @@ +package carpet.script; + +import carpet.CarpetServer; +import carpet.CarpetSettings; +// import carpet.helpers.FeatureGenerator; todo +import carpet.fakes.MinecraftServerInterface; +import carpet.script.exception.CarpetExpressionException; +import carpet.script.exception.ExpressionException; +import carpet.script.exception.InternalExpressionException; +import carpet.script.Fluff.TriFunction; +import carpet.script.value.BlockValue; +import carpet.script.value.EntityValue; +import carpet.script.value.LazyListValue; +import carpet.script.value.ListValue; +import carpet.script.value.NumericValue; +import carpet.script.value.StringValue; +import carpet.script.value.Value; +import carpet.utils.BlockInfo; +import carpet.utils.Messenger; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.entity.EntityType; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.command.arguments.ParticleArgumentType; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.client.network.packet.PlaySoundIdS2CPacket; +import net.minecraft.particle.ParticleEffect; +import net.minecraft.block.BlockPlacementEnvironment; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.state.property.Property; +import net.minecraft.state.StateFactory; +import net.minecraft.predicate.entity.EntityPredicates; +import net.minecraft.util.Identifier; +import net.minecraft.sound.SoundCategory; +import net.minecraft.util.math.BoundingBox; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.registry.Registry; +import net.minecraft.world.LightType; +import net.minecraft.world.SessionLockException; +import net.minecraft.world.World; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.world.Heightmap; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.function.BiFunction; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static java.lang.Math.abs; +import static java.lang.Math.max; +import static java.lang.Math.min; +import static java.lang.Math.sqrt; + +/** + *

Minecraft specific API and scarpet language add-ons and commands

+ *

Here is the gist of the Minecraft related functions. Otherwise the CarpetScript could live without Minecraft.

+ *

Dimension issues

+ *

One note, which is important is that most of the calls for entities and blocks + * would refer to the current dimension of the caller, meaning, that if we for example + * list all the players using player('all') function, if a player is in the + * other dimension, calls to entities and blocks around that player would point incorrectly. + * Moreover, running commandblocks in the spawn chunks would mean that commands will always + * refer to the overworld blocks and entities. + * In case you would want to run commands across all dimensions, just run three of them, using + * /execute in overworld/the_nether/the_end run script run ... and query + * players using player('*'), which only returns players in current dimension.

+ */ +public class CarpetExpression +{ + + private ServerCommandSource source; + private BlockPos origin; + private Expression expr; + private static long tickStart = 0L; + + private static boolean stopAll = false; + + /** + *

script stop/script resume command

+ *
+ *

+ * /script stop allows to stop execution of any script currently running that calls the + * gametick() function which + * allows the game loop to regain control of the game and process other commands. This will also make sure + * that all current and future programs will stop their execution. Execution of all programs will be + * prevented until /script resume command is called. + *

+ *

Lets look at the following example. This is a program computes Fibonacci number in a recursive manner:

+ *
+     * fib(n) -> if(n<3, 1, fib(n-1)+fib(n-2) ); fib(8)
+     * 
+ *

That's really bad way of doing it, because the higher number we need to compute the compute requirements will rise + * exponentially with n. It takes a little over 50 milliseconds to do fib(24), so above one tick, + * but about a minute to do fib(40). Calling fib(40) will not only freeze the game, but also you woudn't be able to interrupt + * its execution. We can modify the script as follows

+ *
fib(n) -> ( gametick(50); if(n<3, 1, fib(n-1)+fib(n-2) ) ); fib(40)
+ *

But this would never finish as such call would finish after ~ 2^40 ticks. To make our computations + * responsive, yet able to respond to user interactions, other commands, as well as interrupt execution, + * we could do the following:

+ *
fib(n) -> ( if(n==23, gametick(50) ); if(n<3, 1, fib(n-1)+fib(n-2) ) ); fib(40)
+ *

This would slow down the computation of fib(40) from a minute to two, but allows the game to keep continue running + * and be responsive to commands, using about half of each tick to advance the computation. + * Obviously depending on the problem, and available hardware, certain things can take + * more or less time to execute, so portioning of work with calling gametick should be balanced in each + * case separately

+ *
+ * @param doStop . + */ + public static void BreakExecutionOfAllScriptsWithCommands(boolean doStop) + { + //unused - accessed via CarpetScriptServer + } + + private LazyValue booleanStateTest( + Context c, + String name, + List params, + BiFunction test + ) + { + CarpetContext cc = (CarpetContext) c; + if (params.size() == 0) + { + throw new InternalExpressionException(name + " requires at least one parameter"); + } + Value v0 = params.get(0).evalValue(c); + if (v0 instanceof BlockValue) + { + Value retval = test.apply(((BlockValue) v0).getBlockState(), ((BlockValue) v0).getPos()) ? Value.TRUE : Value.FALSE; + return (c_, t_) -> retval; + } + BlockValue block = BlockValue.fromParams(cc, params, 0).block; + Value retval = test.apply(block.getBlockState(), block.getPos()) ? Value.TRUE : Value.FALSE; + return (c_, t_) -> retval; + } + + private LazyValue stateStringQuery( + Context c, + String name, + List params, + BiFunction test + ) + { + CarpetContext cc = (CarpetContext) c; + if (params.size() == 0) + { + throw new InternalExpressionException(name + " requires at least one parameter"); + } + + Value v0 = params.get(0).evalValue(c); + if (v0 instanceof BlockValue) + { + Value retval = new StringValue(test.apply( ((BlockValue) v0).getBlockState(), ((BlockValue) v0).getPos())); + return (c_, t_) -> retval; + } + BlockValue block = BlockValue.fromParams(cc, params, 0).block; + Value retval = new StringValue(test.apply(block.getBlockState(), block.getPos())); + return (c_, t_) -> retval; + } + + private LazyValue genericStateTest( + Context c, + String name, + List params, + TriFunction test + ) + { + CarpetContext cc = (CarpetContext) c; + if (params.size() == 0) + { + throw new InternalExpressionException(name + " requires at least one parameter"); + } + Value v0 = params.get(0).evalValue(c); + if (v0 instanceof BlockValue) + { + try + { + Value retval = test.apply(((BlockValue) v0).getBlockState(), ((BlockValue) v0).getPos(), cc.s.getWorld()); + return (_c, _t) -> retval; + } + catch (NullPointerException ignored) + { + throw new InternalExpressionException(name + " function requires a block that is positioned in the world"); + } + } + BlockValue block = BlockValue.fromParams(cc, params, 0).block; + Value retval = test.apply(block.getBlockState(), block.getPos(), cc.s.getWorld()); + return (c_, t_) -> retval; + } + + private > BlockState setProperty(Property property, String name, String value, + BlockState bs) + { + Optional optional = property.getValue(value); + + if (optional.isPresent()) + { + bs = bs.with(property, optional.get()); + } + else + { + throw new InternalExpressionException(value + " is not a valid value for property " + name); + } + return bs; + } + + private static Map particleCache = new HashMap<>(); + private ParticleEffect getParticleData(String name) + { + ParticleEffect particle = particleCache.get(name); + if (particle != null) + return particle; + try + { + particle = ParticleArgumentType.readParameters(new StringReader(name)); + } + catch (CommandSyntaxException e) + { + throw new InternalExpressionException("No such particle: "+name); + } + particleCache.put(name, particle); + return particle; + } + + private int drawParticleLine(ServerWorld world, ParticleEffect particle, Vec3d from, Vec3d to, double density) + { + double lineLengthSq = from.squaredDistanceTo(to); + if (lineLengthSq == 0) return 0; + Vec3d incvec = to.subtract(from).multiply(2*density/sqrt(lineLengthSq)); + int pcount = 0; + for (Vec3d delta = new Vec3d(0.0,0.0,0.0); + delta.lengthSquared()Blocks / World API + *
+ *

Specifying blocks

+ *

block(x, y, z), block(state)

+ *

Returns either a block from specified location, or block with a specific state + * (as used by /setblock command), so allowing for block properties etc. + * Blocks can be referenced everywhere by its simple name, but its only used in its default state

+ *
+     * block('air')  => air
+     * block('iron_trapdoor[half=top]')  => iron_trapdoor
+     * block(0,0,0) == block('bedrock')  => 1
+     * 
+ *

World Manipulation

+ *

All the functions below can be used with block value, queried with coord triple, or 3-long list. + * All pos in the functions referenced below refer to either method of passing block position

+ *

set(pos, block, property?, value?, ...)

+ *

First part of the set function is either a coord triple, list of three numbers, or other block + * with coordinates. Second part, block is either block value as a result of block() function + * string value indicating the block name, and optional property - value pairs for extra block properties. + * If block is specified only by name, then if a destination block is the same the set operation + * is skipped, otherwise is executed, for other potential extra properties

+ *

The returned value is either the block state that has been set, or false if block setting was skipped

+ *
+     * set(0,5,0,'bedrock')  => bedrock
+     * set(l(0,5,0), 'bedrock')  => bedrock
+     * set(block(0,5,0), 'bedrock')  => bedrock
+     * scan(0,5,0,0,0,0,set(_,'bedrock'))  => 1
+     * set(pos(players()), 'bedrock')  => bedrock
+     * set(0,0,0,'bedrock')  => 0   // or 1 in overworlds generated in 1.8 and before
+     * scan(0,100,0,20,20,20,set(_,'glass'))
+     *     // filling the area with glass
+     * scan(0,100,0,20,20,20,set(_,block('glass')))
+     *     // little bit faster due to internal caching of block state selectors
+     * b = block('glass'); scan(0,100,0,20,20,20,set(_,b))
+     *     // yet another option, skips all parsing
+     * set(x,y,z,'iron_trapdoor')  // sets bottom iron trapdoor
+     * set(x,y,z,'iron_trapdoor[half=top]')  // Incorrect. sets bottom iron trapdoor - no parsing of properties
+     * set(x,y,z,'iron_trapdoor','half','top') // correct - top trapdoor
+     * set(x,y,z,block('iron_trapdoor[half=top]')) // also correct, block() provides extra parsing
+     * 
+ *

update(pos)

+ *

Causes a block update at position.

+ *

block_tick(pos)

+ *

Causes a block to tick at position.

+ *

random_tick(pos)

+ *

Causes a random tick at position.

+ * + *

Block and World querying

+ * + *

pos(block), pos(entity)

+ *

Returns a triple of coordinates of a specified block or entity. Technically entities are queried with + * query function and the same can be achieved with query(entity,'pos'), but for simplicity + * pos allows to pass all positional objects.

+ *
+     *     pos(block(0,5,0))  => l(0,5,0)
+     *     pos(players()) => l(12.3, 45.6, 32.05)
+     *     pos(block('stone'))  => Error: Cannot fetch position of an unrealized block
+     * 
+ *

property(pos, name)

+ *

Returns property of block at pos, or specified by block argument. If a block doesn't + * have that property, null value is returned. Returned values are always strings. It is expected from + * the user to know what to expect and convert values to numbers using number() function or booleans + * using bool() function.

+ *
+     *     set(x,y,z,'iron_trapdoor','half','top'); property(x,y,z,'half')  => top
+     *     set(x,y,z,'air'); property(x,y,z,'half')  => null
+     *     property(block('iron_trapdoor[half=top]'),'half')  => top
+     *     property(block('iron_trapdoor[half=top]'),'powered')  => false
+     *     bool(property(block('iron_trapdoor[half=top]'),'powered'))  => 0
+     * 
+ * + *

solid(pos)

+ *

Boolean function, true if the block is solid

+ *

air(pos)

+ *

Boolean function, true if a block is air.... or cave air... + * or void air.... or any other air they come up with.

+ *

liquid(pos)

+ *

Boolean function, true if the block is liquid, or liquidlogged

+ *

flammable(pos)

+ *

Boolean function, true if the block is flammable

+ *

transparent(pos)

+ *

Boolean function, true if the block is transparent

+ *

opacity(pos)

+ *

Numeric, returning opacity level of a block

+ *

blocks_daylight(pos)

+ *

Boolean function, true if the block blocks daylight

+ *

emitted_light(pos)

+ *

Numeric, returning light level emitted from block

+ *

light(pos)

+ *

Integer function, returning total light level at position

+ *

block_light(pos)

+ *

Integer function, returning block light at position. From torches and other light sources.

+ *

sky_light(pos)

+ *

Numeric function, returning sky light at position. From the sky access.

+ *

see_sky(pos)

+ *

Boolean function, returning true if the block can see sky.

+ *

hardness(pos)

+ *

Numeric function, indicating hardness of a block.

+ *

blast_resistance(pos)

+ *

Numeric function, indicating blast_resistance of a block.

+ + *

top(type, x, z)

+ *

Returns the Y value of the topmost block at given x, z coords, according to the + * heightmap specified by type. Valid options are:

+ *
    + *
  • light: topmost light blocking block
  • + *
  • motion: topmost motion blocking block
  • + *
  • terrain: topmost motion blocking block except leaves
  • + *
  • ocean_floor: topmost non-water block
  • + *
  • surface: topmost surface block
  • + *
+ *
+     * top('motion', x, z)  => 63
+     * top('ocean_floor', x, z)  => 41
+     * 
+ *

loaded(pos)

+ *

Boolean function, true if the block is loaded. Normally scarpet doesn't check if operates on + * loaded area - the game will automatically load missing blocks. We see this as advantage. + * Vanilla fill/clone commands only check the specified corners for loadness.

+ *
+     * loaded(pos(players()))  => 1
+     * loaded(100000,100,1000000)  => 0
+     * 
+ *

loaded_ep(pos)

+ *

Boolean function, true if the block is loaded and entity processing, as per 1.13.2

+ *

suffocates(pos)

+ *

Boolean function, true if the block causes suffocation.

+ *

power(pos)

+ *

Numeric function, returning redstone power level at position.

+ *

ticks_randomly(pos)

+ *

Boolean function, true if the block ticks randomly.

+ *

blocks_movement(pos)

+ *

Boolean function, true if block at position blocks movement.

+ *

block_sound(pos)

+ *

Returns the name of sound type made by the block at position. One of:

+ *
    + *
  • wood
  • + *
  • gravel
  • + *
  • grass
  • + *
  • stone
  • + *
  • metal
  • + *
  • glass
  • + *
  • wool
  • + *
  • sand
  • + *
  • snow
  • + *
  • ladder
  • + *
  • anvil
  • + *
  • slime
  • + *
  • sea_grass
  • + *
  • coral
  • + *
+ *

material(pos)

+ *

Returns the name of material of the block at position. very useful to target a group of blocks. One of:

+ *
    + *
  • air
  • + *
  • void
  • + *
  • portal
  • + *
  • carpet
  • + *
  • plant
  • + *
  • water_plant
  • + *
  • vine
  • + *
  • sea_grass
  • + *
  • water
  • + *
  • bubble_column
  • + *
  • lava
  • + *
  • snow_layer
  • + *
  • fire
  • + *
  • redstone_bits
  • + *
  • cobweb
  • + *
  • redstone_lamp
  • + *
  • clay
  • + *
  • dirt
  • + *
  • grass
  • + *
  • packed_ice
  • + *
  • sand
  • + *
  • sponge
  • + *
  • wood
  • + *
  • wool
  • + *
  • tnt
  • + *
  • leaves
  • + *
  • glass
  • + *
  • ice
  • + *
  • cactus
  • + *
  • stone
  • + *
  • iron
  • + *
  • snow
  • + *
  • anvil
  • + *
  • barrier
  • + *
  • piston
  • + *
  • coral
  • + *
  • gourd
  • + *
  • dragon_egg
  • + *
  • cake
  • + *
+ *

map_colour(pos)

+ *

Returns the map colour of a block at position. One of:

+ *
    + *
  • air
  • + *
  • grass
  • + *
  • sand
  • + *
  • wool
  • + *
  • tnt
  • + *
  • ice
  • + *
  • iron
  • + *
  • foliage
  • + *
  • snow
  • + *
  • clay
  • + *
  • dirt
  • + *
  • stone
  • + *
  • water
  • + *
  • wood
  • + *
  • quartz
  • + *
  • adobe
  • + *
  • magenta
  • + *
  • light_blue
  • + *
  • yellow
  • + *
  • lime
  • + *
  • pink
  • + *
  • gray
  • + *
  • light_gray
  • + *
  • cyan
  • + *
  • purple
  • + *
  • blue
  • + *
  • brown
  • + *
  • green
  • + *
  • red
  • + *
  • black
  • + *
  • gold
  • + *
  • diamond
  • + *
  • lapis
  • + *
  • emerald
  • + *
  • obsidian
  • + *
  • netherrack
  • + *
  • white_terracotta
  • + *
  • orange_terracotta
  • + *
  • magenta_terracotta
  • + *
  • light_blue_terracotta
  • + *
  • yellow_terracotta
  • + *
  • lime_terracotta
  • + *
  • pink_terracotta
  • + *
  • gray_terracotta
  • + *
  • light_gray_terracotta
  • + *
  • cyan_terracotta
  • + *
  • purple_terracotta
  • + *
  • blue_terracotta
  • + *
  • brown_terracotta
  • + *
  • green_terracotta
  • + *
  • red_terracotta
  • + *
  • black_terracotta
  • + *
+ *
+ */ + + public void API_BlockManipulation() + { + this.expr.addLazyFunction("block", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + if (lv.size() == 0) + { + throw new InternalExpressionException("Block requires at least one parameter"); + } + if (lv.size() == 1) + { + Value retval = BlockValue.fromCommandExpression(lv.get(0).evalValue(cc).getString()); + return (c_, t_) -> retval; + //return new BlockValue(Registry.field_212618_g.get(new Identifier(lv.get(0).getString())).getDefaultState(), origin); + } + Value retval = BlockValue.fromParams(cc, lv, 0).block; + return (c_, t_) -> retval; + }); + + this.expr.addLazyFunction("pos", 1, (c, t, lv) -> + { + Value arg = lv.get(0).evalValue(c); + if (arg instanceof BlockValue) + { + BlockPos pos = ((BlockValue) arg).getPos(); + if (pos == null) + throw new InternalExpressionException("Cannot fetch position of an unrealized block"); + Value retval = ListValue.of(new NumericValue(pos.getX()), new NumericValue(pos.getY()), new NumericValue(pos.getZ())); + return (c_, t_) -> retval; + } + else if (arg instanceof EntityValue) + { + Entity e = ((EntityValue) arg).getEntity(); + if (e == null) + throw new InternalExpressionException("Null entity"); + Value retval = ListValue.of(new NumericValue(e.x), new NumericValue(e.y), new NumericValue(e.z)); + return(c_, t_) -> retval; + } + else + { + throw new InternalExpressionException("pos works only with a block or an entity type"); + } + }); + + this.expr.addLazyFunction("solid", -1, (c, t, lv) -> + booleanStateTest(c, "solid", lv, (s, p) -> s.isFullBoundsCubeForCulling())); + + this.expr.addLazyFunction("air", -1, (c, t, lv) -> + booleanStateTest(c, "air", lv, (s, p) -> s.isAir())); + + this.expr.addLazyFunction("liquid", -1, (c, t, lv) -> + booleanStateTest(c, "liquid", lv, (s, p) -> !s.getFluidState().isEmpty())); + + this.expr.addLazyFunction("flammable", -1, (c, t, lv) -> + booleanStateTest(c, "flammable", lv, (s, p) -> !s.getMaterial().isBurnable())); + + this.expr.addLazyFunction("transparent", -1, (c, t, lv) -> + booleanStateTest(c, "transparent", lv, (s, p) -> !s.getMaterial().isSolid())); + + /*this.expr.addLazyFunction("opacity", -1, (c, t, lv) -> + genericStateTest(c, "opacity", lv, (s, p, w) -> new NumericValue(s.getOpacity(w, p)))); + + this.expr.addLazyFunction("blocks_daylight", -1, (c, t, lv) -> + genericStateTest(c, "blocks_daylight", lv, (s, p, w) -> new NumericValue(s.propagatesSkylightDown(w, p))));*/ // investigate + + this.expr.addLazyFunction("emitted_light", -1, (c, t, lv) -> + genericStateTest(c, "emitted_light", lv, (s, p, w) -> new NumericValue(s.getLuminance()))); + + this.expr.addLazyFunction("light", -1, (c, t, lv) -> + genericStateTest(c, "light", lv, (s, p, w) -> new NumericValue(w.getLightLevel(p)))); + + this.expr.addLazyFunction("block_light", -1, (c, t, lv) -> + genericStateTest(c, "block_light", lv, (s, p, w) -> new NumericValue(w.getLightLevel(LightType.BLOCK, p)))); + + this.expr.addLazyFunction("sky_light", -1, (c, t, lv) -> + genericStateTest(c, "sky_light", lv, (s, p, w) -> new NumericValue(w.getLightLevel(LightType.SKY, p)))); + + this.expr.addLazyFunction("see_sky", -1, (c, t, lv) -> + genericStateTest(c, "see_sky", lv, (s, p, w) -> new NumericValue(w.isSkyVisible(p)))); + + this.expr.addLazyFunction("hardness", -1, (c, t, lv) -> + genericStateTest(c, "hardness", lv, (s, p, w) -> new NumericValue(s.getHardness(w, p)))); + + this.expr.addLazyFunction("blast_resistance", -1, (c, t, lv) -> + genericStateTest(c, "blast_resistance", lv, (s, p, w) -> new NumericValue(s.getBlock().getBlastResistance()))); + + + this.expr.addLazyFunction("top", -1, (c, t, lv) -> { + String type = lv.get(0).evalValue(c).getString().toLowerCase(Locale.ROOT); + Heightmap.Type htype; + switch (type) + { + //case "light": htype = Heightmap.Type.LIGHT_BLOCKING; break; //investigate + case "motion": htype = Heightmap.Type.MOTION_BLOCKING; break; + case "terrain": htype = Heightmap.Type.MOTION_BLOCKING_NO_LEAVES; break; + case "ocean_floor": htype = Heightmap.Type.OCEAN_FLOOR; break; + case "surface": htype = Heightmap.Type.WORLD_SURFACE; break; + default: throw new InternalExpressionException("Unknown heightmap type: "+type); + } + int x; + int z; + Value v1 = lv.get(1).evalValue(c); + if (v1 instanceof BlockValue) + { + BlockPos inpos = ((BlockValue)v1).getPos(); + x = inpos.getX(); + z = inpos.getZ(); + } + else + { + x = (int) NumericValue.asNumber(lv.get(1).evalValue(c)).getLong(); + z = (int) NumericValue.asNumber(lv.get(2).evalValue(c)).getLong(); + } + Value retval = new NumericValue(((CarpetContext)c).s.getWorld().getChunk(x >> 4, z >> 4).sampleHeightmap(htype, x & 15, z & 15) + 1); + return (c_, t_) -> retval; + //BlockPos pos = new BlockPos(x,y,z); + //return new BlockValue(source.getWorld().getBlockState(pos), pos); + }); + + this.expr.addLazyFunction("loaded", -1, (c, t, lv) -> + { + Value retval = ((CarpetContext) c).s.getWorld().isBlockLoaded(BlockValue.fromParams((CarpetContext) c, lv, 0).block.getPos()) ? Value.TRUE : Value.FALSE; + return (c_, t_) -> retval; + }); + + this.expr.addLazyFunction("loaded_ep", -1, (c, t, lv) -> + { + BlockPos pos = BlockValue.fromParams((CarpetContext)c, lv, 0).block.getPos(); + Value retval = ((CarpetContext)c).s.getWorld().isAreaLoaded(pos.getX() - 32, 0, pos.getZ() - 32, + pos.getX() + 32, 0, pos.getZ() + 32) ? Value.TRUE : Value.FALSE; + return (c_, t_) -> retval; + }); + + this.expr.addLazyFunction("suffocates", -1, (c, t, lv) -> + genericStateTest(c, "suffocates", lv, (s, p, w) -> new NumericValue(s.canSuffocate(w, p)))); + + this.expr.addLazyFunction("power", -1, (c, t, lv) -> + genericStateTest(c, "power", lv, (s, p, w) -> new NumericValue(w.getReceivedRedstonePower(p)))); + + this.expr.addLazyFunction("ticks_randomly", -1, (c, t, lv) -> + booleanStateTest(c, "ticks_randomly", lv, (s, p) -> s.hasRandomTicks())); + + this.expr.addLazyFunction("update", -1, (c, t, lv) -> + booleanStateTest(c, "update", lv, (s, p) -> + { + ((CarpetContext) c).s.getWorld().updateNeighbor(p, s.getBlock(), p); + return true; + })); + + this.expr.addLazyFunction("block_tick", -1, (c, t, lv) -> + booleanStateTest(c, "block_tick", lv, (s, p) -> + { + World w = ((CarpetContext)c).s.getWorld(); + s.onRandomTick(w, p, w.random); + return true; + })); + + this.expr.addLazyFunction("random_tick", -1, (c, t, lv) -> + booleanStateTest(c, "random_tick", lv, (s, p) -> + { + World w = ((CarpetContext)c).s.getWorld(); + if (s.hasRandomTicks() || s.getFluidState().hasRandomTicks()) + s.onRandomTick(w, p, w.random); + return true; + })); + + this.expr.addLazyFunction("set", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext)c; + World world = cc.s.getWorld(); + if (lv.size() < 2 || lv.size() % 2 == 1) + throw new InternalExpressionException("set block should have at least 2 params and odd attributes"); + BlockValue.LocatorResult locator = BlockValue.fromParams(cc, lv, 0); + Value blockArg = lv.get(locator.offset).evalValue(cc); + BlockValue bv = ((blockArg instanceof BlockValue)) ? (BlockValue) blockArg : BlockValue.fromString(blockArg.getString()); + if (bv == BlockValue.NULL) + throw new InternalExpressionException("block to set to should be a valid block"); + BlockState bs = bv.getBlockState(); + + BlockState targetBlockState = world.getBlockState(locator.block.getPos()); + if (lv.size()==1+locator.offset && !(blockArg instanceof BlockValue)) // no reqs for properties + if (targetBlockState.getBlock() == bs.getBlock()) + return (c_, t_) -> Value.FALSE; + + StateFactory states = bs.getBlock().getStateFactory(); + + for (int i = 1+locator.offset; i < lv.size(); i += 2) + { + String paramString = lv.get(i).evalValue(c).getString(); + Property property = states.getProperty(paramString); + if (property == null) + throw new InternalExpressionException("property " + paramString + " doesn't apply to " + blockArg.getString()); + + String paramValue = lv.get(i + 1).evalValue(c).getString(); + + bs = setProperty(property, paramString, paramValue, bs); + } + cc.s.getWorld().setBlockState(locator.block.getPos(), bs, 2 | (CarpetSettings.getBool("fillUpdates") ? 0 : 1024)); + Value retval = new BlockValue(bs, world, locator.block.getPos()); + return (c_, t_) -> retval; + }); + + this.expr.addLazyFunction("blocks_movement", -1, (c, t, lv) -> + booleanStateTest(c, "blocks_movement", lv, (s, p) -> + !s.canPlaceAtSide(((CarpetContext) c).s.getWorld(), p, BlockPlacementEnvironment.LAND))); + + this.expr.addLazyFunction("block_sound", -1, (c, t, lv) -> + stateStringQuery(c, "block_sound", lv, (s, p) -> + BlockInfo.soundName.get(s.getSoundGroup()))); + + this.expr.addLazyFunction("material",-1, (c, t, lv) -> + stateStringQuery(c, "material", lv, (s, p) -> + BlockInfo.materialName.get(s.getMaterial()))); + + this.expr.addLazyFunction("map_colour", -1, (c, t, lv) -> + stateStringQuery(c, "map_colour", lv, (s, p) -> + BlockInfo.mapColourName.get(s.getTopMaterialColor(((CarpetContext)c).s.getWorld(), p)))); + + this.expr.addLazyFunction("property", -1, (c, t, lv) -> + { + BlockValue.LocatorResult locator = BlockValue.fromParams((CarpetContext) c, lv, 0); + BlockState state = locator.block.getBlockState(); + String tag = lv.get(locator.offset).evalValue(c).getString(); + StateFactory states = state.getBlock().getStateFactory(); + Property property = states.getProperty(tag); + if (property == null) + return LazyValue.NULL; + Value retval = new StringValue(state.get(property).toString()); + return (_c, _t ) -> retval; + }); + } + + /** + *

Entity API

+ *
+ *

Entity Selection

+ *

Entities have to be fetched before using them. Entities can also change their state between calls to the script + * if game happens either in between separate calls to the programs, or if the program calls game_tick + * on its own. In this case - entities would need to be re-fetched, or the code should account for entities getting dead

+ *

player(), player(type), player(name)

+ *

+ * With no arguments, it returns the calling player or the player closest to the caller. Note that the main context + * will receive p variable pointing to this player. With type or name specified + * it will try first to match a type, returning a list of players matching a type, and if this fails, will assume its + * player name query retuning player with that name, or null if no player was found. + * With 'all', list of all players in the game, in all dimensions, so end user needs to be cautious, that + * you might be referring to wrong blocks and entities around the player in question. + * WIth type = '*' it returns all players in caller dimension, 'survival' returns all survival + * and adventure players, 'creative' returns all creative players, 'spectating' returns all spectating + * players, and '!spectating', all not-spectating players. If all fails, + * with name, the player in question, if is logged in.

+ *

entity_id(uuid), entity_id(id)

+ *

Fetching entities wither by their ID obtained via entity ~ 'id', which is unique + * for a dimension and current world run, or by UUID, obtained via entity ~ 'uuid'. + * It returns null if no such entity + * is found. Safer way to 'store' entities between calls, as missing entities will be returning null. + * Both calls using UUID or numerical ID are O(1), but obviously using UUIDs takes more memory and compute.

+ *

entity_list(type)

+ *

Returns global lists of entities in the current dimension of a specified type. Currently the following selectors are available:

+ *
    + *
  • *: all
  • + *
  • living
  • + *
  • items
  • + *
  • players
  • + *
  • !players
  • + *
+ * + *

entity_area(type, cx, cy, cz, dx, dy, dz)

+ *

Returns entities of a specified type in an area centered on cx, cy, cz and + * at most dx, dy, dz blocks away from the center point. Uses same selectors as entities_list

+ * + *

entity_selector(selector)

+ *

Returns entities satisfying given vanilla entity selector. Most complex among all the methods of selecting + * entities, but the most capable. Selectors are cached so should be as fast as other methods of selecting entities.

+ * + *

Entity Manipulation

+ * + *

Unlike with blocks, that use plethora of vastly different querying functions, entities are queried with + * query function and altered via modify function. Type of information needed or + * values to be modified are different for each call

+ *

query(e,'removed')

+ *

Boolean. True if the entity is removed

+ *

query(e,'id')

+ *

Returns numerical id of the entity. Most efficient way to keep track of entites in a script. Ids are only unique + * within current game session (ids are not preserved between restarts), and dimension (each dimension has its own ids + * which can overlap.

+ *

query(e,'uuid')

+ *

Returns UUID (unique id) of the entity. Can be used to access entities with the other vanilla commands and remains unique + * regardless of the dimension, and is preserved between game restarts. + * Apparently players cannot be accessed via UUID, but name instead.

+ *
+     * map(entities_area('*',x,y,z,30,30,30),run('kill '+query(_,'id'))) // doesn't kill the player
+     * 
+ *

query(e,'pos')

+ *

Triple of entity position

+ *

query(e,'location')

+ *

Quin-tuple of entity position (x, y, and z coords), and rotation (yaw, pitch)

+ *

query(e,'x'), query(e,'y'), query(e,'z')

+ *

Respective entity coordinate

+ *

query(e,'pitch'), query(e,'yaw')

+ *

Pitch and Yaw or where entity is looking.

+ *

query(e,'motion')

+ *

Triple of entity motion vector, l(motion_x, motion_y, motion_z)

+ *

query(e,'motion_x'), query(e,'motion_y'), query(e,'motion_z')

+ *

Respective component of the motion vector

+ *

query(e,'name'), query(e,'custom_name'), query(e,'type')

+ *

String of entity name

+ *
+     * query(e,'name')  => Leatherworker
+     * query(e,'custom_name')  => null
+     * query(e,'type')  => villager
+     * 
+ *

query(e,'is_riding')

+ *

Boolean. True if riding another entity.

+ *

query(e,'is_ridden')

+ *

Boolean. True if another entity is riding it.

+ *

query(e,'passengers')

+ *

List of entities riding the entity.

+ *

query(e,'mount')

+ *

Entity that e rides.

+ *

query(e,'tags')

+ *

List of entity tags.

+ *

query(e,'has_tags',tag)

+ *

Boolean, True if the entity is marked with tag.

+ *

query(e,'is_burning')

+ *

Boolean, True if the entity is burning.

+ *

query(e,'fire')

+ *

Number of remaining ticks of being on fire.

+ *

query(e,'silent')

+ *

Boolean, True if the entity is silent.

+ *

query(e,'gravity')

+ *

Boolean, True if the entity is affected by gravity, like most entities do.

+ *

query(e,'immune_to_fire')

+ *

Boolean, True if the entity is immune to fire.

+ *

query(e,'dimension')

+ *

Name of the dimension entity is in.

+ *

query(e,'height')

+ *

Height of the entity.

+ *

query(e,'width')

+ *

Width of the entity.

+ *

query(e,'eye_height')

+ *

Eye height of the entity.

+ *

query(e,'age')

+ *

Age, in ticks, of the entity, i.e. how long it existed.

+ *

query(e,'item')

+ *

Name of the item if its an item entity, null otherwise

+ *

query(e,'count')

+ *

Number of items in a stack from item entity, null otherwise

+ *

query(e,'is_baby')

+ *

Boolean, true if its a baby.

+ *

query(e,'target')

+ *

Returns mob's attack target or null if none or not applicable.

+ *

query(e,'home')

+ *

Returns creature's home position or null if none or not applicable.

+ *

query(e,'sneaking')

+ *

Boolean, true if entity is sneaking.

+ *

query(e,'sprinting')

+ *

Boolean, true if entity is sprinting.

+ *

query(e,'swimming')

+ *

Boolean, true if entity is swimming.

+ *

query(e,'jumping')

+ *

Boolean, true if entity is jumping.

+ *

query(e,'gamemode')

+ *

String with gamemode, or null if not a player.

+ *

query(e,'gamemode_id')

+ *

Good'ol gamemode id, or null if not a player.

+ *

query(e,'effect',name?)

+ *

Without extra arguments, it returns list of effect active on a living entity. + * Each entry is a triple of short effect name, amplifier, and remaining duration. + * With an argument, if the living entity has not that potion active, returns null, otherwise + * return a tuple of amplifier and remaining duration

+ *
+     * query(p,'effect')  => [[haste, 0, 177], [speed, 0, 177]]
+     * query(p,'effect','haste')  => [0, 177]
+     * query(p,'effect','resistance')  => null
+     * 
+ *

query(e,'health')

+ *

Number indicating remaining entity health, or null if not applicable.

+ *

query(e,'holds',slot?)

+ *

Returns triple of short name, stack count, and NBT of item held in slot. + * Available options for slot are:

+ *
    + *
  • main
  • + *
  • offhand
  • + *
  • head
  • + *
  • chest
  • + *
  • legs
  • + *
  • feet
  • + *
+ *

If slot is not specified, it defaults to the main hand.

+ *

query(e,'nbt',path?)

+ *

Returns full NBT of the entity. If path is specified, it fetches only that portion of the NBT, + * that corresponds to the path. For specification of path attribute, consult + * vanilla /data get entity command.

+ *

Note that calls to nbt are considerably more expensive comparing to other + * calls in Minecraft API, and should only be used when there is no other option. Also returned + * NBT is just a string to any retrieval of information post can currently only be done with matching + * operator ~. With time we are hoping not to support the 'nbt' call better, + * but rather to fill the API, so that 'nbt' calls are not needed

+ *

Entity Modification

+ *

Like with entity querying, entity modifications happen through one function. Most position and movements + * modifications don't work currently on players as their position is controlled by clients.

+ *

Currently there is no ability to modify NBT directly, but you could always use run('data modify entity

+ *

modify(e,'remove')

+ *

Removes (not kills) entity from the game.

+ *

modify(e,'kill')

+ *

Kills the entity.

+ *

modify(e, 'pos', x, y, z), modify(e, 'pos', l(x,y,z) )

+ *

Moves the entity to a specified coords.

+ *

modify(e, 'location', x, y, z, yaw, pitch), modify(e, 'location', l(x, y, z, yaw, pitch) )

+ *

Changes full location vector all at once.

+ *

modify(e, 'x', x), modify(e, 'y', y), modify(e, 'z', z)

+ *

Moves the entity in.... one direction.

+ *

modify(e, 'pitch', pitch), modify(e, 'yaw', yaw)

+ *

Changes entity's pitch or yaw.

+ *

modify(e, 'move', x, y, z), modify(e, 'move', l(x,y,z) )

+ *

Moves the entity by a vector from its current location.

+ *

modify(e, 'motion', x, y, z), modify(e, 'motion', l(x,y,z) )

+ *

Sets the motion vector (where and how much entity is moving).

+ *

modify(e, 'motion_z', x), modify(e, 'motion_y', y), modify(e, 'motion_z', z)

+ *

Sets the corresponding component of the motion vector.

+ *

modify(e, 'accelerate', x, y, z), modify(e, 'accelerate', l(x, y, z) )

+ *

Adds a vector to the motion vector. Most realistic way to apply a force to an entity.

+ *

modify(e, 'custom_name'), modify(e, 'custom_name', name )

+ *

Sets a custom name for an entity. No argument sets it empty, not removes it. Vanilla doesn't allow removing + * of attributes.

+ *

modify(e, 'dismount')

+ *

Dismounts riding entity.

+ *

modify(e, 'mount', other)

+ *

Mounts the entity to the other.

+ *

modify(e, 'drop_passengers')

+ *

Shakes off all passengers.

+ *

modify(e, 'mount_passengers', passenger, ? ...), modify(e, 'mount_passengers', l(passengers) )

+ *

Mounts on all listed entities on e.

+ *

modify(e, 'tag', tag, ? ...), modify(e, 'tag', l(tags) )

+ *

Adds tag / tags to the entity.

+ *

modify(e, 'clear_tag', tag, ? ...), modify(e, 'clear_tag', l(tags) )

+ *

Removes tag from entity.

+ *

modify(e, 'talk')

+ *

Make noises.

+ *

modify(e, 'home', null), modify(e, 'home', block, distance?), modify(e, 'home', x, y, z, distance?)

+ *

Sets AI to stay around the home position, within distance blocks from it. distance + * defaults to 16 blocks. null removes it. May not work fully with mobs that have this AI built in, like + * Villagers.

+ *
+ */ + + public void API_EntityManipulation() + { + this.expr.addLazyFunction("player", -1, (c, t, lv) -> { + if (lv.size() ==0) + { + + Entity callingEntity = ((CarpetContext)c).s.getEntity(); + if (callingEntity instanceof PlayerEntity) + { + Value retval = new EntityValue(callingEntity); + return (_c, _t) -> retval; + } + Vec3d pos = ((CarpetContext)c).s.getPosition(); + PlayerEntity closestPlayer = ((CarpetContext)c).s.getWorld().getClosestPlayer(pos.x, pos.y, pos.z, -1.0, EntityPredicates.VALID_ENTITY); + if (closestPlayer != null) + { + Value retval = new EntityValue(closestPlayer); + return (_c, _t) -> retval; + } + return (_c, _t) -> Value.NULL; + } + String playerName = lv.get(0).evalValue(c).getString(); + Value retval = Value.NULL; + if ("all".equalsIgnoreCase(playerName)) + { + retval = ListValue.wrap( + ((CarpetContext)c).s.getMinecraftServer().getPlayerManager().getPlayerList(). + stream().map(EntityValue::new).collect(Collectors.toList())); + } + else if ("*".equalsIgnoreCase(playerName)) + { + retval = ListValue.wrap( + ((CarpetContext)c).s.getWorld().getPlayers(). + stream().map(EntityValue::new).collect(Collectors.toList())); + } + else if ("survival".equalsIgnoreCase(playerName)) + { + retval = ListValue.wrap( + ((CarpetContext)c).s.getWorld().getPlayers((p) -> p.interactionManager.isSurvivalLike()). + stream().map(EntityValue::new).collect(Collectors.toList())); + } + else if ("creative".equalsIgnoreCase(playerName)) + { + retval = ListValue.wrap( + ((CarpetContext)c).s.getWorld().getPlayers(PlayerEntity::isCreative). + stream().map(EntityValue::new).collect(Collectors.toList())); + } + else if ("spectating".equalsIgnoreCase(playerName)) + { + retval = ListValue.wrap( + ((CarpetContext)c).s.getWorld().getPlayers(PlayerEntity::isSpectator). + stream().map(EntityValue::new).collect(Collectors.toList())); + } + else if ("!spectating".equalsIgnoreCase(playerName)) + { + retval = ListValue.wrap( + ((CarpetContext)c).s.getWorld().getPlayers((p) -> !p.isSpectator()). + stream().map(EntityValue::new).collect(Collectors.toList())); + } + else + { + ServerPlayerEntity player = ((CarpetContext) c).s.getMinecraftServer().getPlayerManager().getPlayer(playerName); + if (player != null) + retval = new EntityValue(player); + } + Value finalVar = retval; + return (cc, tt) -> finalVar; + }); + + this.expr.addLazyFunction("entity_id", 1, (c, t, lv) -> + { + Value who = lv.get(0).evalValue(c); + Entity e; + if (who instanceof NumericValue) + { + e = ((CarpetContext)c).s.getWorld().getEntityById((int)((NumericValue) who).getLong()); + } + else + { + e = ((CarpetContext)c).s.getWorld().getEntity(UUID.fromString(who.getString())); + } + if (e==null) + { + return LazyValue.NULL; + } + return (cc, tt) -> new EntityValue(e); + }); + + this.expr.addLazyFunction("entity_list", 1, (c, t, lv) -> { + String who = lv.get(0).evalValue(c).getString(); + Pair, Predicate> pair = EntityValue.getPredicate(who); + if (pair == null) + { + throw new InternalExpressionException("Unknown entity selection criterion: "+who); + } + List entityList = ((CarpetContext)c).s.getWorld().getEntities(pair.getKey(), pair.getValue()); + Value retval = ListValue.wrap(entityList.stream().map(EntityValue::new).collect(Collectors.toList())); + return (_c, _t ) -> retval; + }); + + this.expr.addLazyFunction("entity_area", 7, (c, t, lv) -> { + BlockPos center = new BlockPos( + NumericValue.asNumber(lv.get(1).evalValue(c)).getDouble(), + NumericValue.asNumber(lv.get(2).evalValue(c)).getDouble(), + NumericValue.asNumber(lv.get(3).evalValue(c)).getDouble() + ); + BoundingBox area = new BoundingBox(center).expand( + NumericValue.asNumber(lv.get(4).evalValue(c)).getDouble(), + NumericValue.asNumber(lv.get(5).evalValue(c)).getDouble(), + NumericValue.asNumber(lv.get(6).evalValue(c)).getDouble() + ); + String who = lv.get(0).evalValue(c).getString(); + Pair, Predicate> pair = EntityValue.getPredicate(who); + if (pair == null) + { + throw new InternalExpressionException("Unknown entity selection criterion: "+who); + } + List entityList = ((CarpetContext)c).s.getWorld().getEntities(pair.getKey(), area, pair.getValue()); + Value retval = ListValue.wrap(entityList.stream().map(EntityValue::new).collect(Collectors.toList())); + return (_c, _t ) -> retval; + }); + + this.expr.addLazyFunction("entity_selector", -1, (c, t, lv) -> + { + String selector = lv.get(0).evalValue(c).getString(); + List retlist = new ArrayList<>(); + for (Entity e: EntityValue.getEntitiesFromSelector(((CarpetContext)c).s, selector)) + { + retlist.add(new EntityValue(e)); + } + return (c_, t_) -> ListValue.wrap(retlist); + }); + + + this.expr.addLazyFunction("query", -1, (c, t, lv) -> { + if (lv.size()<2) + { + throw new InternalExpressionException("query_entity takes entity as a first argument, and queried feature as a second"); + } + Value v = lv.get(0).evalValue(c); + if (!(v instanceof EntityValue)) + throw new InternalExpressionException("First argument to query_entity should be an entity"); + String what = lv.get(1).evalValue(c).getString(); + Value retval; + if (lv.size()==2) + retval = ((EntityValue) v).get(what, null); + else if (lv.size()==3) + retval = ((EntityValue) v).get(what, lv.get(2).evalValue(c)); + else + retval = ((EntityValue) v).get(what, ListValue.wrap(lv.subList(2, lv.size()).stream().map((vv) -> vv.evalValue(c)).collect(Collectors.toList()))); + return (cc, tt) -> retval; + }); + + // or update + this.expr.addLazyFunction("modify", -1, (c, t, lv) -> { + if (lv.size()<2) + { + throw new InternalExpressionException("modify_entity takes entity as a first argument, and queried feature as a second"); + } + Value v = lv.get(0).evalValue(c); + if (!(v instanceof EntityValue)) + throw new InternalExpressionException("First argument to get should be an entity"); + String what = lv.get(1).evalValue(c).getString(); + if (lv.size()==2) + ((EntityValue) v).set(what, null); + else if (lv.size()==3) + ((EntityValue) v).set(what, lv.get(2).evalValue(c)); + else + ((EntityValue) v).set(what, ListValue.wrap(lv.subList(2, lv.size()).stream().map((vv) -> vv.evalValue(c)).collect(Collectors.toList()))); + return lv.get(0); + }); + } + + /** + *

Iterating over larger areas of blocks

+ *
+ *

These functions help scan larger areas of blocks without using generic loop functions, + * like nested loop.

+ *

+ *

scan(cx, cy, cz, dx, dy, dz, px?, py?, pz?, expr)

+ *

Evaluates expression over area of blocks defined by its center (cx, cy, cz), + * expanded in all directions by dx, dy, dz blocks, or optionally in negative with d coords, + * and p coords in positive values. expr receives _x, _y, _z + * as coords of current analyzed block and _ which represents the block itself.

+ *

volume(x1, y1, z1, x2, y2, z2, expr)

+ *

Evaluates expression for each block in the area, the same as the scanfunction, but using two opposite + * corners of the rectangular cuboid. Any corners can be specified, its like you would do with /fill command

+ *

neighbours(x, y, z), neighbours(block), neighbours(l(x,y,z))

+ *

Returns the list of 6 neighbouring blocks to the argument. Commonly used with other loop functions like for

+ *
+     * for(neighbours(x,y,z),air(_)) => 4 // number of air blocks around a block
+     * 
+ *

rect(cx, cy, cz, dx?, dy?, dz?, px?, py?, pz?)

+ *

returns an iterator, just like range function that iterates over rectangular cubarea of blocks. If + * only center point is specified, it iterates over 27 blocks. If d arguments are specified, expands selection + * of respective number of blocks in each direction. If p arguments are specified, it uses d for + * negative offset, and p for positive.

+ *

diamond(cx, cy, cz, radius?, height?)

+ *

Iterates over a diamond like area of blocks. With no radius and height, its 7 blocks centered around the middle + * (block + neighbours). With a radius specified, it expands shape on x and z coords, and with a custom height, on y. Any of these can be + * zero as well. radius of 0 makes a stick, height of 0 makes a diamond shape pad.

+ *
+ */ + + public void API_IteratingOverAreasOfBlocks() + { + this.expr.addLazyFunction("scan", -1, (c, t, lv) -> + { + int lvsise = lv.size(); + if (lvsise != 7 && lvsise != 10) + throw new InternalExpressionException("scan takes 2, or 3 triples of coords, and the expression"); + int cx = (int) NumericValue.asNumber(lv.get(0).evalValue(c)).getLong(); + int cy = (int) NumericValue.asNumber(lv.get(1).evalValue(c)).getLong(); + int cz = (int) NumericValue.asNumber(lv.get(2).evalValue(c)).getLong(); + int xrange = (int) NumericValue.asNumber(lv.get(3).evalValue(c)).getLong(); + int yrange = (int) NumericValue.asNumber(lv.get(4).evalValue(c)).getLong(); + int zrange = (int) NumericValue.asNumber(lv.get(5).evalValue(c)).getLong(); + int xprange = xrange; + int yprange = yrange; + int zprange = zrange; + LazyValue expr; + if (lvsise == 7) + { + expr = lv.get(6); + } + else + { + xprange = (int) NumericValue.asNumber(lv.get(6).evalValue(c)).getLong(); + yprange = (int) NumericValue.asNumber(lv.get(7).evalValue(c)).getLong(); + zprange = (int) NumericValue.asNumber(lv.get(8).evalValue(c)).getLong(); + expr = lv.get(9); + } + + //saving outer scope + LazyValue _x = c.getVariable("_x"); + LazyValue _y = c.getVariable("_y"); + LazyValue _z = c.getVariable("_z"); + LazyValue __ = c.getVariable("_"); + int sCount = 0; + for (int y=cy-yrange; y <= cy+yprange; y++) + { + int yFinal = y; + c.setVariable("_y", (c_, t_) -> new NumericValue(yFinal).bindTo("_y")); + for (int x=cx-xrange; x <= cx+xprange; x++) + { + int xFinal = x; + c.setVariable("_x", (c_, t_) -> new NumericValue(xFinal).bindTo("_x")); + for (int z=cz-zrange; z <= cz+zprange; z++) + { + int zFinal = z; + + c.setVariable("_z", (c_, t_) -> new NumericValue(zFinal).bindTo("_z")); + Value blockValue = BlockValue.fromCoords(((CarpetContext)c), xFinal,yFinal,zFinal).bindTo("_"); + c.setVariable( "_", (cc_, t_c) -> blockValue); + if (expr.evalValue(c, Context.BOOLEAN).getBoolean()) + { + sCount += 1; + } + } + } + } + //restoring outer scope + c.setVariable("_x", _x); + c.setVariable("_y", _y); + c.setVariable("_z", _z); + c.setVariable("_", __); + int finalSCount = sCount; + return (c_, t_) -> new NumericValue(finalSCount); + }); + + this.expr.addLazyFunction("volume", 7, (c, t, lv) -> + { + int xi = (int) NumericValue.asNumber(lv.get(0).evalValue(c)).getLong(); + int yi = (int) NumericValue.asNumber(lv.get(1).evalValue(c)).getLong(); + int zi = (int) NumericValue.asNumber(lv.get(2).evalValue(c)).getLong(); + int xj = (int) NumericValue.asNumber(lv.get(3).evalValue(c)).getLong(); + int yj = (int) NumericValue.asNumber(lv.get(4).evalValue(c)).getLong(); + int zj = (int) NumericValue.asNumber(lv.get(5).evalValue(c)).getLong(); + int minx = min(xi, xj); + int miny = min(yi, yj); + int minz = min(zi, zj); + int maxx = max(xi, xj); + int maxy = max(yi, yj); + int maxz = max(zi, zj); + LazyValue expr = lv.get(6); + + //saving outer scope + LazyValue _x = c.getVariable("_x"); + LazyValue _y = c.getVariable("_y"); + LazyValue _z = c.getVariable("_z"); + LazyValue __ = c.getVariable("_"); + int sCount = 0; + for (int y=miny; y <= maxy; y++) + { + int yFinal = y; + c.setVariable("_y", (c_, t_) -> new NumericValue(yFinal).bindTo("_y")); + for (int x=minx; x <= maxx; x++) + { + int xFinal = x; + c.setVariable("_x", (c_, t_) -> new NumericValue(xFinal).bindTo("_x")); + for (int z=minz; z <= maxz; z++) + { + int zFinal = z; + c.setVariable("_z", (c_, t_) -> new NumericValue(zFinal).bindTo("_z")); + Value blockValue = BlockValue.fromCoords(((CarpetContext)c), xFinal,yFinal,zFinal).bindTo("_"); + c.setVariable( "_", (cc_, t_c) -> blockValue); + if (expr.evalValue(c, Context.BOOLEAN).getBoolean()) + { + sCount += 1; + } + } + } + } + //restoring outer scope + c.setVariable("_x", _x); + c.setVariable("_y", _y); + c.setVariable("_z", _z); + c.setVariable("_", __); + int finalSCount = sCount; + return (c_, t_) -> new NumericValue(finalSCount); + }); + + this.expr.addLazyFunction("neighbours", -1, (c, t, lv)-> + { + + BlockPos center = BlockValue.fromParams((CarpetContext) c, lv,0).block.getPos(); + World world = ((CarpetContext) c).s.getWorld(); + + List neighbours = new ArrayList<>(); + neighbours.add(new BlockValue(null, world, center.up())); + neighbours.add(new BlockValue(null, world, center.down())); + neighbours.add(new BlockValue(null, world, center.north())); + neighbours.add(new BlockValue(null, world, center.south())); + neighbours.add(new BlockValue(null, world, center.east())); + neighbours.add(new BlockValue(null, world, center.west())); + Value retval = ListValue.wrap(neighbours); + return (c_, t_) -> retval; + }); + + this.expr.addLazyFunction("rect", -1, (c, t, lv)-> + { + if (lv.size() != 3 && lv.size() != 6 && lv.size() != 9) + { + throw new InternalExpressionException("rectangular region should be specified with 3, 6, or 9 coordinates"); + } + int cx; + int cy; + int cz; + int sminx; + int sminy; + int sminz; + int smaxx; + int smaxy; + int smaxz; + try + { + cx = (int)((NumericValue) lv.get(0).evalValue(c)).getLong(); + cy = (int)((NumericValue) lv.get(1).evalValue(c)).getLong(); + cz = (int)((NumericValue) lv.get(2).evalValue(c)).getLong(); + if (lv.size()==3) // only done this way because of stupid Java lambda final reqs + { + sminx = 1; + sminy = 1; + sminz = 1; + smaxx = 1; + smaxy = 1; + smaxz = 1; + } + else if (lv.size()==6) + { + sminx = (int) ((NumericValue) lv.get(3).evalValue(c)).getLong(); + sminy = (int) ((NumericValue) lv.get(4).evalValue(c)).getLong(); + sminz = (int) ((NumericValue) lv.get(5).evalValue(c)).getLong(); + smaxx = sminx; + smaxy = sminy; + smaxz = sminz; + } + else // size == 9 + { + sminx = (int) ((NumericValue) lv.get(3).evalValue(c)).getLong(); + sminy = (int) ((NumericValue) lv.get(4).evalValue(c)).getLong(); + sminz = (int) ((NumericValue) lv.get(5).evalValue(c)).getLong(); + smaxx = (int)((NumericValue) lv.get(6).evalValue(c)).getLong(); + smaxy = (int)((NumericValue) lv.get(7).evalValue(c)).getLong(); + smaxz = (int)((NumericValue) lv.get(8).evalValue(c)).getLong(); + } + } + catch (ClassCastException exc) + { + throw new InternalExpressionException("Attempted to pass a non-number to rect"); + } + CarpetContext cc = (CarpetContext)c; + return (c_, t_) -> new LazyListValue() + { + int minx = cx-sminx; + int miny = cy-sminy; + int minz = cz-sminz; + int maxx = cx+smaxx; + int maxy = cy+smaxy; + int maxz = cz+smaxz; + int x; + int y; + int z; + { + x = minx; + y = miny; + z = minz; + } + @Override + public boolean hasNext() + { + return y <= maxy; + } + + @Override + public Value next() + { + Value r = BlockValue.fromCoords(cc, x,y,z); + //possibly reroll context + x++; + if (x > maxx) + { + x = minx; + z++; + if (z > maxz) + { + z = minz; + y++; + // hasNext should fail if we went over + } + } + + return r; + } + + @Override + public void fatality() + { + // possibly return original x, y, z + } + }; + }); + + this.expr.addLazyFunction("diamond", -1, (c, t, lv)-> + { + CarpetContext cc = (CarpetContext)c; + if (lv.size() != 3 && lv.size() != 4 && lv.size() != 5) + { + throw new InternalExpressionException("diamond region should be specified with 3 to 5 coordinates"); + } + + int cx; + int cy; + int cz; + int width; + int height; + try + { + cx = (int)((NumericValue) lv.get(0).evalValue(c)).getLong(); + cy = (int)((NumericValue) lv.get(1).evalValue(c)).getLong(); + cz = (int)((NumericValue) lv.get(2).evalValue(c)).getLong(); + if (lv.size()==3) + { + Value retval = new ListValue(Arrays.asList( + BlockValue.fromCoords(cc, cx, cy-1, cz), + BlockValue.fromCoords(cc, cx, cy, cz), + BlockValue.fromCoords(cc, cx-1, cy, cz), + BlockValue.fromCoords(cc, cx, cy, cz-1), + BlockValue.fromCoords(cc, cx+1, cy, cz), + BlockValue.fromCoords(cc, cx, cy, cz+1), + BlockValue.fromCoords(cc, cx, cy+1, cz) + )); + return (_c, _t ) -> retval; + } + else if (lv.size()==4) + { + width = (int) ((NumericValue) lv.get(3).evalValue(c)).getLong(); + height = 0; + } + else // size == 5 + { + width = (int) ((NumericValue) lv.get(3).evalValue(c)).getLong(); + height = (int) ((NumericValue) lv.get(4).evalValue(c)).getLong(); + } + } + catch (ClassCastException exc) + { + throw new InternalExpressionException("Attempted to pass a non-number to diamond"); + } + if (height == 0) + { + return (c_, t_) -> new LazyListValue() + { + int curradius = 0; + int curpos = 0; + { + + } + @Override + public boolean hasNext() + { + return curradius <= width; + } + + @Override + public Value next() + { + if (curradius == 0) + { + curradius = 1; + return BlockValue.fromCoords(cc, cx, cy, cz); + } + // x = 3-|i-6| + // z = |( (i-3)%12-6|-3 + Value block = BlockValue.fromCoords(cc, cx+(curradius-abs(curpos-2*curradius)), cy, cz-curradius+abs( abs(curpos-curradius)%(4*curradius) -2*curradius )); + curpos++; + if (curpos>=curradius*4) + { + curradius++; + curpos = 0; + } + return block; + + } + }; + } + else + { + return (c_, t_) -> new LazyListValue() + { + int curradius = 0; + int curpos = 0; + int curheight = -height; + @Override + public boolean hasNext() + { + return curheight <= height; + } + + @Override + public Value next() + { + if (curheight == -height || curheight == height) + { + return BlockValue.fromCoords(cc, cx, cy+curheight++, cz); + } + if (curradius == 0) + { + curradius++; + return BlockValue.fromCoords(cc, cx, cy+curheight, cz); + } + // x = 3-|i-6| + // z = |( (i-3)%12-6|-3 + + Value block = BlockValue.fromCoords(cc, cx+(curradius-abs(curpos-2*curradius)), cy+curheight, cz-curradius+abs( abs(curpos-curradius)%(4*curradius) -2*curradius )); + curpos++; + if (curpos>=curradius*4) + { + curradius++; + curpos = 0; + if (curradius>width -abs(width*curheight/height)) + { + curheight++; + curradius = 0; + curpos = 0; + } + } + return block; + } + }; + } + }); + } + + //TODO sounds + /** + *

Auxiliary aspects

+ *
+ *

Collection of other methods that control smaller, yet still important aspects of the game

+ *

Sounds

+ *

sound(name, pos, volume?, pitch?)

+ *

Plays a specific sound name, at block or position pos, with optional + * volume and modified pitch. pos can be either a block, triple of coords, + * or a list of thee numbers. Uses the same options as a corresponding playsound command.

+ *

Particles

+ *

particle(name, pos, count?. spread?, speed?, playername?)

+ *

Renders a cloud of particles name centered around pos position, by default + * count 10 of them, default speed of 0, and to all players nearby, but these + * options can be changed via optional arguments. Follow vanilla /particle command on details on those + * options. Valid particle names are for example + * 'angry_villager', 'item diamond', 'block stone', 'dust 0.8 0.1 0.1 4'

+ *

particle_line(name, pos, pos2, density?)

+ *

Renders a line of particles from point pos to pos2 with supplied density (defaults 1), + * which indicates how far part you would want particles to appear, so 0.1 means one every 10cm.

+ *

particle_rect(name, pos, pos2, density?)

+ *

Renders a cuboid of particles between point pos to pos2 with supplied density.

+ *

System function

+ *

print(expr)

+ *

Displays the result of the expression to the chat. Overrides default scarpet behaviour of + * sending everyting to stderr.

+ *

run(expr)

+ *

Runs a command from the string result of the expr expression, and returns its success count

+ *

save()

+ *

Performs autosave, saves all chunks, player data, etc. Useful for programs where autosave is disabled + * due to performance reasons and saves the world only on demand.

+ *

tick_time()

+ *

Returns game tick counter. Can be used to run certain operations every n-th ticks, or to count in-game time

+ *

game_tick(mstime?)

+ *

Causes game to run for one tick. By default runs it and returns control to the program, but can optionally + * accept expected tick length, in milliseconds. You can't use it to permanently change the game speed, but + * setting longer commands with custom tick speeds can be interrupted via /script stop command

+ *
+     * loop(1000,tick())  // runs the game as fast as it can for 1000 ticks
+     * loop(1000,tick(100)) // runs the game twice as slow for 1000 ticks
+     * 
+ *

current_dimension()

+ *

Returns current dimension that scripts run in.

+ *

plop(pos, what)

+ *

Plops a structure or a feature at a given pos, so block, triple position coordinates + * or a list of coordinates. To what gets plopped and exactly where it often depends on the + * feature or structure itself. For example, all structures are chunk aligned, and often span multiple chunks. + * Repeated calls to plop a structure in the same chunk would result either in the same strucuture generated on + * top of each other, or with different state, but same position. Most + * structures generate at specific altitudes, which are hardcoded, or with certain blocks around them. API will cancel + * all extra position / biome / random requirements for structure / feature placement, but some hardcoded limitations + * may still cause some of strucutures/features not to place. Some features require special blocks to be present, like + * coral -> water or ice spikes -> snow block, and for some features, like fossils, placement is all sorts of + * messed up.

+ *

+ * All generated structures will retain their properties, like mob spawning, however in many cases the world / dimension + * itself has certain rules to spawn mobs, like plopping a nether fortress in the overworld will not spawn nether mobs + * because nether mobs can spawn only in the nether, but plopped in the nether - will behave like a valid nether + * fortress.

+ *

plop will not use world random number generator to generate structures and features, but its own. + * This has a benefit that they will generate properly randomly, not the same time every time

+ *

Structure list:

+ *
    + * + *
  • monument: Ocean Monument. Generates at fixed Y coordinate, surrounds itself with water.
  • + *
  • fortress: Nether Fortress. Altitude varies, but its bounded by the code.
  • + *
  • mansion: Woodland Mansion
  • + *
  • jungle_temple: Jungle Temple
  • + *
  • desert_temple: Desert Temple. Generates at fixed Y altitude.
  • + *
  • end_city: End City with Shulkers
  • + *
  • igloo: Igloo
  • + *
  • shipwreck: Shipwreck, version1?
  • + *
  • shipwreck2: Shipwreck, version2?
  • + *
  • witchhut
  • + *
  • oceanruin, oceanruin_small, oceanruin_tall: Stone variants of ocean ruins.
  • + *
  • oceanruin_warm, oceanruin_warm_small, oceanruin_warm_tall: Sandstone variants of ocean ruins.
  • + *
  • treasure: A treasure chest. Yes, its a whole structure.
  • + *
  • mineshaft: A mineshaft.
  • + *
  • mineshaft_mesa: A Mesa (Badlands) version of a mineshaft.
  • + *
  • village: Plains, oak village.
  • + *
  • village_desert: Desert, sandstone village.
  • + *
  • village_savanna: Savanna, acacia village.
  • + *
  • village_taiga: Taiga, spruce village.
  • + *
+ *

Feature list:

+ *
    + *
  • oak
  • + *
  • oak_large: oak with branches.
  • + *
  • birch
  • + *
  • birch_large: tall variant of birch tree.
  • + *
  • shrub: low bushes that grow in jungles.
  • + *
  • shrub_acacia: low bush but configured with acacia
  • + *
  • shrub_snowy: low bush with white blocks
  • + *
  • jungle: a tree
  • + *
  • spruce_matchstick: tall spruce with minimal leafage.
  • + *
  • dark_oak
  • + *
  • acacia
  • + *
  • spruce
  • + *
  • oak_swamp: oak with more leaves and vines.
  • + *
  • jungle_large: 2x2 jungle tree
  • + *
  • spruce_matchstick_large: 2x2 spruce tree with minimal leafage
  • + *
  • spruce_large: 2x2 spruce tree
  • + *
  • well: desert well
  • + *
  • grass: a few spots of tall grass
  • + *
  • grass_jungle: little bushier grass feature
  • + *
  • fern: a few random ferns
  • + *
  • cactus: random cacti
  • + *
  • dead_bush: a few random dead bushi
  • + *
  • fossils: underground fossils, placement little wonky
  • + *
  • mushroom_brown: large brown mushroom.
  • + *
  • mushroom_red: large red mushroom.
  • + *
  • ice_spike: ice spike. Require snow block below to place.
  • + *
  • glowstone: glowstone cluster. Required netherrack above it.
  • + *
  • melon: a patch of melons
  • + *
  • pumpkin: a patch of pumpkins
  • + *
  • sugarcane
  • + *
  • lilypad
  • + *
  • dungeon: Dungeon. These are hard to place, and fail often.
  • + *
  • iceberg: Iceberg. Generate at sea level.
  • + *
  • iceberg_blue: Blue ice iceberg.
  • + *
  • lake
  • + *
  • lava_lake
  • + *
  • end_island
  • + *
  • chorus: Chorus plant. Require endstone to place.
  • + *
  • sea_grass: a patch of sea grass. Require water.
  • + *
  • sea_grass_river: a variant.
  • + *
  • kelp
  • + *
  • coral_tree, coral_mushroom, coral_claw: various coral types, random color.
  • + *
  • coral: random coral structure. Require water to spawn.
  • + *
  • sea_pickle
  • + *
  • boulder: A rocky, mossy formation from a giant taiga biome. Doesn't update client properly, + * needs relogging.
  • + *
+ *
+ */ + + public void API_AuxiliaryAspects() + { + this.expr.addLazyFunction("sound", -1, (c, t, lv) -> { + CarpetContext cc = (CarpetContext)c; + Identifier soundName = new Identifier(lv.get(0).evalValue(c).getString()); + BlockValue.VectorLocator locator = BlockValue.locateVec(cc, lv, 1); + if (!(Registry.SOUND_EVENT.containsId(soundName))) + throw new InternalExpressionException("No such sound: "+soundName.getPath()); + float volume = 1.0F; + float pitch = 1.0F; + if (lv.size() > 0+locator.offset) + { + volume = (float) NumericValue.asNumber(lv.get(0+locator.offset).evalValue(c)).getDouble(); + if (lv.size() > 1+locator.offset) + { + pitch = (float) NumericValue.asNumber(lv.get(1+locator.offset).evalValue(c)).getDouble(); + } + } + Vec3d vec = locator.vec; + double d0 = Math.pow(volume > 1.0F ? (double)(volume * 16.0F) : 16.0D, 2.0D); + int count = 0; + for (ServerPlayerEntity player : cc.s.getWorld().getPlayers( (p) -> p.squaredDistanceTo(vec) < d0)) + { + count++; + player.networkHandler.sendPacket(new PlaySoundIdS2CPacket(soundName, SoundCategory.PLAYERS, vec, volume, pitch)); + } + int totalPlayed = count; + return (_c, _t) -> new NumericValue(totalPlayed); + }); + + this.expr.addLazyFunction("particle", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext)c; + MinecraftServer ms = cc.s.getMinecraftServer(); + ServerWorld world = cc.s.getWorld(); + BlockValue.VectorLocator locator = BlockValue.locateVec(cc, lv, 1); + String particleName = lv.get(0).evalValue(c).getString(); + int count = 10; + double speed = 0; + float spread = 0.5f; + ServerPlayerEntity player = null; + if (lv.size() > locator.offset) + { + count = (int) NumericValue.asNumber(lv.get(locator.offset).evalValue(c)).getLong(); + if (lv.size() > 1+locator.offset) + { + spread = (float) NumericValue.asNumber(lv.get(1+locator.offset).evalValue(c)).getDouble(); + if (lv.size() > 2+locator.offset) + { + speed = NumericValue.asNumber(lv.get(2 + locator.offset).evalValue(c)).getDouble(); + if (lv.size() > 3 + locator.offset) // should accept entity as well as long as it is player + { + player = ms.getPlayerManager().getPlayer(lv.get(3 + locator.offset).evalValue(c).getString()); + } + } + } + } + ParticleEffect particle = getParticleData(particleName); + Vec3d vec = locator.vec; + if (player == null) + { + for (PlayerEntity p : (world.getPlayers())) + { + world.spawnParticles((ServerPlayerEntity)p, particle, true, vec.x, vec.y, vec.z, count, + spread, spread, spread, speed); + } + } + else + { + world.spawnParticles(player, + particle, true, vec.x, vec.y, vec.z, count, + spread, spread, spread, speed); + } + + return (c_, t_) -> Value.TRUE; + }); + + this.expr.addLazyFunction("particle_line", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext)c; + ServerWorld world = cc.s.getWorld(); + String particleName = lv.get(0).evalValue(c).getString(); + ParticleEffect particle = getParticleData(particleName); + BlockValue.VectorLocator pos1 = BlockValue.locateVec(cc, lv, 1); + BlockValue.VectorLocator pos2 = BlockValue.locateVec(cc, lv, pos1.offset); + int offset = pos2.offset; + double density = (lv.size() > offset)? NumericValue.asNumber(lv.get(offset).evalValue(c)).getDouble():1.0; + if (density <= 0) + { + throw new InternalExpressionException("Particle density should be positive"); + } + Value retval = new NumericValue(drawParticleLine(world, particle, pos1.vec, pos2.vec, density)); + return (c_, t_) -> retval; + }); + + this.expr.addLazyFunction("particle_rect", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext)c; + ServerWorld world = cc.s.getWorld(); + String particleName = lv.get(0).evalValue(c).getString(); + ParticleEffect particle = getParticleData(particleName); + BlockValue.VectorLocator pos1 = BlockValue.locateVec(cc, lv, 1); + BlockValue.VectorLocator pos2 = BlockValue.locateVec(cc, lv, pos1.offset); + int offset = pos2.offset; + double density = 1.0; + if (lv.size() > offset) + { + density = NumericValue.asNumber(lv.get(offset).evalValue(c)).getDouble(); + } + if (density <= 0) + { + throw new InternalExpressionException("Particle density should be positive"); + } + Vec3d a = pos1.vec; + Vec3d b = pos2.vec; + double ax = min(a.x, b.x); + double ay = min(a.y, b.y); + double az = min(a.z, b.z); + double bx = max(a.x, b.x); + double by = max(a.y, b.y); + double bz = max(a.z, b.z); + int pc = 0; + pc += drawParticleLine(world, particle, new Vec3d(ax, ay, az), new Vec3d(ax, by, az), density); + pc += drawParticleLine(world, particle, new Vec3d(ax, by, az), new Vec3d(bx, by, az), density); + pc += drawParticleLine(world, particle, new Vec3d(bx, by, az), new Vec3d(bx, ay, az), density); + pc += drawParticleLine(world, particle, new Vec3d(bx, ay, az), new Vec3d(ax, ay, az), density); + + pc += drawParticleLine(world, particle, new Vec3d(ax, ay, bz), new Vec3d(ax, by, bz), density); + pc += drawParticleLine(world, particle, new Vec3d(ax, by, bz), new Vec3d(bx, by, bz), density); + pc += drawParticleLine(world, particle, new Vec3d(bx, by, bz), new Vec3d(bx, ay, bz), density); + pc += drawParticleLine(world, particle, new Vec3d(bx, ay, bz), new Vec3d(ax, ay, bz), density); + + pc += drawParticleLine(world, particle, new Vec3d(ax, ay, az), new Vec3d(ax, ay, bz), density); + pc += drawParticleLine(world, particle, new Vec3d(ax, by, az), new Vec3d(ax, by, bz), density); + pc += drawParticleLine(world, particle, new Vec3d(bx, by, az), new Vec3d(bx, by, bz), density); + pc += drawParticleLine(world, particle, new Vec3d(bx, ay, az), new Vec3d(bx, ay, bz), density); + int particleCount = pc; + return (c_, t_) -> new NumericValue(particleCount); + }); + + //"overridden" native call that prints to stderr + this.expr.addLazyFunction("print", 1, (c, t, lv) -> + { + ServerCommandSource s = ((CarpetContext)c).s; + if (s.getEntity() instanceof PlayerEntity) + { + Messenger.m((PlayerEntity) s.getEntity(), "w "+ lv.get(0).evalValue(c).getString()); + } + else + { + Messenger.m(s, "w "); + } + return lv.get(0); // pass through for variables + }); + + + this.expr.addLazyFunction("run", 1, (c, t, lv) -> { + BlockPos target = ((CarpetContext)c).origin; + Vec3d posf = new Vec3d((double)target.getX()+0.5D,(double)target.getY(),(double)target.getZ()+0.5D); + ServerCommandSource s = ((CarpetContext)c).s; + Value retval = new NumericValue(s.getMinecraftServer().getCommandManager().execute( + s.withPosition(posf).withSilent(), lv.get(0).evalValue(c).getString())); + return (c_, t_) -> retval; + }); + + this.expr.addLazyFunction("save", 0, (c, t, lv) -> { + ServerCommandSource s = ((CarpetContext)c).s; + s.getMinecraftServer().getPlayerManager().saveAllPlayerData(); + s.getMinecraftServer().save(true,true,true); + s.getWorld().getChunkManager().tick(() -> true); + CarpetSettings.LOG.warn("Saved chunks"); + return (cc, tt) -> Value.TRUE; + }); + + this.expr.addLazyFunction("tick_time", 0, (c, t, lv) -> + { + Value time = new NumericValue(((CarpetContext) c).s.getMinecraftServer().getTicks()); + return (cc, tt) -> time; + }); + + this.expr.addLazyFunction("game_tick", -1, (c, t, lv) -> { + ServerCommandSource s = ((CarpetContext)c).s; + ((MinecraftServerInterface)s.getMinecraftServer()).forceTick( () -> System.nanoTime()- CarpetServer.scriptServer.tickStart<50000000L); + if (lv.size()>0) + { + long ms_total = NumericValue.asNumber(lv.get(0).evalValue(c)).getLong(); + long end_expected = CarpetServer.scriptServer.tickStart+ms_total*1000000L; + long wait = end_expected-System.nanoTime(); + if (wait > 0L) + { + try + { + Thread.sleep(wait/1000000L); + } + catch (InterruptedException ignored) + { + } + } + } + CarpetServer.scriptServer.tickStart = System.nanoTime(); // for the next tick + Thread.yield(); + if(CarpetServer.scriptServer.stopAll) + throw new Expression.ExitStatement(Value.NULL); + return (cc, tt) -> Value.TRUE; + }); + + this.expr.addLazyFunction("current_dimension", 0, (c, t, lv) -> { + ServerCommandSource s = ((CarpetContext)c).s; + Value retval = new StringValue(s.getWorld().dimension.getType().toString().replaceFirst("minecraft:","")); + return (cc, tt) -> retval; + }); + + /*this.expr.addLazyFunction("plop", 4, (c, t, lv) ->{ + BlockValue.LocatorResult locator = BlockValue.fromParams((CarpetContext)c, lv, 0); + Boolean res = FeatureGenerator.spawn(lv.get(locator.offset).evalValue(c).getString(), ((CarpetContext)c).s.getWorld(), locator.block.getPos()); + if (res == null) + return (c_, t_) -> Value.NULL; + return (c_, t_) -> new NumericValue(res); + });*/ + } + + /** + *

.

+ * @param expression expression + * @param source source + * @param origin origin + */ + public CarpetExpression(String expression, ServerCommandSource source, BlockPos origin) + { + this.origin = origin; + this.source = source; + this.expr = new Expression(expression); + + API_BlockManipulation(); + API_EntityManipulation(); + API_IteratingOverAreasOfBlocks(); + API_AuxiliaryAspects(); + } + + /** + *

/script scan, /script fill and /script outline commands

+ *
+ *

These commands can be used to evaluate an expression over an area of blocks. They all need to have specified + * the origin of the analyzed area (which is used as referenced (0,0,0), and two corners of an area to analyzed. If + * you would want that the script block coordinates refer to the actual world coordinates, use origin of 0 0 0, + * or if it doesn't matter, duplicating coordinates of one of the corners is the easiest way.

+ *

These commands, unlike raw /script run are limited by vanilla fill / clone command + * limit of 32k blocks, which can be altered with carpet mod's own /carpet fillLimit command.

+ *

+ *

/script scan origin<x y z> corner<x y z> corner<x y z> expr

+ *

Evaluates expression for each point in the area and returns number of successes (result was positive). Since + * the command by itself doesn't affect the area, the effects would be in side effects.

+ *

/script fill origin<x y z> corner<x y z> corner<x y z> "expr" <block> (? replace <replacement>)

+ *

Think of it as a regular fill command, that sets blocks based on whether a result of the command was successful. + * Note that the expression is in quotes. Thankfully string constants in scarpet use single quotes. Can be used + * to fill complex geometric shapes.

+ *

/script outline origin<x y z> corner<x y z> corner<x y z> "expr" <block> (? replace <replacement>)

+ *

Similar to fill command it evaluates an expression for each block in the area, but in this case setting blocks + * where condition was true and any of the neighbouring blocks were evaluated negatively. This allows to create surface areas, + * like sphere for example, without resorting to various rounding modes and tricks.

+ *

Here is an example of seven ways to draw a sphere of radius of 32 blocks around 0 100 0:

+ *
+     * /script outline 0 100 0 -40 60 -40 40 140 40 "x*x+y*y+z*z <  32*32" white_stained_glass replace air
+     * /script outline 0 100 0 -40 60 -40 40 140 40 "x*x+y*y+z*z <= 32*32" white_stained_glass replace air
+     * /script outline 0 100 0 -40 60 -40 40 140 40 "x*x+y*y+z*z <  32.5*32.5" white_stained_glass replace air
+     * /script fill    0 100 0 -40 60 -40 40 140 40 "floor(sqrt(x*x+y*y+z*z)) == 32" white_stained_glass replace air
+     * /script fill    0 100 0 -40 60 -40 40 140 40 "round(sqrt(x*x+y*y+z*z)) == 32" white_stained_glass replace air
+     * /script fill    0 100 0 -40 60 -40 40 140 40 "ceil(sqrt(x*x+y*y+z*z)) == 32" white_stained_glass replace air
+     * /draw sphere 0 100 0 32 white_stained_glass replace air
+     *
+     * fluffy ball round(sqrt(x*x+y*y+z*z)-rand(abs(y)))==32
+     *
+     * 
+ *

The last method is the one that world edit is using (part of carpet mod). It turns out that the outline method with 32.5 radius, + * fill method with round function and draw command are equivalent

+ *
+ * @param x . + * @param y . + * @param z . + * @return . + */ + public boolean fillAndScanCommand(ScriptHost host, int x, int y, int z) + { + if (CarpetServer.scriptServer.stopAll) + return false; + try + { + Context context = new CarpetContext(host, source, origin). + with("x", (c, t) -> new NumericValue(x - origin.getX()).bindTo("x")). + with("y", (c, t) -> new NumericValue(y - origin.getY()).bindTo("y")). + with("z", (c, t) -> new NumericValue(z - origin.getZ()).bindTo("z")). + with("_", (c, t) -> new BlockValue(null, source.getWorld(), new BlockPos(x, y, z)).bindTo("_")); + Entity e = source.getEntity(); + if (e==null) + { + Value nullPlayer = Value.NULL.reboundedTo("p"); + context.with("p", (cc, tt) -> nullPlayer ); + } + else + { + Value playerValue = new EntityValue(e).bindTo("p"); + context.with("p", (cc, tt) -> playerValue); + } + return this.expr.eval(context).getBoolean(); + } + catch (ExpressionException e) + { + throw new CarpetExpressionException(e.getMessage()); + } + catch (ArithmeticException ae) + { + throw new CarpetExpressionException("math doesn't compute... "+ae.getMessage()); + } + } + + /** + *

/script run command

+ *
+ *

primary way to input commands. The command executes in the context, position, and dimension of the executing + * player, commandblock, etc... The command receives 4 variables, x, y, z + * and p indicating position and the executing entity of the command. + * It is advisable to use /execute in ... at ... as ... run script run ... or similar, to simulate running + * commands in a different scope

+ *
+ * @param pos . + * @return . + */ + public String scriptRunCommand(ScriptHost host, BlockPos pos) + { + if (CarpetServer.scriptServer.stopAll) + return "SCRIPTING PAUSED"; + try + { + Context context = new CarpetContext(host, source, origin). + with("x", (c, t) -> new NumericValue(pos.getX() - origin.getX()).bindTo("x")). + with("y", (c, t) -> new NumericValue(pos.getY() - origin.getY()).bindTo("y")). + with("z", (c, t) -> new NumericValue(pos.getZ() - origin.getZ()).bindTo("z")); + Entity e = source.getEntity(); + if (e==null) + { + Value nullPlayer = Value.NULL.reboundedTo("p"); + context.with("p", (cc, tt) -> nullPlayer ); + } + else + { + Value playerValue = new EntityValue(e).bindTo("p"); + context.with("p", (cc, tt) -> playerValue); + } + return this.expr.eval(context).getString(); + } + catch (ExpressionException e) + { + throw new CarpetExpressionException(e.getMessage()); + } + catch (ArithmeticException ae) + { + throw new CarpetExpressionException("math doesn't compute... "+ae.getMessage()); + } + } + + /** + *

/script invoke / invokepoint / invokearea, /script globals commands

+ *
+ *

invoke family of commands provide convenient way to invoke stored procedures (i.e. functions + * that has been defined previously by any running script. To view current stored procedure set, + * run /script globals, to define a new stored procedure, just run a /script run function(a,b) -> ( ... ) + * command with your procedure once, and to forget a procedure, use undef function: + * /script run undef('function')

+ *

+ *

/script invoke <fun> <args?> ...

+ *

Equivalent of running /script run fun(args, ...), but you get the benefit of getting the tab completion of the + * command name, and lower permission level required to run these (since player is not capable of running any custom code + * in this case, only this that has been executed before by an operator). Arguments will be checked for validity, and + * you can only pass simple values as arguments (strings, numbers, or null value). Use quotes to include + * whitespaces in argument strings.

+ *

Command will check provided arguments with required arguments (count) and fail if not enough or too much arguments + * are provided. Operators defining functions are advised to use descriptive arguments names, as these will be visible + * for invokers and form the base of understanding what each argument does.

+ *

invoke family of commands will tab complete any stored function that does not start with '_', + * it will still allow to run procedures starting with '_' but not suggest them, and ban execution of any + * hidden stored procedures, so ones that start with '__'. In case operator needs to use subroutines + * for convenience and don't want to expose them to the invoke callers, they can use this mechanic.

+ *
+     * /script run example_function(const, phrase, price) -> print(const+' '+phrase+' '+price)
+     * /script invoke example_function pi costs 5
+     * 
+ *

/script invokepoint <fun> <coords x y z> <args?> ...

+ *

It is equivalent to invoke except it assumes that the first three arguments are coordinates, and provides + * coordinates tab completion, with looking at... mechanics for convenience. All other arguments are expected + * at the end

+ *

/script invokearea <fun> <coords x y z> <coords x y z> <args?> ...

+ *

It is equivalent to invoke except it assumes that the first three arguments are one set of ccordinates, + * followed by the second set of coordinates, providing tab completion, with looking at... mechanics for convenience, + * followed by any other required arguments

+ *
+ * @return . + */ + + public static String invokeGlobalFunctionCommand() + { + return "Yeah"; + } +} diff --git a/src/main/java/carpet/script/CarpetScriptServer.java b/src/main/java/carpet/script/CarpetScriptServer.java new file mode 100644 index 00000000..e96b2127 --- /dev/null +++ b/src/main/java/carpet/script/CarpetScriptServer.java @@ -0,0 +1,260 @@ +package carpet.script; + +import carpet.CarpetServer; +import carpet.CarpetSettings; +import carpet.script.bundled.CameraPathModule; +import carpet.script.bundled.FileModule; +import carpet.script.bundled.ModuleInterface; +import carpet.script.exception.CarpetExpressionException; +import carpet.script.exception.ExpressionException; +import carpet.script.value.Value; +import carpet.utils.Messenger; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.util.math.BlockPos; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import static net.minecraft.server.command.CommandManager.argument; +import static net.minecraft.server.command.CommandManager.literal; + +public class CarpetScriptServer +{ + //make static for now, but will change that later: + public ScriptHost globalHost; + public Map modules; + long tickStart; + public boolean stopAll; + + public static List bundledModuleData = new ArrayList(){{ + add(new CameraPathModule()); + }}; + + public CarpetScriptServer() + { + globalHost = createMinecraftScriptHost(""); + modules = new HashMap<>(); + tickStart = 0L; + stopAll = false; + resetErrorSnooper(); + } + + ModuleInterface getModule(String name) + { + for (ModuleInterface moduleData : bundledModuleData) + { + if (moduleData.getName().equalsIgnoreCase(name)) + { + return moduleData; + } + } + + File folder = CarpetServer.minecraft_server.getLevelStorage().resolveFile( + CarpetServer.minecraft_server.getLevelName(), "scripts"); + File[] listOfFiles = folder.listFiles(); + if (listOfFiles == null) + return null; + for (File script : listOfFiles) + { + if (script.getName().equalsIgnoreCase(name+".sc")) + { + return new FileModule(script); + } + } + return null; + } + + public List listAvailableModules() + { + List moduleNames = new ArrayList<>(); + for (ModuleInterface mi: bundledModuleData) + { + if (!modules.containsKey(mi.getName())) + moduleNames.add(mi.getName()); + } + File folder = CarpetServer.minecraft_server.getLevelStorage().resolveFile( + CarpetServer.minecraft_server.getLevelName(), "scripts"); + File[] listOfFiles = folder.listFiles(); + if (listOfFiles == null) + return moduleNames; + for (File script : listOfFiles) + { + if (script.getName().endsWith(".sc")) + { + String name = script.getName().replaceFirst("\\.sc","").toLowerCase(Locale.ROOT); + if (!modules.containsKey(name)) + moduleNames.add(name); + } + } + return moduleNames; + } + + + private static ScriptHost createMinecraftScriptHost(String name) + { + ScriptHost host = new ScriptHost(name); + host.globalVariables.put("_x", (c, t) -> Value.ZERO); + host.globalVariables.put("_y", (c, t) -> Value.ZERO); + host.globalVariables.put("_z", (c, t) -> Value.ZERO); + return host; + } + + public boolean addScriptHost(ServerCommandSource source, String name) + { + name = name.toLowerCase(Locale.ROOT); + ScriptHost newHost = createMinecraftScriptHost(name); + ModuleInterface module = getModule(name); + if (module == null) + { + Messenger.m(source, "r Unable to load the package - not found"); + return false; + } + String code = module.getCode(); + if (code == null) + { + Messenger.m(source, "r Unable to load the package - not found"); + return false; + } + // parse code and convert to expression + // fill functions etc. + // possibly add a command + try + { + setChatErrorSnooper(source); + CarpetExpression ex = new CarpetExpression(module.getCode(), source, new BlockPos(0, 0, 0)); + ex.scriptRunCommand(newHost, new BlockPos(source.getPosition())); + } + catch (CarpetExpressionException e) + { + Messenger.m(source, "r Exception white evaluating expression at "+new BlockPos(source.getPosition())+": "+e.getMessage()); + resetErrorSnooper(); + return false; + } + modules.put(name, newHost); + + addCommand(name); + return true; + } + + + + public void addCommand(String hostName) + { + ScriptHost host = modules.get(hostName); + if (host == null) + { + return; + } + if (!host.globalFunctions.containsKey("__command")) + { + return; + } + LiteralArgumentBuilder command = literal(hostName). + requires((player) -> modules.containsKey(hostName)). + executes( (c) -> + { + Messenger.m(c.getSource(), "gi "+modules.get(hostName).call(c.getSource(),"__command", null, "")); + return 1; + }); + + for (String function : host.getPublicFunctions()) + { + command = command. + then(literal(function). + requires((player) -> modules.containsKey(hostName) && modules.get(hostName).getPublicFunctions().contains(function)). + executes( (c) -> { + Messenger.m( + c.getSource(), + "gi "+modules.get(hostName).call( + c.getSource(), + function, + null, + "" + ) + ); + return 1; + }). + then(argument("args...", StringArgumentType.greedyString()). + executes( (c) -> { + Messenger.m( + c.getSource(), + "gi "+modules.get(hostName).call( + c.getSource(), + function, + null, + StringArgumentType.getString(c, "args...") + ) + ); + return 1; + }))); + } + CarpetServer.minecraft_server.getCommandManager().getDispatcher().register(command); + CarpetSettings.notifyPlayersCommandsChanged(); + } + + public void setChatErrorSnooper(ServerCommandSource source) + { + ExpressionException.errorSnooper = (expr, token, message) -> + { + try + { + source.getPlayer(); + } + catch (CommandSyntaxException e) + { + return null; + } + String[] lines = expr.getCodeString().split("\n"); + + String shebang = message; + + if (lines.length > 1) + { + shebang += " at line "+(token.lineno+1)+", pos "+(token.linepos+1); + } + else + { + shebang += " at pos "+(token.pos+1); + } + if (expr.getName() != null) + { + shebang += " in "+expr.getName()+""; + } + Messenger.m(source, "r "+shebang); + + if (lines.length > 1 && token.lineno > 0) + { + Messenger.m(source, "l "+lines[token.lineno-1]); + } + Messenger.m(source, "l "+lines[token.lineno].substring(0, token.linepos), "r HERE>> ", "l "+ + lines[token.lineno].substring(token.linepos)); + + if (lines.length > 1 && token.lineno < lines.length-1) + { + Messenger.m(source, "l "+lines[token.lineno+1]); + } + return new ArrayList<>(); + }; + } + public void resetErrorSnooper() + { + ExpressionException.errorSnooper=null; + } + + public boolean removeScriptHost(String name) + { + name = name.toLowerCase(Locale.ROOT); + if (!modules.containsKey(name)) + return false; + // stop all events associated with name + modules.remove(name); + return true; + } +} diff --git a/src/main/java/carpet/script/Context.java b/src/main/java/carpet/script/Context.java new file mode 100644 index 00000000..6d39a603 --- /dev/null +++ b/src/main/java/carpet/script/Context.java @@ -0,0 +1,90 @@ +package carpet.script; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +public class Context +{ + static final int NONE = 0; + static final int VOID = 1; + static final int BOOLEAN = 2; + static final int NUMBER = 3; + static final int STRING = 4; + static final int LIST = 5; + static final int ITERATOR = 6; + static final int SIGNATURE = 7; + static final int LOCALIZATION = 8; + + private Map variables = new HashMap<>(); + + protected ScriptHost host; + + Context(ScriptHost host) + { + this.host = host; + } + + + LazyValue getVariable(String name) + { + if (variables.containsKey(name)) + { + return variables.get(name); + } + return host.globalVariables.get(name); + } + + void setVariable(String name, LazyValue lv) + { + if (name.startsWith("global_")) + { + host.globalVariables.put(name, lv); + return; + } + variables.put(name, lv); + } + + + boolean isAVariable(String name) + { + return variables.containsKey(name) || host.globalVariables.containsKey(name); + } + + + void delVariable(String variable) + { + if (variable.startsWith("global_")) + { + host.globalVariables.remove(variable); + return; + } + variables.remove(variable); + } + void clearAll(String variable) + { + if (variable.startsWith("global_")) + { + host.globalVariables.remove(variable); + return; + } + variables.remove(variable); + } + + public Context with(String variable, LazyValue lv) + { + variables.put(variable, lv); + return this; + } + + public Set getAllVariableNames() + { + return variables.keySet(); + } + + public Context recreate() + { + return new Context(this.host); + } +} diff --git a/src/main/java/carpet/script/Expression.java b/src/main/java/carpet/script/Expression.java new file mode 100644 index 00000000..b46bb963 --- /dev/null +++ b/src/main/java/carpet/script/Expression.java @@ -0,0 +1,2926 @@ +package carpet.script; + +import carpet.script.Fluff.AbstractFunction; +import carpet.script.Fluff.AbstractLazyFunction; +import carpet.script.Fluff.AbstractLazyOperator; +import carpet.script.Fluff.AbstractOperator; +import carpet.script.Fluff.AbstractUnaryOperator; +import carpet.script.Fluff.ILazyFunction; +import carpet.script.Fluff.ILazyOperator; +import carpet.script.Fluff.QuadFunction; +import carpet.script.Fluff.SexFunction; +import carpet.script.Fluff.TriFunction; +import carpet.script.exception.ExpressionException; +import carpet.script.exception.InternalExpressionException; +import carpet.script.value.FunctionSignatureValue; +import carpet.script.value.GlobalValue; +import carpet.script.value.LazyListValue; +import carpet.script.value.ListValue; +import carpet.script.value.NumericValue; +import carpet.script.value.StringValue; +import carpet.script.value.Value; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.IllegalFormatException; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Random; +import java.util.Stack; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static java.lang.Math.abs; +import static java.lang.Math.max; +import static java.lang.Math.min; + + +/** + *

Fundamental components of scarpet programming language.

+ * + *

Scarpet (a.k.a. Carpet Script, or Script for Carpet) is a programming language designed to provide + * the ability to write custom programs to run within Minecraft and + * interact with the world.

+ * + *

The project was initially built based on the EvalEx project, + * however it now diverged so far away from the original implementation, + * it would be hard to tell it without this mention. + * EvalEx is a handy expression evaluator for Java, that + * allows to evaluate simple mathematical and boolean expressions. + * For more information, see: + * EvalEx GitHub + * repository

+ * + *

This specification is divided into two sections: this one is agnostic + * to any Minecraft related features and could function on its own, and {@see carpet.script.CarpetExpression} for + * Minecraft specific routines and world manipulation functions.

+ * + *

Synopsis

+ * + *
+ * script run print('Hello World!')
+ * 
+ *

or an overly complex example:

+ *
+ * /script run
+ * $    block_check(x1, y1, z1, x2, y2, z2, block_to_check) ->
+ * $    (
+ * $        l(minx, maxx) = sort(l(x1, x2));
+ * $        l(miny, maxy) = sort(l(y1, y2));
+ * $        l(minz, maxz) = sort(l(z1, z2));
+ * $        'Need to compute the size of the area of course';
+ * $        'Cause this language doesn\'t support comments';
+ * $        xsize = maxx - minx + 1;
+ * $        ysize = maxy - miny + 1;
+ * $        zsize = maxz - minz + 1;
+ * $        total_count = 0;
+ * $        loop(xsize,
+ * $            xx = minx + _ ;
+ * $            loop(ysize,
+ * $                yy = miny + _ ;
+ * $                loop(zsize,
+ * $                    zz = minz + _ ;
+ * $                    if ( block(xx,yy,zz) == block_to_check,
+ * $                        total_count += ceil(rand(1))
+ * $                    )
+ * $                )
+ * $            )
+ * $        );
+ * $        total_count
+ * $    );
+ * $    check_area_around_closest_player_for_block(block_to_check) ->
+ * $    (
+ * $        closest_player = players();
+ * $        l(posx, posy, posz) = query(closest_player, 'pos');
+ * $        total_count = block_check( posx-8,1,posz-8, posx+8,17,posz+8, block_to_check);
+ * $        print('There is '+total_count+' of '+block_to_check+' around you')
+ * $    )
+ *
+ * /script invoke check_area_around_closest_player_for_block 'diamond_ore'
+ *
+ * 
+ *

or simply

+ *
+ * script run print('There is'+for(rect(x,9,z,8,8,8), _ == 'diamond_ore')+' diamond ore around you')
+ * 
+ *

It definitely pays to check what higher level scarpet functions have to offer.

+ *

Programs

+ *

+ * You can think of an program like a mathematical expression, like + * "2.4*sin(45)/(2-4)" or "sin(y)>0 & max(z, 3)>3" + * Writing a program, is like writing a 2+3, just a bit longer

+ * + *

Basic language components

+ *

Programs consist of constants, like 2, 3.14, pi, or 'foo', + * operators like +, /, ->, variables which you can define, like foo + * or special ones that will be defined for you, like _x, or _ , which I specific to + * a each built in function, and functions with name, and arguments in the form of f(a,b,c), where + * f is the function name, and a, b, c are the arguments which can be any other expression. + * And that's all the parts of the language, so all in all - sounds quite simple.

+ * + *

Code flow

+ *

+ * Like any other proper programming language, scarpet needs brackets, basically to identify + * where stuff begins and where it ends. In the languages that uses much more complicated constructs, like Java, + * they tend to use all sort of them, round ones to indicate function calls, curly to indicate section of code, + * square to access lists, pointy for generic types etc... I mean - there is no etc, cause they have exhausted + * all the bracket options... + *

+ *

Scarpet is different, since it runs everything based on functions (although its not per se a functional language like lisp) + * only needs the round brackets for everything, and it is up to the programmer to organize its code so its readable, + * as adding more brackets does not have any effect on the performance of the programs as they are compiled before they are executed. + * Look at the following example usage of if() function: + *

+ *
+ * if(x<y+6,set(x,8+y,z,'air');plop(x,top('surface',x,z),z,'birch'),sin(query(player(),'yaw'))>0.5,plop(0,0,0,'boulder'),particle('fire',x,y,z))
+ * 
+ *

Would you prefer to read

+ *
+ * if(   x<y+6,
+ *            set(x,8+y,z,'air');
+ *            plop(x,top('surface',x,z),z,'birch'),
+ *       sin(query(player(),'yaw'))>0.5,
+ *            plop(0,0,0,'boulder'),
+ *       particle('fire',x,y,z)
+ * )
+ * 
+ *

Or rather:

+ *
+ * if
+ * (
+ *     x<y+6,
+ *     (
+ *         set(x,8+y,z,'air');
+ *         plop(x,top('surface',x,z),z,'birch')
+ *     ),
+ *
+ *     sin(query(player(),'yaw'))>0.5,
+ *     (
+ *         plop(0,0,0,'boulder')
+ *     ),
+ *
+ *     particle('fire',x,y,z)
+ * )
+ * 
+ *

Whichever style you prefer it doesn't matter. It typically depends on the situation and the complexity of the + * subcomponents. No matter how many whitespaces and extra brackets you add - the code will evaluate to exactly the + * same expression, and will run exactly the same, so make sure your programs are nice and clean so others don't + * have problems with them

+ * + *

Functions and scoping

+ *

+ * Users can define functions in the form fun(args....) -> expression and they are compiled and saved + * for further execution in this but also subsequent calls of /script command. This means that once defined functions + * are saved with the world for further use. There are two types of variables, + * global - which are shared anywhere in the code, and those are all which name starts with 'global_', and + * local variables which is everything else and those are only visible inside each function. + * This also means that all the parameters in functions are passed 'by value', not 'by reference'. + *

+ * + *

Outer variables

+ *

Functions can still 'borrow' variables from the outer scope, + * by adding them to the function signature wrapped around built-in function outer. What this does is + * it borrows the reference to that variable from the outer scope to be used inside the function and any modifications to that outer + * variable would result in changes of that value in the outer scope as well. Its like passing the parameters by reference, + * except the calling function itself decides what variables its borrowing and what its name. This can be used to + * return more than one result from a function call, although its a very ugly way of doing it - + * I would still recommend returning a list of values instead. + * Variables from outer scopes have a similar behaviour to, for example, nonlocal variables from python.

+ * + * + *

Line indicators

+ *

Since the maximum command that can be input to the chat is limited in length, you will be probably inserting your + * programs by pasting them to command blocks, however pasting to command blocks will remove some whitespaces and squish + * your newlines making the code not readable. If you are pasting a program that is perfect and will never cause an error, + * I salute you, but for the most part it is quite likely that your program might break, either at compile time, when + * its initially analyzed, or at execute time, when you suddenly attempt to divide something by zero. In these cases + * you would want to get a meaningful error message, but for that you would need to indicate for the compiler where + * did you put these new lines, since command block would squish them. For that, place at the beginning + * of the line to let the copiler know where are you. This makes so that $ is the only character that is + * illegal in programs, since it will be replaced with new lines. As far as I know, $ is not used + * anywhere inside Minecraft identifiers, so this shoudn't hinder the abilities of your programs.

+ *

Consider the following program executed as command block command:

+ *
+ * /script run
+ * run_program() -> (
+ *   loop( 10,
+ *     foo = floor(rand(10));
+ *     check_not_zero(foo);
+ *     print(_+' - foo: '+foo);
+ *     print('  reciprocal: '+  _/foo )
+ *   )
+ * );
+ * check_not_zero(foo) -> (
+ *   if (foo==0, foo = 1)
+ * )
+ * 
+ *

Lets say that the intention was to check if the bar is zero and prevent division by zero in print, + * but because the foo is passed as a variable, it never changes the original foo value. + * Because of the inevitable division by zero, we get the following message: + *

+ *
+ * Your math is wrong, Incorrect number format for Infinity at pos 112
+ * run_program() -> (  loop( 10,    foo = floor(rand(10));    check_not_zero(foo);
+ * print(_+' - foo: '+foo);     HERE>> print('  reciprocal: '+  _/foo )  ));
+ * check_not_zero(foo) -> (  if (foo==0, foo = 1))
+ * 
+ * + * As we can see, we got our problem where the result of the mathematical operation was not a number (Infinity, so not a number), + * however by pasting our program + * into the command made it squish the newlines so while it is clear where the error happened and we still can track the error down, + * the position of the error (112) is not very helpful and wouldn't be useful if the program gets significantly longer. + * To combat this issue we can precede every line of the script with dollar signs $: + *
+ * /script run
+ * $run_program() -> (
+ * $  loop( 10,
+ * $    foo = floor(rand(10));
+ * $    check_not_zero(foo);
+ * $    print(_+' - foo: '+foo);
+ * $    print('  reciprocal: '+  _/foo )
+ * $  )
+ * $);
+ * $check_not_zero(foo) -> (
+ * $  if (foo==0, foo = 1)
+ * $)
+ * 
+ * + *

Then we get the following error message

+ * + *
+ * Your math is wrong, Incorrect number format for Infinity at line 7, pos 5
+ *     print(_+' - foo: '+foo);
+ *      HERE>> print('  reciprocal: '+  _/foo )
+ *   )
+ * 
+ * + * + *

As we can note not only we get much more concise snippet, but also information about the line + * number and position, so means its way easier to locate the potential problems problem

+ * + *

Obviously that's not the way we intended this program to work. To get it foo modified via + * a function call, we would either return it as a result and assign it to the new variable: + *

+ *
+ *     foo = check_not_zero(foo);
+ *     ...
+ *     check_not_zero(foo) -> if(foo == 0, 1, foo)
+ * 
+ *

.. or convert it to a global variable, which in this case passing as an argument is not required

+ *
+ *     global_foo = floor(rand(10));
+ *     check_foo_not_zero();
+ *     ...
+ *     check_foo_not_zero() -> if(global_foo == 0, global_foo = 1)
+ * 
+ *

.. or scope foo from the outer function - in this case the inner function has to determine what it is accessing

+ *
+ *     foo = floor(rand(10));
+ *     check_foo_not_zero();
+ *     ...
+ *     check_foo_not_zero(outer(foo)) -> if(foo == 0, foo = 1)
+ * 
+ *

outer scope can only be used in + * function signatures to indicate outer variables. They are not arguments, but still you would want to use + * locally without affecting other uses of foo in your program. + *

+ *

For the most part - passing arguments as values, and using returned values . + * The main usecase of Scarpet would rather be simpler scripts, default scope for all variables is global, unless variable + * is declared with local scope explicitly. + *

+ */ + +public class Expression implements Cloneable +{ + private static final Map precedence = new HashMap() {{ + put("unary+-!", 60); + put("exponent^", 40); + put("multiplication*/%", 30); + put("addition+-", 20); + put("compare>=><=<", 10); + put("equal==!=", 7); + put("and&&", 5); + put("or||", 4); + put("assign=<>", 3); + put("def->", 2); + put("nextop;", 1); + }}; + protected static final Random randomizer = new Random(); + + static final Value PI = new NumericValue( + "3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679"); + + static final Value euler = new NumericValue( + "2.71828182845904523536028747135266249775724709369995957496696762772407663"); + + // %[argument_index$][flags][width][.precision][t]conversion + private static Pattern formatPattern = Pattern.compile("%(\\d+\\$)?([-#+ 0,(\\<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])"); + + /** The current infix expression */ + private String expression; + String getCodeString() {return expression;} + + private String name; + String getName() {return name;} + Expression withName(String alias) + { + name = alias; + return this; + } + + /** Cached AST (Abstract Syntax Tree) (root) of the expression */ + private LazyValue ast = null; + + /** script specific operatos and built-in functions */ + private Map operators = new HashMap<>(); + boolean isAnOperator(String opname) { return operators.containsKey(opname) || operators.containsKey(opname+"u");} + + private Map functions = new HashMap<>(); + + //static final Map globalFunctions = new HashMap<>(); + + //static final Map globalVariables = new HashMap<>(); + + /*static + { + setGlobals(); + } + static void setGlobals() + { + globalVariables.put("euler", (c, t) -> euler); + globalVariables.put("pi", (c, t) -> PI); + globalVariables.put("null", (c, t) -> Value.NULL); + globalVariables.put("true", (c, t) -> Value.TRUE); + globalVariables.put("false", (c, t) -> Value.FALSE); + + //special variables for second order functions so we don't need to check them all the time + globalVariables.put("_", (c, t) -> Value.ZERO); + globalVariables.put("_i", (c, t) -> Value.ZERO); + globalVariables.put("_a", (c, t) -> Value.ZERO); + } + */ + + @Override + protected Expression clone() throws CloneNotSupportedException + { + // very very shallow copy for global functions to grab the context for error msgs + Expression copy = (Expression) super.clone(); + copy.expression = this.expression; + copy.name = this.name; + return copy; + } + + /* Exception thrown to terminate execution mid expression (aka return statement) */ + static class ExitStatement extends RuntimeException + { + Value retval; + ExitStatement(Value value) + { + retval = value; + } + } + static class ReturnStatement extends ExitStatement + { + + ReturnStatement(Value value) + { + super(value); + } + } + static class ThrowStatement extends ExitStatement + { + ThrowStatement(Value value) + { + super(value); + } + } + + + static List getExpressionSnippet(Tokenizer.Token token, Expression expr) + { + String code = expr.getCodeString(); + List output = new ArrayList<>(); + for (String line: getExpressionSnippetLeftContext(token, code, 1)) + { + output.add(line); + } + List context = getExpressionSnippetContext(token, code); + output.add(context.get(0)+" HERE>> "+context.get(1)); + for (String line: getExpressionSnippetRightContext(token, code, 1)) + { + output.add(line); + } + return output; + } + + private static List getExpressionSnippetLeftContext(Tokenizer.Token token, String expr, int contextsize) + { + List output = new ArrayList<>(); + String[] lines = expr.split("\n"); + if (lines.length == 1) return output; + for (int lno=token.lineno-1; lno >=0 && output.size() < contextsize; lno-- ) + { + output.add(lines[lno]); + } + Collections.reverse(output); + return output; + } + + private static List getExpressionSnippetContext(Tokenizer.Token token, String expr) + { + List output = new ArrayList<>(); + String[] lines = expr.split("\n"); + if (lines.length > 1) + { + output.add(lines[token.lineno].substring(0, token.linepos)); + output.add(lines[token.lineno].substring(token.linepos)); + } + else + { + output.add( expr.substring(max(0, token.pos-40), token.pos)); + output.add( expr.substring(token.pos, min(token.pos+1+40, expr.length()))); + } + return output; + } + + private static List getExpressionSnippetRightContext(Tokenizer.Token token, String expr, int contextsize) + { + List output = new ArrayList<>(); + String[] lines = expr.split("\n"); + if (lines.length == 1) { return output; } + for (int lno=token.lineno+1; lno < lines.length && output.size() < contextsize; lno++ ) + { + output.add(lines[lno]); + } + return output; + } + + + private void addLazyUnaryOperator(String surface, int precedence, boolean leftAssoc, + TriFunction lazyfun) + { + operators.put(surface+"u", new AbstractLazyOperator(precedence, leftAssoc) + { + @Override + public LazyValue lazyEval(Context c, Integer t, Expression e, Tokenizer.Token token, LazyValue v, LazyValue v2) + { + try + { + if (v2 != null) + { + throw new ExpressionException(e, token, "Did not expect a second parameter for unary operator"); + } + Value.assertNotNull(v); + return lazyfun.apply(c, t, v); + } + catch (InternalExpressionException exc) + { + throw new ExpressionException(e, token, exc.getMessage()); + } + catch (ArithmeticException exc) + { + throw new ExpressionException(e, token, "Your math is wrong, "+exc.getMessage()); + } + } + }); + } + + + private void addLazyBinaryOperatorWithDelegation(String surface, int precedence, boolean leftAssoc, + SexFunction lazyfun) + { + operators.put(surface, new AbstractLazyOperator(precedence, leftAssoc) + { + @Override + public LazyValue lazyEval(Context c, Integer type, Expression e, Tokenizer.Token t, LazyValue v1, LazyValue v2) + { + try + { + Value.assertNotNull(v1, v2); + return lazyfun.apply(c, type, e, t, v1, v2); + } + catch (InternalExpressionException exc) // might not actually throw it + { + throw new ExpressionException(e, t, exc.getMessage()); + } + catch (ArithmeticException exc) + { + throw new ExpressionException(e, t, "Your math is wrong, "+exc.getMessage()); + } + } + }); + } + + private void addLazyBinaryOperator(String surface, int precedence, boolean leftAssoc, + QuadFunction lazyfun) + { + operators.put(surface, new AbstractLazyOperator(precedence, leftAssoc) + { + @Override + public LazyValue lazyEval(Context c, Integer t, Expression e, Tokenizer.Token token, LazyValue v1, LazyValue v2) + { + try + { + Value.assertNotNull(v1, v2); + return lazyfun.apply(c, t, v1, v2); + } + catch (InternalExpressionException exc) + { + throw new ExpressionException(e, token, exc.getMessage()); + } + catch (ArithmeticException exc) + { + throw new ExpressionException(e, token, "Your math is wrong, "+exc.getMessage()); + } + } + }); + } + + private void addUnaryOperator(String surface, boolean leftAssoc, Function fun) + { + operators.put(surface+"u", new AbstractUnaryOperator(precedence.get("unary+-!"), leftAssoc) + { + @Override + public Value evalUnary(Value v1) + { + return fun.apply(Value.assertNotNull(v1)); + } + }); + } + + private void addBinaryOperator(String surface, int precedence, boolean leftAssoc, BiFunction fun) + { + operators.put(surface, new AbstractOperator(precedence, leftAssoc) + { + @Override + public Value eval(Value v1, Value v2) + { + Value.assertNotNull(v1, v2); + return fun.apply(v1, v2); + } + }); + } + + + private void addUnaryFunction(String name, Function fun) + { + name = name.toLowerCase(Locale.ROOT); + functions.put(name, new AbstractFunction(1) + { + @Override + public Value eval(List parameters) + { + return fun.apply(Value.assertNotNull(parameters.get(0))); + } + }); + } + + void addBinaryFunction(String name, BiFunction fun) + { + name = name.toLowerCase(Locale.ROOT); + functions.put(name, new AbstractFunction(2) + { + @Override + public Value eval(List parameters) + { + Value v1 = parameters.get(0); + Value v2 = parameters.get(1); + Value.assertNotNull(v1, v2); + return fun.apply(v1, v2); + } + }); + } + + private void addFunction(String name, Function, Value> fun) + { + name = name.toLowerCase(Locale.ROOT); + functions.put(name, new AbstractFunction(-1) + { + @Override + public Value eval(List parameters) + { + for (Value v: parameters) + Value.assertNotNull(v); + return fun.apply(parameters); + } + }); + } + + private void addMathematicalUnaryFunction(String name, Function fun) + { + addUnaryFunction(name, (v) -> new NumericValue(fun.apply(NumericValue.asNumber(v).getDouble()))); + } + + private void addMathematicalBinaryFunction(String name, BiFunction fun) + { + addBinaryFunction(name, (w, v) -> + new NumericValue(fun.apply(NumericValue.asNumber(w).getDouble(), NumericValue.asNumber(v).getDouble()))); + } + + + void addLazyFunction(String name, int num_params, TriFunction, LazyValue> fun) + { + name = name.toLowerCase(Locale.ROOT); + functions.put(name, new AbstractLazyFunction(num_params) + { + @Override + public LazyValue lazyEval(Context c, Integer i, Expression e, Tokenizer.Token t, List lazyParams) + { + try + { + return fun.apply(c, i, lazyParams); + } + catch (InternalExpressionException exc) + { + throw new ExpressionException(e, t, exc.getMessage()); + } + catch (ArithmeticException exc) + { + throw new ExpressionException(e, t, "Your math is wrong, "+exc.getMessage()); + } + } + }); + } + private void addContextFunction(Context context, String name, Expression expr, Tokenizer.Token token, List arguments, List globals, LazyValue code) + { + name = name.toLowerCase(Locale.ROOT); + if (functions.containsKey(name)) + throw new ExpressionException(expr, token, "Function "+name+" would mask a built-in function"); + Expression function_context; + try + { + function_context = expr.clone(); + function_context.name = name; + } + catch (CloneNotSupportedException e) + { + throw new ExpressionException(expr, token, "Problems in allocating global function "+name); + } + + context.host.globalFunctions.put(name, new UserDefinedFunction(arguments, function_context, token) + { + @Override + public LazyValue lazyEval(Context c, Integer type, Expression e, Tokenizer.Token t, List lazyParams) + { + if (arguments.size() != lazyParams.size()) // something that might be subject to change in the future + { + throw new ExpressionException(e, t, + "Incorrect number of arguments for function "+name+ + ". Should be "+arguments.size()+", not "+lazyParams.size()+" like "+arguments + ); + } + Context newFrame = c.recreate(); + + for (String global : globals) + { + LazyValue lv = c.getVariable(global); + if (lv == null) + { + Value zero = Value.ZERO.reboundedTo(global); + newFrame.setVariable(global, (cc, tt) -> zero); + } + else + { + newFrame.setVariable(global, lv); + } + } + for (int i=0; i val); + } + Value retVal; + boolean rethrow = false; + try + { + retVal = code.evalValue(newFrame, type); // todo not sure if we need to propagete type / consider boolean context in defined functions - answer seems ye + } + catch (ReturnStatement returnStatement) + { + retVal = returnStatement.retval; + } + catch (ThrowStatement throwStatement) + { + retVal = throwStatement.retval; + rethrow = true; + } + catch (InternalExpressionException exc) + { + throw new ExpressionException(function_context, t, exc.getMessage()); + } + catch (ArithmeticException exc) + { + throw new ExpressionException(function_context, t, "Your math is wrong, "+exc.getMessage()); + } + for (String global: globals) + { + LazyValue lv = newFrame.getVariable(global); + if (lv != null) + { + c.setVariable(global, lv); + } + } + if (rethrow) + { + throw new ThrowStatement(retVal); + } + Value otherRetVal = retVal; + return (cc, tt) -> otherRetVal; + } + }); + } + + /** + *

Variables and Constants

+ *
+ *

scarpet provides a number of constants that can be used literally in scripts

+ *
    + *
  • null: nothing, zilch, not even false
  • + *
  • true: pure true, or just 1 (one)
  • + *
  • false: false truth, or true falsth, 0 (zero) actually
  • + *
  • pi: for the fans of perimeters, its a perimeter of an apple pi of diameter 1. About 3.14
  • + *
  • euler: clever guy. Derivative of its exponent is goto 1. About 2.72
  • + *
+ *

Apart from that, there is a bunch of system variables, that start with _ that are set by + * scarpet built-ins, like _, typically each consecutive value in loops, + * _i indicating iteration, or _a like an accumulator for reduce + * function. Certain calls to Minecraft specific calls would also set _x, + * _y, _z, indicating block positions. All variables starting with + * _ are read-only, and cannot be declared and modified in client code.

+ * + *

Literals

+ *

scarpet accepts numeric and string liters constants. + * Numbers look like 1, 2.5, -3e-7, 0xff, and are internally represented as Java's double + * but scarpet will try to trim trailing zeros as much as possible so if you need to use them as intergers, + * you can. Strings using single quoting, for multiple reasons, but primarily to allow for easier use of strings inside + * doubly quoted command arguments (when passing a script as a parameter of /script fill for example, + * or when typing in jsons inside scarpet to feed back into a /data merge command for example. Strings also + * use backslashes \ for quoting special characters, in both plain strings and regular expressions

+ *
+     * 'foo'
+     * print('This doesn\'t work')
+     * nbt ~ '\\.foo'   // matching '.' as a '.', not 'any character match'
+     * 
+ *
+ */ + + public void VariablesAndConstants() // public just to get the Javadocs right + { + // all declared as global variables to save on switching scope cost + } + + /** + *

User-defined functions and program control flow

+ *
+ *

Writing programs with more than 1 line

+ *

Operator ;

+ *

To effectively write programs that have more than one line, a programmer needs way to specify a sequence of + * commands that execute one after another. In scarpet this can be achieved with ;. Its an operator, + * and by separating statements with semicolons. And since whitespaces and $ signs are all treats as + * whitespaces, how you layout your code doesn't matter, as long as it is readable to everyone involved. The + * usefulness of preceding all the lines of the script with $ is explained in the preamble

+ *
+     * expr;
+     * expr;
+     * expr;
+     * expr
+     * 
+ *

Notice that the last expression is not followed by a semicolon. Since instruction separation is functional + * in scarpet, and not barely an instruction delimiter, + * terminating the code with a dangling operator wouldn't be valid.

+ *

In general expr; expr; expr; expr is equivalent to + * (((expr ; expr) ; expr) ; expr). In case the programmer forgets that it should be warned with a + * helpful error at compile time.

+ *

Result of the evaluated expression is the same as the result of the second expression, but first expression is + * also evaluated for sideeffects

+ *
+     * expr1 ; expr2 => expr2  // with expr1 as a sideeffect
+     * 
+ *

All defined functions are compiled, stored persistently, and available globally - + * accessible to all other scripts. Functions can only be undefined via call to undef('fun'), which + * would erase global entry for function fun. Since all variables have local scope inside each function, + * one way to share large objects is via global variables + *

+ *

Global variables

+ *

Any variable that is used with a name that starts with 'global_' will be stored and accessible globally, + * not, inside current scope. It will also persist across scripts, so if a procedure needs to use its own construct, it needs to + * define it, or initialize it explicitly, or undefine it via undef

+ *
+     * a() -> global_list+=1; global_list = l(1,2,3); a(); a(); global_list  // => [1,2,3,1,1]
+     * 
+ *

Operator ->

+ *

To organize code better than a flat sequence of operations, one can define functions. Definition is correct if + * has the following form

+ *
+     *     fun(args, ...) -> expr
+     * 
+ *

Where fun(args, ...) is a function signature indicating function name, number of arguments, + * and their names, and expr is an expression (can be complex) that is evaluated when fun is called. + * Names in the signature don't need to be used anywhere else, other occurrences of these names + * will be masked in this function scope. + * Function call creates new scope for variables inside expr, so all non-global variables are not + * visible from the caller scope. All parameters are passed by value to the new scope, including lists

+ *
+     * a(lst) -> lst+=1; list = l(1,2,3); a(list); a(list); list  // => [1,2,3]
+     * 
+ *

In case the inner function wants to operate and modify larger objects, lists from the outer + * scope, but not global, it needs to use outer function in function signature

+ *

outer(arg)

+ *

outer function can only be used in the function signature, and it will + * cause an error everywhere else. It borrows the reference to that variable from the outer scope and allows + * its modification in the inner scope. Any modification of outer variable will result in change of them in + * the outer function. In case the variable was not set yet in the outer scope - it will be created. This construct + * is similar to nonlocal scoping from python

+ *
+     * a(outer(list)) -> list+=1; list = l(1,2,3); a(); a(); list  // => [1,2,3,1,1]
+     * 
+ *

The return value of a function is the value of the last expression. This as the same effect as using outer + * or global lists, but is more expensive

+ *
+     * a(lst) -> lst+=1; list = l(1,2,3); list=a(list); list=a(list); list  // => [1,2,3,1,1]
+     * 
+ *

Ability to combine more statements into one expression, with functions, passing parameters, and global and outer + * scoping allow to organize even larger scripts

+ *

Control flow

+ *

return(expr)

+ *

Sometimes its convenient to break the organized control flow, or it is not practical to pass + * the final result value of a function to the last statement, in this case a return statement can be used

+ *
+     * def() -> (
+     *  expr1;
+     *  expr2;
+     *  return(expr3); // function terminates returning expr3
+     *  expr4;     // skipped
+     *  expr5      // skipped
+     * )
+     * 
+ *

In general its cheaper to leave the last expression as a return value, rather than calling returns everywhere, + * but it would often lead to a messy code.

+ *

exit(expr)

+ *

It terminates entire program passing expr as the result of the program execution.

+ *

try(expr, catch_expr(_)) ... throw(value)

+ *

try function evaluates expression, and continues further unless throw function is called + * anywhere inside expr. In that case the catch_expr is evaluates with _ set + * to the argument throw was called with. This mechanism allows to terminate large portion of a convoluted + * call stack and continue program execution. There is only one level of exceptions currently in carpet, so if the inner + * function also defines the try catchment area, it will received the exception first, but it can technically + * rethrow the value its getting for the outer scope. Unhandled throw acts like an exit statement.

+ *

if(cond, expr, cond?, expr?, ..., default?)

+ *

If statement is a function that takes a number of conditions that are evaluated one after another and if + * any of them turns out true, its expr gets returned, otherwise, if all conditions fail, the return value is + * default expression, or null if default is skipped

+ *

if function is equivalent to if (cond) expr; else if (cond) expr; else default; + * from Java, just in a functional form

+ *
+ */ + + public void UserDefinedFunctionsAndControlFlow() // public just to get the javadoc right + { + // artificial construct to handle user defined functions and function definitions + addLazyFunction(".",-1, (c, t, lv) -> { // adjust based on c + String name = lv.get(lv.size()-1).evalValue(c).getString(); + //lv.remove(lv.size()-1); // aint gonna cut it // maybe it will because of the eager eval changes + if (t != Context.SIGNATURE) // just call the function + { + if (!c.host.globalFunctions.containsKey(name)) + { + throw new InternalExpressionException("Function "+name+" is not defined yet"); + } + List lvargs = new ArrayList<>(lv.size()-1); + for (int i=0; i< lv.size()-1; i++) + { + lvargs.add(lv.get(i)); + } + UserDefinedFunction acf = c.host.globalFunctions.get(name); + Value retval = acf.lazyEval(c, t, acf.expression, acf.token, lvargs).evalValue(c); + return (cc, tt) -> retval; ///!!!! dono might need to store expr and token in statics? (e? t?) + } + + // gimme signature + List args = new ArrayList<>(); + List globals = new ArrayList<>(); + for (int i = 0; i < lv.size() - 1; i++) + { + Value v = lv.get(i).evalValue(c, Context.LOCALIZATION); + if (!v.isBound()) + { + throw new InternalExpressionException("Only variables can be used in function signature, not " + v.getString()); + } + if (v instanceof GlobalValue) + { + globals.add(v.boundVariable); + } + else + { + args.add(v.boundVariable); + } + } + Value retval = new FunctionSignatureValue(name, args, globals); + return (cc, tt) -> retval; + }); + addLazyFunction("outer", 1, (c, t, lv) -> { + if (t != Context.LOCALIZATION) + throw new InternalExpressionException("outer scoping of variables is only possible in function signatures"); + return (cc, tt) -> new GlobalValue(lv.get(0).evalValue(c)); + }); + + addLazyBinaryOperator(";",precedence.get("nextop;"), true, (c, t, lv1, lv2) -> + { + Value v1 = lv1.evalValue(c, Context.VOID); + return lv2; + }); + + //assigns const procedure to the lhs, returning its previous value + addLazyBinaryOperatorWithDelegation("->", precedence.get("def->"), false, (c, type, e, t, lv1, lv2) -> + { + Value v1 = lv1.evalValue(c, Context.SIGNATURE); + if (v1 instanceof FunctionSignatureValue) + { + FunctionSignatureValue sign = (FunctionSignatureValue) v1; + addContextFunction(c, sign.getName(), e, t, sign.getArgs(), sign.getGlobals(), lv2); + } + else + { + v1.assertAssignable(); + c.setVariable(v1.getVariable(), lv2); + } + return (cc, tt) -> new StringValue("OK"); + }); + + addUnaryFunction("exit", (v) -> { throw new ExitStatement(v); }); + addUnaryFunction("return", (v) -> { throw new ReturnStatement(v); }); + addUnaryFunction("throw", (v)-> {throw new ThrowStatement(v); }); + + addLazyFunction("try", 2, (c, t, lv) -> + { + try + { + Value retval = lv.get(0).evalValue(c, t); + return (c_, t_) -> retval; + } + catch (ThrowStatement ret) + { + LazyValue __ = c.getVariable("_"); + c.setVariable("_", (__c, __t) -> ret.retval.reboundedTo("_")); + Value val = lv.get(1).evalValue(c, t); + c.setVariable("_",__); + return (c_, t_) -> val; + } + }); + + // if(cond1, expr1, cond2, expr2, ..., ?default) => value + addLazyFunction("if", -1, (c, t, lv) -> + { + if ( lv.size() < 2 ) + throw new InternalExpressionException("if statement needs to have at least one condition and one case"); + for (int i=0; i ret; + } + } + if (lv.size()%2 == 1) + { + Value ret = lv.get(lv.size() - 1).evalValue(c); + return (cc, tt) -> ret; + } + return (cc, tt) -> Value.ZERO; + }); + } + + /** + *

Operators

+ *
+ * + *

There is a number of operators you can use inside the expressions. Those could be considered + * generic type operators that apply to most data types. They also follow standard operator + * precedence, i.e. 2+2*2 is understood as 2+(2*2), + * not (2+2)*2, otherwise they are applied from left to right, i.e. + * 2+4-3 is interpreted as (2+4)-3, which in case of numbers + * doesn't matter, but since scarpet allows for mixing all value types + * the associativity would matter, and may lead to unintended effects:

+ *

Important operator is function definition -> operator. It will be covered + * in {@link carpet.script.Expression#UserDefinedFunctionsAndControlFlow}

+ *
+     * '123'+4-2 => ('123'+4)-2 => '1234'-2 => '134'
+     * '123'+(4-2) => '123'+2 => '1232'
+     * 3*'foo' => 'foofoofoo'
+     * 1357-5 => 1352
+     * 1357-'5' => 137
+     * 3*'foo'-'o' => 'fff'
+     * l(1,3,5)+7 => l(8,10,12)
+     * 
+ *

As you can see, values can behave differently when mixed with other types + * in the same expression. in case values are of the same types, the result + * tends to be obvious, but Scarpet tries to make sense of whatever it + * has to deal with

+ *

Operator Precedence

+ *

+ * Here is the complete list of operators in scarpet including control flow operators. + * note, that commas and brackets are not operators, but behave like them: + *

+ *
    + *
  • Unary + - !
  • + *
  • Exponent ^
  • + *
  • Multiplication * / %
  • + *
  • Addition + -
  • + *
  • Comparison == != > >= <= < ~
  • + *
  • Logical And&&
  • + *
  • Logical Or ||
  • + *
  • Assignment = += <>
  • + *
  • Definition ->
  • + *
  • Next statement;
  • + *
  • Comma ,
  • + *
  • Bracket ( )
  • + *
+ * + *

Basic Arithmetic Operators + - * /

+ *

Allows to add the results of two expressions. If the operands resolve to numbers, the result is + * arithmetic operation. + * In case of strings, adding or subtracting from a string results in string concatenation and + * removal of substrings from that string. Multiplication of strings and numbers results in repeating the + * string N times and division results in taking the first k'th part of the string, so that str*n/n ~ str + * In case first operand is a list, either it results in a new list + * with all elements modified one by one with the other operand, or if the operand is a list with the same number of + * items - elementwise addition/subtraction

+ *

Examples:

+ *
+     * 2+3 => 5
+     * 'foo'+3+2 => 'foo32'
+     * 'foo'+(3+2) => 'foo5'
+     * 3+2+'bar' => '5bar'
+     * 'foo'*3 => 'foofoofoo'
+     * 'foofoofoo' / 3 => 'foo'
+     * 'foofoofoo'-'o' => 'fff'
+     * l(1,2,3)+1  => l(2,3,4)
+     * b = l(100,63,100); b+l(10,0,10)  => l(110,63,110)
+     * 
+ * + *

Just Operators % ^

+ *

The modulo and exponent (power) operators work only if both operands are numbers

+ *
+     * pi^pi%euler  => 1.124....
+     * -9 % 4  => -1
+     * 9 % -4  => 0 ¯\_(ツ)_/¯ Java
+     * -3 ^ 2  => 9
+     * -3 ^ pi => // Error
+     * 
+ * + *

Comparison Operators == != < > <= >=

+ *

Allows to compare the results of two expressions. + * For numbers it is considers arithmetic order of numbers, for strings - lexicographical, + * nulls are always 'less' than everything else, and lists check their elements - if the sizes + * are different, the size matters, otherwise, pairwise comparisons for each elements are performed. + * The same order rules than with all these operators are used with the default sortographical order as + * used by sort function. All of these are true: + *

+ *
+     * null == null
+     * null != false
+     * 0 == false
+     * 1 == true
+     * null < 0
+     * null < -1000
+     * 1000 < 'a'
+     * 'bar' < 'foo'
+     * 3 == 3.0
+     * 
+ * + *

Logical Operators && ||

+ *

These operator compute respective boolean operation on the operands. What it important is that if calculating + * of the second operand is not necessary, it won't be evaluated, which means one can use them as conditional + * statements. In case of success returns first positive operand (||) or last one (&&).

+ *
+     * true || false  => 1
+     * null || false => 0
+     * null != false || run('kill gnembon')  => 1 // gnembon survives
+     * null != false && run('kill gnembon')  => 0 // when cheats not allowed
+     * null != false && run('kill gnembon')  => 1 // gnembon dies, cheats allowed
+     * 
+ * + *

Matching Operator ~

+ *

This operator should be understood as 'matches', or 'in'. For strings it matches the right operand as a regular + * expression to the left one, returning the first match. This can be used to extract information from unparsed nbt's + * in a more efficient way. For lists it checks if an element is in the list, and returns the index of that element, + * or null if no such element was found, especially that the use of first function will not + * return the index. Currently it doesn't have any special behaviour for numbers - it checks for existence of characters + * in string representation of the left operand with respect of the regular expression on the right hand side. + * string

+ *

in Minecraft API portion entity ~ feature is a shortcode for query(entity,feature) + * for queries that do not take any extra arguments.

+ *
+     * l(1,2,3) ~ 2  => 1
+     * l(1,2,3) ~ 4  => null
+     * 'foobar' ~ '.b'  => 'ob'
+     * player('*') ~ 'gnembon'  // null unless player gnembon is logged in (better to use player('gnembon') instead
+     * p ~ 'sneaking' // if p is an entity returns whether p is sneaking
+     * 
+ *

Or a longer example of an ineffective way to searching for a squid

+ *
+     * entities = entities_area('all',x,y,z,100,10,100);
+     * sid = entities ~ 'Squid';
+     * if(sid != null, run('execute as '+query(element(entities,sid),'id')+' run say I am here '+query(element(entities,sid),'pos') ) )
+     * 
+ *

Or an example to find if a player has specific enchantment on a held axe (either hand) and get its level + * (despite obvious lack of support for json NBT's):

+ *
+     * global_get_enchantment(p, ench) -> (
+     * $   for(l('main','offhand'),
+     * $      holds = query(p, 'holds', _);
+     * $      if( holds,
+     * $         l(what, count, nbt) = holds;
+     * $         if( what ~ '_axe' && nbt ~ ench,
+     * $            lvl = max(lvl, number(nbt ~ '(?<=lvl:)\\d') )
+     * $         )
+     * $      )
+     * $   );
+     * $   lvl
+     * $);
+     * /script run global_get_enchantment(players(), 'sharpness')
+     * 
+ *

Assignment Operators = <> +=

+ *

A set of assignment operators. All require bounded variable on the LHS, <> requires + * bounded arguments on the right hand side as well (bounded, meaning being variables). Additionally they can also + * handle list constructors with all bounded variables, and work then as list assignment operators. + * When += is used on a list, it extends that list of that element, and returns the list (old == new). + * scarpet doesn't support currently removal of items. Removal of items can be obtaine via + * filter command, and reassigning it fo the same variable. Both operations would require rewriting of the + * array anyways.

+ *
+     * a = 5  => a == 5
+     * l(a,b,c) = l(3,4,5) => a==3, b==4, c==5
+     * l(minx,maxx) = sort(xi,xj);  // minx assumes min(xi, xj) and maxx, max(xi, xj)
+     * l(a,b,c,d,e,f) = l(range(6)); l(a,b,c) <> l(d,e,f); l(a,b,c,d,e,f)  => [3,4,5,0,1,2]
+     * a = l(1,2,3); a += 4  => [1,2,3,4]
+     * a = l(1,2,3,4); a = filter(a,_!=2)  => [1,3,4]
+     * 
+ *

Unary Operators - +

+ *

Require a number, flips the sign. One way to assert it's a number is by crashing the script. gg.

+ *
+     * -4  => -4
+     * +4  => 4
+     * +'4'  // Error message
+     * 
+ * + *

Negation Operator !

+ *

flips boolean condition of the expression. Equivalent of bool(expr)==false

+ *
+     * !true  => 0
+     * !false  => 1
+     * !null  => 1
+     * !5  => 0
+     * !l() => 1
+     * !l(null) => 0
+     * 
+ *
+ */ + public void Operators() + { + addBinaryOperator("+", precedence.get("addition+-"), true, Value::add); + addBinaryOperator("-", precedence.get("addition+-"), true, Value::subtract); + addBinaryOperator("*", precedence.get("multiplication*/%"), true, Value::multiply); + addBinaryOperator("/", precedence.get("multiplication*/%"), true, Value::divide); + addBinaryOperator("%", precedence.get("multiplication*/%"), true, (v1, v2) -> + new NumericValue(NumericValue.asNumber(v1).getDouble() % NumericValue.asNumber(v2).getDouble())); + addBinaryOperator("^", precedence.get("exponent^"), false, (v1, v2) -> + new NumericValue(Math.pow(NumericValue.asNumber(v1).getDouble(), NumericValue.asNumber(v2).getDouble()))); + + addLazyBinaryOperator("&&", precedence.get("and&&"), false, (c, t, lv1, lv2) -> + { + Value v1 = lv1.evalValue(c, Context.BOOLEAN); + if (!v1.getBoolean()) return (cc, tt) -> v1; + Value v2 = lv2.evalValue(c, Context.BOOLEAN); + return v2.getBoolean() ? ((cc, tt) -> v2) : LazyValue.FALSE; + }); + + addLazyBinaryOperator("||", precedence.get("or||"), false, (c, t, lv1, lv2) -> + { + Value v1 = lv1.evalValue(c, Context.BOOLEAN); + if (v1.getBoolean()) return (cc, tt) -> v1; + Value v2 = lv2.evalValue(c, Context.BOOLEAN); + return v2.getBoolean() ? ((cc, tt) -> v2) : LazyValue.FALSE; + }); + + addBinaryOperator("~", precedence.get("compare>=><=<"), true, Value::in); + + addBinaryOperator(">", precedence.get("compare>=><=<"), false, (v1, v2) -> + v1.compareTo(v2) > 0 ? Value.TRUE : Value.FALSE); + addBinaryOperator(">=", precedence.get("compare>=><=<"), false, (v1, v2) -> + v1.compareTo(v2) >= 0 ? Value.TRUE : Value.FALSE); + addBinaryOperator("<", precedence.get("compare>=><=<"), false, (v1, v2) -> + v1.compareTo(v2) < 0 ? Value.TRUE : Value.FALSE); + addBinaryOperator("<=", precedence.get("compare>=><=<"), false, (v1, v2) -> + v1.compareTo(v2) <= 0 ? Value.TRUE : Value.FALSE); + addBinaryOperator("==", precedence.get("equal==!="), false, (v1, v2) -> + v1.equals(v2) ? Value.TRUE : Value.FALSE); + addBinaryOperator("!=", precedence.get("equal==!="), false, (v1, v2) -> + v1.equals(v2) ? Value.FALSE : Value.TRUE); + + addLazyBinaryOperator("=", precedence.get("assign=<>"), false, (c, t, lv1, lv2) -> + { + Value v1 = lv1.evalValue(c); + Value v2 = lv2.evalValue(c); + if (v1 instanceof ListValue.ListConstructorValue && v2 instanceof ListValue) + { + List ll = ((ListValue)v1).getItems(); + List rl = ((ListValue)v2).getItems(); + if (ll.size() < rl.size()) throw new InternalExpressionException("Too many values to unpack"); + if (ll.size() > rl.size()) throw new InternalExpressionException("Too few values to unpack"); + for (Value v: ll) v.assertAssignable(); + Iterator li = ll.iterator(); + Iterator ri = rl.iterator(); + while(li.hasNext()) + { + String lname = li.next().getVariable(); + Value vval = ri.next().reboundedTo(lname); + c.setVariable(lname, (cc, tt) -> vval); + } + return (cc, tt) -> Value.TRUE; + } + v1.assertAssignable(); + String varname = v1.getVariable(); + Value copy = v2.reboundedTo(varname); + LazyValue boundedLHS = (cc, tt) -> copy; + c.setVariable(varname, boundedLHS); + return boundedLHS; + }); + + addLazyBinaryOperator("+=", precedence.get("assign=<>"), false, (c, t, lv1, lv2) -> + { + Value v1 = lv1.evalValue(c); + Value v2 = lv2.evalValue(c); + if (v1 instanceof ListValue.ListConstructorValue && v2 instanceof ListValue) + { + List ll = ((ListValue)v1).getItems(); + List rl = ((ListValue)v2).getItems(); + if (ll.size() < rl.size()) throw new InternalExpressionException("Too many values to unpack"); + if (ll.size() > rl.size()) throw new InternalExpressionException("Too few values to unpack"); + for (Value v: ll) v.assertAssignable(); + Iterator li = ll.iterator(); + Iterator ri = rl.iterator(); + while(li.hasNext()) + { + Value lval = li.next(); + String lname = lval.getVariable(); + Value result = lval.add(ri.next()).bindTo(lname); + c.setVariable(lname, (cc, tt) -> result); + } + return (cc, tt) -> Value.TRUE; + } + v1.assertAssignable(); + String varname = v1.getVariable(); + LazyValue boundedLHS; + if (v1 instanceof ListValue) + { + ((ListValue) v1).append(v2); + boundedLHS = (cc, tt)-> v1; + } + else + { + Value result = v1.add(v2).bindTo(varname); + boundedLHS = (cc, tt) -> result; + } + c.setVariable(varname, boundedLHS); + return boundedLHS; + }); + + addLazyBinaryOperator("<>", precedence.get("assign=<>"), false, (c, t, lv1, lv2) -> + { + Value v1 = lv1.evalValue(c); + Value v2 = lv2.evalValue(c); + if (v1 instanceof ListValue.ListConstructorValue && v2 instanceof ListValue.ListConstructorValue) + { + List ll = ((ListValue)v1).getItems(); + List rl = ((ListValue)v2).getItems(); + if (ll.size() < rl.size()) throw new InternalExpressionException("Too many values to unpack"); + if (ll.size() > rl.size()) throw new InternalExpressionException("Too few values to unpack"); + for (Value v: ll) v.assertAssignable(); + for (Value v: rl) v.assertAssignable(); + Iterator li = ll.iterator(); + Iterator ri = rl.iterator(); + while(li.hasNext()) + { + Value lval = li.next(); + Value rval = ri.next(); + String lname = lval.getVariable(); + String rname = rval.getVariable(); + lval.reboundedTo(rname); + rval.reboundedTo(lname); + c.setVariable(lname, (cc, tt) -> rval); + c.setVariable(rname, (cc, tt) -> lval); + } + return (cc, tt) -> Value.TRUE; + } + v1.assertAssignable(); + v2.assertAssignable(); + String lvalvar = v1.getVariable(); + String rvalvar = v2.getVariable(); + Value lval = v2.reboundedTo(lvalvar); + Value rval = v1.reboundedTo(rvalvar); + c.setVariable(lvalvar, (cc, tt) -> lval); + c.setVariable(rvalvar, (cc, tt) -> rval); + return (cc, tt) -> lval; + }); + + addUnaryOperator("-", false, (v) -> new NumericValue(-NumericValue.asNumber(v).getDouble())); + + addUnaryOperator("+", false, (v) -> new NumericValue(NumericValue.asNumber(v).getDouble())); + + addLazyUnaryOperator("!", precedence.get("unary+-!"), false, (c, t, lv)-> lv.evalValue(c, Context.BOOLEAN).getBoolean() ? (cc, tt)-> Value.FALSE : (cc, tt) -> Value.TRUE); // might need context boolean + + } + + /** + *

Arithmetic operations

+ *
+ *

Basic Arithmetic Functions

+ *

There is bunch of them - they require a number and spit out a number, + * doing what you would expect them to do.

+ *

fact(n)

+ *

Factorial of a number, a.k.a n!, just not in scarpet. Gets big... quick...

+ *

sqrt(n)

+ *

Square root. For other fancy roots, use ^, math and yo noggin. Imagine square roots on a tree...

+ *

abs(n)

+ *

Absolut value.

+ *

round(n)

+ *

Closest integer value. Did you know the earth is also round?

+ *

floor(n)

+ *

Highest integer that is still no larger then n. Insert a floor pun here.

+ *

ceil(n)

+ *

First lucky integer that is not smalller than n. As you would expect, ceiling is typically + * right above the floor.

+ *

ln(n)

+ *

Natural logarithm of n. Naturally.

+ *

ln1p(n)

+ *

Natural logarithm of n+1. Very optimistic.

+ *

log10(n)

+ *

Decimal logarithm of n. Its ceiling is the length of its floor.

+ *

log(n)

+ *

Binary logarithm of n. Finally, a proper one, not like the other 100.

+ *

log1p(n)

+ *

Binary logarithm of n+1. Also always positive.

+ *

mandelbrot(a, b, limit)

+ *

Computes the value of the mandelbrot set, for set a and b spot. + * Spot the beetle. Why not.

+ *

min(arg, ...), min(list), max(arg, ...), max(list)

+ *

Compute minimum or maximum of supplied arguments assuming default sortraphical order. In case you are + * missing argmax, just use a ~ max(a), little less efficient, but still fun. + *

+ *

+ * Interesting bit - min and max don't remove variable associations from arguments, which + * means can be used as LHS of assignments (obvious case), or argument spec in function definitions (far less obvious). + *

+ *
+     * a = 1; b = 2; min(a,b) = 3; l(a,b)  => [3, 2]
+     * a = 1; b = 2; fun(x, min(a,b)) -> l(a,b); fun(3,5)  => [5, 0]
+     * 
+ *

Absolutely no idea, how the latter might be useful in practice. But since it compiles, can ship it.

+ * + *

relu(n)

+ *

Linear rectifier of n. 0 below 0, n above. Why not. max(0,n) + * with less moral repercussions.

+ *

Trigonometric / Geometric Functions

+ *

sin(x)

+ *

cos(x)

+ *

tan(x)

+ *

asin(x)

+ *

acos(x)

+ *

atan(x)

+ *

atan2(x,y)

+ *

sinh(x)

+ *

cosh(x)

+ *

tanh(x)

+ *

sec(x)

+ *

csc(x)

+ *

sech(x)

+ *

csch(x)

+ *

cot(x)

+ *

acot(x)

+ *

coth(x)

+ *

asinh(x)

+ *

acosh(x)

+ *

atanh(x)

+ *

rad(deg)

+ *

deg(rad)

+ *

Use as you wish

+ * + *
+ * + */ + + public void ArithmeticOperations() + { + addLazyFunction("not", 1, (c, t, lv) -> lv.get(0).evalValue(c, Context.BOOLEAN).getBoolean() ? ((cc, tt) -> Value.FALSE) : ((cc, tt) -> Value.TRUE)); + + addUnaryFunction("fact", (v) -> + { + long number = NumericValue.asNumber(v).getLong(); + long factorial = 1; + for (int i = 1; i <= number; i++) + { + factorial = factorial * i; + } + return new NumericValue(factorial); + }); + addMathematicalUnaryFunction("sin", (d) -> Math.sin(Math.toRadians(d))); + addMathematicalUnaryFunction("cos", (d) -> Math.cos(Math.toRadians(d))); + addMathematicalUnaryFunction("tan", (d) -> Math.tan(Math.toRadians(d))); + addMathematicalUnaryFunction("asin", (d) -> Math.toDegrees(Math.asin(d))); + addMathematicalUnaryFunction("acos", (d) -> Math.toDegrees(Math.acos(d))); + addMathematicalUnaryFunction("atan", (d) -> Math.toDegrees(Math.atan(d))); + addMathematicalBinaryFunction("atan2", (d, d2) -> Math.toDegrees(Math.atan2(d, d2)) ); + addMathematicalUnaryFunction("sinh", Math::sinh ); + addMathematicalUnaryFunction("cosh", Math::cosh ); + addMathematicalUnaryFunction("tanh", Math::tanh ); + addMathematicalUnaryFunction("sec", (d) -> 1.0 / Math.cos(Math.toRadians(d)) ); // Formula: sec(x) = 1 / cos(x) + addMathematicalUnaryFunction("csc", (d) -> 1.0 / Math.sin(Math.toRadians(d)) ); // Formula: csc(x) = 1 / sin(x) + addMathematicalUnaryFunction("sech", (d) -> 1.0 / Math.cosh(d) ); // Formula: sech(x) = 1 / cosh(x) + addMathematicalUnaryFunction("csch", (d) -> 1.0 / Math.sinh(d) ); // Formula: csch(x) = 1 / sinh(x) + addMathematicalUnaryFunction("cot", (d) -> 1.0 / Math.tan(Math.toRadians(d)) ); // Formula: cot(x) = cos(x) / sin(x) = 1 / tan(x) + addMathematicalUnaryFunction("acot", (d) -> Math.toDegrees(Math.atan(1.0 / d)) );// Formula: acot(x) = atan(1/x) + addMathematicalUnaryFunction("coth", (d) -> 1.0 / Math.tanh(d) ); // Formula: coth(x) = 1 / tanh(x) + addMathematicalUnaryFunction("asinh", (d) -> Math.log(d + (Math.sqrt(Math.pow(d, 2) + 1)))); // Formula: asinh(x) = ln(x + sqrt(x^2 + 1)) + addMathematicalUnaryFunction("acosh", (d) -> Math.log(d + (Math.sqrt(Math.pow(d, 2) - 1)))); // Formula: acosh(x) = ln(x + sqrt(x^2 - 1)) + addMathematicalUnaryFunction("atanh", (d) -> // Formula: atanh(x) = 0.5*ln((1 + x)/(1 - x)) + { + if (Math.abs(d) > 1 || Math.abs(d) == 1) + throw new InternalExpressionException("Number must be |x| < 1"); + return 0.5 * Math.log((1 + d) / (1 - d)); + }); + addMathematicalUnaryFunction("rad", Math::toRadians); + addMathematicalUnaryFunction("deg", Math::toDegrees); + addMathematicalUnaryFunction("ln", Math::log); + addMathematicalUnaryFunction("ln1p", Math::log1p); + addMathematicalUnaryFunction("log10", Math::log10); + addMathematicalUnaryFunction("log", a -> Math.log(a)/Math.log(2)); + addMathematicalUnaryFunction("log1p", x -> Math.log1p(x)/Math.log(2)); + addMathematicalUnaryFunction("sqrt", Math::sqrt); + addMathematicalUnaryFunction("abs", Math::abs); + addMathematicalUnaryFunction("round", (d) -> (double)Math.round(d)); + addMathematicalUnaryFunction("floor", Math::floor); + addMathematicalUnaryFunction("ceil", Math::ceil); + + addLazyFunction("mandelbrot", 3, (c, t, lv) -> { + double a0 = NumericValue.asNumber(lv.get(0).evalValue(c)).getDouble(); + double b0 = NumericValue.asNumber(lv.get(1).evalValue(c)).getDouble(); + long maxiter = NumericValue.asNumber(lv.get(2).evalValue(c)).getLong(); + double a = 0.0D; + double b = 0.0D; + long iter = 0; + while(a*a+b*b<4 && iter < maxiter) + { + double temp = a*a-b*b+a0; + b = 2*a*b+b0; + a = temp; + iter++; + } + long iFinal = iter; + return (cc, tt) -> new NumericValue(iFinal); + }); + + addFunction("max", (lv) -> + { + if (lv.size() == 0) + throw new InternalExpressionException("MAX requires at least one parameter"); + Value max = null; + if (lv.size()==1 && lv.get(0) instanceof ListValue) + lv = ((ListValue) lv.get(0)).getItems(); + for (Value parameter : lv) + { + if (max == null || parameter.compareTo(max) > 0) max = parameter; + } + return max; + }); + + addFunction("min", (lv) -> + { + if (lv.size() == 0) + throw new InternalExpressionException("MIN requires at least one parameter"); + Value min = null; + if (lv.size()==1 && lv.get(0) instanceof ListValue) + lv = ((ListValue) lv.get(0)).getItems(); + for (Value parameter : lv) + { + if (min == null || parameter.compareTo(min) < 0) min = parameter; + } + return min; + }); + + addUnaryFunction("relu", (v) -> v.compareTo(Value.ZERO) < 0 ? Value.ZERO : v); + } + + /** + *

Lists, loops, and higher order functions

+ *
+ *

Efficient use of these functions cangreatly simplify your programs and speed them up, as these functions + * will internalize most of the operations that need to be applied on multiple values at the same time

+ *

Basic list operations

+ *

l(values ...), l(iterator)

+ *

Creates a list of values of the expressions passed as parameters. It can be used as an L-value and if all + * elements are variables, you coujld use it to return multiple results from one function call, if that function returns + * a list of results with the same size as the l call uses. In case there is only one argument and it is + * an iterator (vanilla expression specification has range, but Minecraft API implements + * a bunch of them, like diamond), it will convert it to a proper list. Iterators can only be used in + * high order functions, and are treated as empty lists, unless unrolled with l

+ *
+     * l(1,2,'foo') => [1, 2, foo]
+     * l() => [] (empty list)
+     * l(range(10)) => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+     * l(1, 2) = l(3, 4) => Error: l is not a variable
+     * l(foo, bar) = l(3,4); foo==3 && bar==4 => 1
+     * l(foo, bar, baz) = l(2, 4, 6); l(min(foo, bar), baz) = l(3, 5); l(foo, bar, baz)  => [3, 4, 5]
+     * 
+ *

In the last example l(min(foo, bar), baz) creates a valid L-value, as min(foo, bar) finds the + * lower of the variables (in this case foo) creating a valid assignable L-list of [foo, baz], and these + * values will be assigned new values

+ * + *

join(delim, list), join(delim, values ...)

+ *

Returns a string that contains joined elements of the list, iterator, or all values, concatenated with delim delimiter

+ *
+     *     join('-',range(10))  => 0-1-2-3-4-5-6-7-8-9
+     *     join('-','foo')  => foo
+     *     join('-', 'foo', 'bar')  => foo-bar
+     * 
+ * + *

split(delim, expr)

+ *

Splits a string undr expr by delim which can be a regular expression

+ *
+     *     split('',foo)  => [f, o, o]
+     *     split('.','foo.bar')  => []
+     *     split('\\.','foo.bar')  => [foo, bar]
+     * 
+ * + *

slice(expr, from, to?)

+ *

extracts a substring, or sublist (based on the type of the result of the expression under expr with starting index + * of from, and ending at to if provided, or the end, if omitted

+ *
+     *     slice(l(0,1,2,3,4,5), 1, 3)  => [1, 2, 3] 
+     *     slice('foobar', 0, 1)  => 'f'
+     *     slice('foobar', 3)  => 'bar'
+     *     slice(range(10), 3, 5)  => [3, 4, 5]
+     *     slice(range(10), 5)  => [5, 6, 7, 8, 9]
+     * 
+ * + * + * + *

sort(list), sort(values ...)

+ *

Sorts in the default sortographical order either all arguments, or a list if its the only argument. It returns a new + * sorted list, not affecting the list passed to the argument

+ *
+     * sort(3,2,1)  => [1, 2, 3]
+     * sort('a',3,11,1)  => [1, 3, 11, 'a']
+     * list = l(4,3,2,1); sort(list)  => [1, 2, 3, 4]
+     * 
+ * + *

sort_key(list, key_expr)

+ *

Sorts a copy of the list in the order or keys as defined by the key_expr for each element

+ *
+     *     sort_key([1,3,2],_)  => [1, 2, 3]
+     *     sort_key([1,3,2],-_)  => [3, 2, 1]
+     *     sort_key(l(range(10)),rand(1))  => [1, 0, 9, 6, 8, 2, 4, 5, 7, 3]
+     *     sort_key(l(range(20)),str(_))  => [0, 1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9]
+     * 
+ * + *

range(to), range(from, to), range(from, to, step)

+ *

Creates a range of numbers from from, no greater/larger than to. + * The step parameter dictates not only the increment size, but also direction (can be negative). + * The returned value is not a proper list, just the iterator + * but if for whatever reason you need a proper list with all items evaluated, use l(range(to)). + * Primarily to be used in higher order functions

+ *
+     *     range(10)  => [...]
+     *     l(range(10))  => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+     *     map(range(10),_*_)  => [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
+     *     reduce(range(10),_a+_, 0)  => 45
+     *     range(5,10)  => [5, 6, 7, 8, 9]
+     *     range(20, 10, -2)  => [20, 18, 16, 14, 12]
+     * 
+ * + *

element(list, index)

+ *

Returns the value at index element from the list. + * use negative numbers to reach elements from the end of the list. element + * call will always be able to find the index. In case there is few items, it will loop over

+ *
+     *     element(l(range(10)), 5)  => 5
+     *     element(l(range(10)), -1)  => 9
+     *     element(l(range(10)), 10)  => 0
+     *     element(l(range(10)), 93)  => 3
+     * 
+ * + *

put(list, index, values ...), put(list, null, values ...)

+ *

Modifies the list by replacing values starting from index with values. + * use negative numbers to reach elements from the end of the list. put + * call will always be able to find the index. In case there is few items, it will loop over. In case end + * of the list is reached before values run out, list is extended to accomodate for more values. in case you + * want to append at the end of the list, use null as index. returns number of elements inserted.

+ *
+     *     a = l(1, 2, 3); put(a, 1, 4); a  => [1, 4, 3]
+     *     a = l(1, 2, 3); put(a, null, 4, 5, 6); a  => [1, 2, 3, 4, 5, 6]
+     *     a = l(l(0,0,0),l(0,0,0),l(0,0,0)); put(element(a, 1), 1, 1); a => [[0, 0, 0], [0, 1, 0], [0, 0, 0]]
+     * 
+ *

while(cond, limit, expr)

+ *

Evaluates expression expr repeatedly until condition cond becomes false, + * but not more than limit times. Returns the result of the last expr evaluation, + * or null if nothing was successful. both expr and cond will recveived a + * bound variable _ indicating current iteration, so its a number

+ *
+     *  while(a<100,10,a=_*_)  => 81 // loop exhausted via limit
+     *  while(a<100,20,a=_*_)  => 100 // loop stopped at condition, but a has already been assigned
+     *  while(_*_<100,20,a=_*_)  => 81 // loop stopped at condition, before a was assigned a value
+     * 
+ * + *

loop(num,expr(_),exit(_)?)

+ *

Evaluates expression expr, num number of times. Optionally, + * if exit condition is present, stops the execution if the condition becomes true. + * Both expr and exit receive _ system variable indicating the iteration

+ *
+     *     loop(5, tick())  => repeat tick 5 times
+     *     list = l(); loop(5, x = _; loop(5, list += l(x, _) ) ); list
+     *       // double loop, produces: [[0, 0], [0, 1], [0, 2], [0, 3], [0, 4], [1, 0], [1, 1], ... , [4, 2], [4, 3], [4, 4]]
+     * 
+ *

In this small example we will search for first 10 primes, apparently including 0:

+ *
+     *     check_prime(n) -> !first( range(2, sqrt(n)+1), !(n % _) );
+     *     primes = l();
+     *     loop(10000, if(check_prime(_), primes += _), length(primes) >= 10 );
+     *     primes
+     *
+     *     // outputs: [0, 1, 2, 3, 5, 7, 11, 13, 17, 19]
+     * 
+ *

map(list,expr(_,_i), exit(_,_i)?)

+ *

Converts a list of values, to another list where each value is result of an expression + * v = expr(_, _i) where _ is passed as each element of the list, and _i is + * the index of such element, optional exit condition is evaluated after each mapping and if it is false, + * the map is terminated and returned whatever got collected so far

+ *
+     *     map(range(10), _*_)  => [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
+     *     map(players('*'), _+' is stupid') [gnembon is stupid, herobrine is stupid]
+     * 
+ * + *

filter(list,expr(_,_i),exit(_,_i))

+ *

filters list elements returning only these that return positive result of the expr. + * Like with map if the optional condition exit gets true, the list is returned up to this point

+ *
+     *     filter(range(100), !(_%5), _*_>1000)  => [0, 5, 10, 15, 20, 25, 30]
+     *     map(filter(entities_list('all'),_=='Witch'), query(_,'pos') )  => [[1082.5, 57, 1243.5]]
+     * 
+ * + *

first(list,expr(_,_i))

+ *

Finds and returns the first item in the list that satisfies expr. It sets _ for current element value, + * and _i for index of that element

+ *
+     *     first(range(1000,10000), n=_; !first( range(2, sqrt(n)+1), !(n % _) ) )  => 1009 // first prime after 1000
+     * 
+ *

Notice in the example above, that we needed to rename the outer _ to be albe to use in in the inner + * first call

+ * + * + *

all(list,expr(_,_i))

+ *

Returns true if all elements on the list satisfy the condition. Its roughly equivalent to + * all(list,expr) <=> for(list,expr)==length(list), but works with generators. expr + * also receives bound _ and _i variables

+ *
+     *     all([1,2,3], check_prime(_))  => 1
+     *     all(neighbours(x,y,z), _=='stone')  => 1 // if all neighbours of [x, y, z] are stone
+     *     map(filter(rect(0,4,0,1000,0,1000), l(x,y,z)=pos(_); all(rect(x,y,z,1,0,1),_=='bedrock') ), pos(_) )
+     *       => [[-298, 4, -703], [-287, 4, -156], [-269, 4, 104], [242, 4, 250], [-159, 4, 335], [-208, 4, 416], [-510, 4, 546], [376, 4, 806]]
+     *         // find all 3x3 bedrock structures in the top bedrock layer
+     *     map( filter( rect(0,4,0,1000,1,1000,1000,0,1000), l(x,y,z)=pos(_);
+     *             all(rect(x,y,z,1,0,1),_=='bedrock') && for(rect(x,y-1,z,1,1,1,1,0,1),_=='bedrock')<8),
+     *        pos(_) )  => [[343, 3, -642], [153, 3, -285], [674, 3, 167], [-710, 3, 398]]
+     *         // ditto, but requiring at most 7 bedrock block in the 18 blocks below them
+     * 
+ * + * + *

for(list,expr(_,_i),exit(_,_i)?)

+ *

Evaluates expression over list of items from the list, and can optionally stop early if + * exit expression is specified and evaluates true for a given iteration. Supplies + * _(value) and _i(iteration number) to both expr and exit. + * Returns the number of times expr was successful

+ *
+     *     check_prime(n) -> !first( range(2, sqrt(n)+1), !(n % _) );
+     *        for(range(1000000,1100000),check_prime(_))  => 7216
+     * 
+ *

From which we can learn that there is 7216 primes between 1M and 1.1M

+ *

reduce(list,expr(_a,_,_i), initial)

+ *

Applies expr for each element of the list and saves the result in _a accumulator. + * Consecutive calls to expr can access that value to apply more values. You also need to specify + * the initial value to apply for the accumulator

+ *
+     *     reduce([1,2,3,4],_a+_,0)  => 10
+     *     reduce([1,2,3,4],_a*_,1)  => 24
+     * 
+ *
+ */ + public void ListsLoopsAndHigherOrderFunctions() + { + addFunction("l", lv -> + { + if (lv.size() == 1 && lv.get(0) instanceof LazyListValue) + return ListValue.wrap(((LazyListValue) lv.get(0)).unroll()); + return new ListValue.ListConstructorValue(lv); + }); + + addFunction("join", (lv) -> + { + if (lv.size() < 2) + throw new InternalExpressionException("join takes at least 2 arguments"); + String delimiter = lv.get(0).getString(); + List toJoin; + if (lv.size()==2 && lv.get(1) instanceof LazyListValue) + { + toJoin = ((LazyListValue) lv.get(1)).unroll(); + + } + else if (lv.size() == 2 && lv.get(1) instanceof ListValue) + { + toJoin = new ArrayList<>(((ListValue)lv.get(1)).getItems()); + } + else + { + toJoin = lv.subList(1,lv.size()); + } + return new StringValue(toJoin.stream().map(Value::getString).collect(Collectors.joining(delimiter))); + }); + + addBinaryFunction("split", (d, v) -> { + String delimiter = d.getString(); + String hwat = v.getString(); + return ListValue.wrap(Arrays.stream(hwat.split(delimiter)).map(StringValue::new).collect(Collectors.toList())); + }); + + addFunction("slice", (lv) -> { + + if (lv.size() != 2 && lv.size() != 3) + throw new InternalExpressionException("slice takes 2 or 3 arguments"); + Value hwat = lv.get(0); + long from = NumericValue.asNumber(lv.get(1)).getLong(); + long to = -1; + if (lv.size()== 3) + to = NumericValue.asNumber(lv.get(2)).getLong(); + return hwat.slice(from, to); + }); + + addFunction("sort", (lv) -> + { + List toSort = lv; + if (lv.size()==1 && lv.get(0) instanceof ListValue) + { + toSort = new ArrayList<>(((ListValue)lv.get(0)).getItems()); + } + Collections.sort(toSort); + return ListValue.wrap(toSort); + }); + + addLazyFunction("sort_key", 2, (c, t, lv) -> //get working with iterators + { + Value v = lv.get(0).evalValue(c); + if (!(v instanceof ListValue)) + throw new InternalExpressionException("First argument for sort_key should be a List"); + LazyValue sortKey = lv.get(1); + //scoping + LazyValue __ = c.getVariable("_"); + + List toSort = new ArrayList<>(((ListValue) v).getItems()); + + Collections.sort(toSort,(v1, v2) -> { + c.setVariable("_",(cc, tt) -> v1); + Value ev1 = sortKey.evalValue(c); + c.setVariable("_",(cc, tt) -> v2); + Value ev2 = sortKey.evalValue(c); + return ev1.compareTo(ev2); + }); + //revering scope + c.setVariable("_", __); + return (cc, tt) -> ListValue.wrap(toSort); + }); + + addFunction("range", (lv) -> + { + long from = 0; + long to = 0; + long step = 1; + int argsize = lv.size(); + if (argsize == 0 || argsize > 3) + throw new InternalExpressionException("range accepts from 1 to 3 arguments, not "+argsize); + to = NumericValue.asNumber(lv.get(0)).getLong(); + if (lv.size() > 1) + { + from = to; + to = NumericValue.asNumber(lv.get(1)).getLong(); + if (lv.size() > 2) + { + step = NumericValue.asNumber(lv.get(2)).getLong(); + } + } + return LazyListValue.range(from, to, step); + }); + + addBinaryFunction("element", (v1, v2) -> { + if (v1 instanceof LazyListValue || !(v1 instanceof ListValue)) + { + throw new InternalExpressionException("First argument of element should be a list"); + } + List items = ((ListValue)v1).getItems(); + long index = NumericValue.asNumber(v2).getLong(); + int numitems = items.size(); + long range = abs(index)/numitems; + index += (range+2)*numitems; + index = index % numitems; + return items.get((int)index); + }); + addFunction("put", (lv) -> + { + if(lv.size()<3) + { + throw new InternalExpressionException("put takes at least three arguments, a list, index, and values to insert at that index"); + } + Value list = lv.get(0); + if (list instanceof LazyListValue || !(list instanceof ListValue)) + { + throw new InternalExpressionException("First argument of element should be a list"); + } + int size = lv.size(); + //List items = ((ListValue)lv.get(0)).getItems(); + Value index = lv.get(1); + if (index == Value.NULL) + { + ((ListValue) list).extend(lv.subList(2,size)); + } + else + { + ((ListValue) list).addAtIndex((int) NumericValue.asNumber(index).getLong(), lv.subList(2,size)); + } + return new NumericValue(size-2); + }); + + //condition and expression will get a bound 'i' + //returns last successful expression or false + // while(cond, limit, expr) => ?? + addLazyFunction("while", 3, (c, t, lv) -> + { + long limit = NumericValue.asNumber(lv.get(1).evalValue(c)).getLong(); + LazyValue condition = lv.get(0); + LazyValue expr = lv.get(2); + long i = 0; + Value lastOne = Value.NULL; + //scoping + LazyValue _val = c.getVariable("_"); + c.setVariable("_",(cc, tt) -> new NumericValue(0).bindTo("_")); + while (i new NumericValue(seriously).bindTo("_")); + } + //revering scope + c.setVariable("_", _val); + Value lastValueNoKidding = lastOne; + return (cc, tt) -> lastValueNoKidding; + }); + + // loop(Num, expr, exit_condition) => last_value + // loop(list, expr) + // expr receives bounded variable '_' indicating iteration + addLazyFunction("loop", -1, (c, t, lv) -> + { + if (lv.size()<2 || lv.size()>3) + { + throw new InternalExpressionException("Incorrect number of attributes for loop, should be 2 or 3, not "+lv.size()); + } + long limit = NumericValue.asNumber(lv.get(0).evalValue(c)).getLong(); + Value lastOne = Value.NULL; + LazyValue expr = lv.get(1); + LazyValue cond = null; + if(lv.size() > 2) cond = lv.get(2); + //scoping + LazyValue _val = c.getVariable("_"); + for (long i=0; i < limit; i++) + { + long whyYouAsk = i; + c.setVariable("_", (cc, tt) -> new NumericValue(whyYouAsk).bindTo("_")); + lastOne = expr.evalValue(c); + if (cond != null && cond.evalValue(c).getBoolean()) + break; + } + //revering scope + c.setVariable("_", _val); + Value trulyLastOne = lastOne; + return (cc, tt) -> trulyLastOne; + }); + + // map(list or Num, expr) => list_results + // receives bounded variable '_' with the expression + addLazyFunction("map", -1, (c, t, lv) -> + { + if (lv.size()<2 || lv.size()>3) + { + throw new InternalExpressionException("Incorrect number of attributes for map, should be 2 or 3, not "+lv.size()); + } + + Value rval= lv.get(0).evalValue(c); + + if (!(rval instanceof ListValue)) + throw new InternalExpressionException("First argument of map function should be a list or iterator"); + Iterator iterator = ((ListValue) rval).iterator(); + LazyValue expr = lv.get(1); + LazyValue cond = null; + if(lv.size() > 2) cond = lv.get(2); + //scoping + LazyValue _val = c.getVariable("_"); + LazyValue _iter = c.getVariable("_i"); + List result = new ArrayList<>(); + for (int i=0; iterator.hasNext(); i++) + { + Value next = iterator.next(); + String var = next.boundVariable; + next.bindTo("_"); + int doYouReally = i; + c.setVariable("_", (cc, tt) -> next); + c.setVariable("_i", (cc, tt) -> new NumericValue(doYouReally).bindTo("_i")); + result.add(expr.evalValue(c)); + if (cond != null && cond.evalValue(c).getBoolean()) + { + next.boundVariable = var; + break; + } + next.boundVariable = var; + } + ((ListValue) rval).fatality(); + LazyValue ret = (cc, tt) -> ListValue.wrap(result); + //revering scope + c.setVariable("_", _val); + c.setVariable("_i", _iter); + return ret; + }); + + // grep(list or num, expr, exit_expr) => list + // receives bounded variable '_' with the expression, and "_i" with index + // produces list of values for which the expression is true + addLazyFunction("filter", -1, (c, t, lv) -> + { + if (lv.size()<2 || lv.size()>3) + { + throw new InternalExpressionException("Incorrect number of attributes for filter, should be 2 or 3, not "+lv.size()); + } + + Value rval= lv.get(0).evalValue(c); + if (!(rval instanceof ListValue)) + throw new InternalExpressionException("First argument of filter function should be a list or iterator"); + Iterator iterator = ((ListValue) rval).iterator(); + LazyValue expr = lv.get(1); + LazyValue cond = null; + if(lv.size() > 2) cond = lv.get(2); + //scoping + LazyValue _val = c.getVariable("_"); + LazyValue _iter = c.getVariable("_i"); + List result = new ArrayList<>(); + for (int i=0; iterator.hasNext(); i++) + { + Value next = iterator.next(); + String var = next.boundVariable; + next.bindTo("_"); + int seriously = i; + c.setVariable("_", (cc, tt) -> next); + c.setVariable("_i", (cc, tt) -> new NumericValue(seriously).bindTo("_i")); + if(expr.evalValue(c).getBoolean()) + result.add(next); + if (cond != null && cond.evalValue(c).getBoolean()) + { + next.boundVariable = var; + break; + } + next.boundVariable = var; + } + ((ListValue) rval).fatality(); + LazyValue ret = (cc, tt) -> ListValue.wrap(result); // might be a trap - lazy evaluation + //revering scope + c.setVariable("_", _val); + c.setVariable("_i", _iter); + return ret; + }); + + // first(list, expr) => elem or null + // receives bounded variable '_' with the expression, and "_i" with index + // returns first element on the list for which the expr is true + addLazyFunction("first", 2, (c, t, lv) -> + { + + Value rval= lv.get(0).evalValue(c); + if (!(rval instanceof ListValue)) + throw new InternalExpressionException("First argument of 'first' function should be a list or iterator"); + Iterator iterator = ((ListValue) rval).iterator(); + LazyValue expr = lv.get(1); + //scoping + LazyValue _val = c.getVariable("_"); + LazyValue _iter = c.getVariable("_i"); + Value result = Value.NULL; + for (int i=0; iterator.hasNext(); i++) + { + Value next = iterator.next(); + String var = next.boundVariable; + next.bindTo("_"); + int seriously = i; + c.setVariable("_", (cc, tt) -> next); + c.setVariable("_i", (cc, tt) -> new NumericValue(seriously).bindTo("_i")); + if(expr.evalValue(c).getBoolean()) + { + result = next; + next.boundVariable = var; + break; + } + next.boundVariable = var; + } + //revering scope + ((ListValue) rval).fatality(); + Value whyWontYouTrustMeJava = result; + c.setVariable("_", _val); + c.setVariable("_i", _iter); + return (cc, tt) -> whyWontYouTrustMeJava; + }); + + // all(list, expr) => boolean + // receives bounded variable '_' with the expression, and "_i" with index + // returns true if expr is true for all items + addLazyFunction("all", 2, (c, t, lv) -> + { + Value rval= lv.get(0).evalValue(c); + if (!(rval instanceof ListValue)) + throw new InternalExpressionException("First argument of 'all' function should be a list or iterator"); + Iterator iterator = ((ListValue) rval).iterator(); + LazyValue expr = lv.get(1); + //scoping + LazyValue _val = c.getVariable("_"); + LazyValue _iter = c.getVariable("_i"); + LazyValue result = LazyValue.TRUE; + for (int i=0; iterator.hasNext(); i++) + { + Value next = iterator.next(); + String var = next.boundVariable; + next.bindTo("_"); + int seriously = i; + c.setVariable("_", (cc, tt) -> next); + c.setVariable("_i", (cc, tt) -> new NumericValue(seriously).bindTo("_i")); + if(!expr.evalValue(c).getBoolean()) + { + result = LazyValue.FALSE; + next.boundVariable = var; + break; + } + next.boundVariable = var; + } + //revering scope + ((ListValue) rval).fatality(); + c.setVariable("_", _val); + c.setVariable("_i", _iter); + return result; + }); + + // similar to map, but returns total number of successes + // for(list, expr, exit_expr) => success_count + // can be substituted for first and all, but first is more efficient and all doesn't require knowing list size + addLazyFunction("for", -1, (c, t, lv) -> + { + if (lv.size()<2 || lv.size()>3) + { + throw new InternalExpressionException("Incorrect number of attributes for 'for', should be 2 or 3, not "+lv.size()); + } + Value rval= lv.get(0).evalValue(c); + if (!(rval instanceof ListValue)) + throw new InternalExpressionException("Second argument of 'for' function should be a list or iterator"); + Iterator iterator = ((ListValue) rval).iterator(); + LazyValue expr = lv.get(1); + LazyValue cond = null; + if(lv.size() > 2) cond = lv.get(2); + + //scoping + LazyValue _val = c.getVariable("_"); + LazyValue _iter = c.getVariable("_i"); + int successCount = 0; + for (int i=0; iterator.hasNext(); i++) + { + Value next = iterator.next(); + String var = next.boundVariable; + next.bindTo("_"); + int seriously = i; + c.setVariable("_", (cc, tt) -> next); + c.setVariable("_i", (cc, tt) -> new NumericValue(seriously).bindTo("_i")); + if(expr.evalValue(c).getBoolean()) + successCount++; + if (cond != null && cond.evalValue(c).getBoolean()) + { + next.boundVariable = var; + break; + } + next.boundVariable = var; + } + //revering scope + ((ListValue) rval).fatality(); + c.setVariable("_", _val); + c.setVariable("_i", _iter); + long promiseWontChange = successCount; + return (cc, tt) -> new NumericValue(promiseWontChange); + }); + + + // reduce(list, expr, ?acc) => value + // reduces values in the list with expression that gets accumulator + // each iteration expr receives acc - accumulator, and '_' - current list value + // returned value is substituted to the accumulator + addLazyFunction("reduce", 3, (c, t, lv) -> + { + LazyValue expr = lv.get(1); + + Value acc = lv.get(2).evalValue(c); + Value rval= lv.get(0).evalValue(c); + if (!(rval instanceof ListValue)) + throw new InternalExpressionException("First argument of 'reduce' should be a list or iterator"); + Iterator iterator = ((ListValue) rval).iterator(); + + if (!iterator.hasNext()) + { + Value seriouslyWontChange = acc; + return (cc, tt) -> seriouslyWontChange; + } + + //scoping + LazyValue _val = c.getVariable("_"); + LazyValue _acc = c.getVariable("_a"); + + while (iterator.hasNext()) + { + Value next = iterator.next(); + String var = next.boundVariable; + next.bindTo("_"); + Value promiseWontChangeYou = acc; + c.setVariable("_a", (cc, tt) -> promiseWontChangeYou.bindTo("_a")); + c.setVariable("_", (cc, tt) -> next); + acc = expr.evalValue(c); + next.boundVariable = var; + } + //reverting scope + ((ListValue) rval).fatality(); + c.setVariable("_a", _acc); + c.setVariable("_", _val); + + Value hopeItsEnoughPromise = acc; + return (cc, tt) -> hopeItsEnoughPromise; + }); + } + + /** + * + * + *

System functions

+ *
+ *

Type conversion functions

+ *

bool(expr)

+ *

Returns a boolean context of the expression. Note that there are no true/false values in + * scarpet. true is alias of 1, and false is 0. Bool is also interpreting + * string values as boolean, which is different from other places where boolean context can be used. + * This can be used in places where API functions return string values to represent binary values

+ *
+     * bool(pi) => 1
+     * bool(false) => 0
+     * bool('') => 0
+     * bool(l()) => 0
+     * bool(l('')) => 1
+     * bool('foo') => 1
+     * bool('false') => 0
+     * bool('nulL') => 0
+     * if('false',1,0) => 1
+     * 
+ * + *

number(expr)

+ *

Returns a numeric context of the expression. + * Can be used to read numbers from strings

+ *
+     * number(null) => null
+     * number(false) => 0
+     * number('') => null
+     * number('3.14') => 3.14
+     * number(l()) => 0
+     * number(l('')) => 1
+     * number('foo') => null
+     * number('3bar') => null
+     * number('2')+number('2') => 4
+     * 
+ * + *

str(expr, params? ... )

+ * + *

Returns a formatted string representing expression. + * Accepts formatting style accepted by String.format. + * Supported types (with "%?" syntax):

+ *
    + *
  • d, o, x: integers
  • + *
  • a, e, f, g: floats
  • + *
  • b: booleans
  • + *
  • s: strings
  • + *
+ * + *
+     * str(null) => null
+     * str(false) => 0
+     * str('') => null
+     * str('3.14') => 3.14
+     * str(l()) => 0
+     * str(l('')) => 1
+     * str('foo') => null
+     * str('3bar') => null
+     * str(2)+str(2) => 22
+     * str('pi: %.2f',pi) => 'pi: 3.14'
+     * 
+ * + *
+ *

Auxiliary functions

+ * + *

length(expr)

+ *

Returns length of the expression, the length of the string, + * the length of the integer part of the number, or length of the list

+ *
+     * length(pi) => 1
+     * length(pi*pi) => 1
+     * length(pi^pi) => 2
+     * length(l()) => 0
+     * length(l(1,2,3)) => 3
+     * length('') => 0
+     * length('foo') => 3
+     * 
+ * + *

rand(expr)

+ *

returns a random number from 0.0 + * (inclusive) to expr (exclusive). + * In boolean context (in conditions, boolean functions, or bool), returns + * false if the randomly selected value is less than 1. This means that rand(2) returns true half + * of the time and rand(5) returns true for 80% (4/5) of the time

+ *
+     * map(range(10), floor(rand(10))) => [5, 8, 0, 6, 9, 3, 9, 9, 1, 8]
+     * map(range(10), bool(rand(2))) => [1, 1, 1, 0, 0, 1, 1, 0, 0, 0]
+     * map(range(10), str('%.1f',rand(_))) => [0.0, 0.4, 0.6, 1.9, 2.8, 3.8, 5.3, 2.2, 1.6, 5.6]
+     * 
+ * + *

print(expr)

+ *

prints the value of the expression to chat. + * Passes the result of the argument to the output unchanged, so print-statements can + * be weaved in code to debug programming issues

+ *
+     *     print('foo') => results in foo, prints: foo
+     *     a = 1; print(a = 5) => results in 5, prints: 5
+     *     a = 1; print(a) = 5 => results in 5, prints: 1
+     *     print('pi = '+pi) => prints: pi = 3.141592653589793
+     *     print(str('pi = %.2f',pi)) => prints: pi = 3.14
+     * 
+ * + *

sleep(expr)

+ *

Halts the execution of the program (and the game itself) for expr milliseconds. + * All in all, its better to use tick(expr) to let the game do its job while the program waits

+ *
sleep(50)
+ *

time()

+ *

Returns the number of milliseconds since 'some point', + * like Java's System.nanoTime(). It returns a float, which has 1 microsecond precision + * (0.001 ms)

+ *
+     *     start_time = time();
+     *     flip_my_world_upside_down();
+     *     print(str('this took %d milliseconds',time()-start_time))
+     * 
+ * + *
+ *

Access to variables and stored functions

+ * + *

var(expr)

+ *

Returns the variable under the name of the string value of the expression. Allows to + * manipulate variables in more programmatic manner, which allows to use local variable set with a + * hash map type key-value access, can also be used with global variables

+ *
+     *     a = 1; var('a') = 'foo'; a => a == 'foo'
+     * 
+ * + *

undef(expr)

+ *

Removes all bindings of a variable with a name of expr. + * Removes also all function definitions with that name. It can affect global variable pool, and local variable set + * for a particular function which will become invalid ........,

+ *
+     *     inc(i) -> i+1; foo = 5; inc(foo) => 6
+     *     inc(i) -> i+1; foo = 5; undef('foo'); inc(foo) => 1
+     *     inc(i) -> i+1; foo = 5; undef('inc'); undef('foo'); inc(foo) => Error: Function inc is not defined yet at pos 53
+     *     undef('pi')  => bad idea - removes hidden variable holding the pi value
+     *     undef('true')  => even worse idea, unbinds global true value, all references to true would now refer to the default 0
+     * 
+ *

vars(prefix)

+ *

It returns all names of variables from local scope (if prefix does not start with 'global') + * or global variables (otherwise). + * Here is a larger example that uses combination of vars and var functions to + * be used for object counting

+ *
+     * /script run
+     * $ count_blocks(ent) -> (
+     * $   l(cx, cy, cz) = query(ent, 'pos');
+     * $   scan(cx, cy, cz, 16, 16, 16, var('count_'+_) += 1);
+     * $   for ( sort_key( vars('count_'), -var(_)),
+     * $     print(str( '%s: %d', slice(_,6), var(_) ))
+     * $   )
+     * $ )
+     *
+     * /script run count_blocks(player())
+     * 
+ * + *
+ */ + public void SystemFunctions() + { + addLazyFunction("bool", 1, (c, t, lv) -> { + Value v = lv.get(0).evalValue(c, Context.BOOLEAN); + if (v instanceof StringValue) + { + String str = v.getString(); + if ("false".equalsIgnoreCase(str) || "null".equalsIgnoreCase(str)) + { + return (cc, tt) -> Value.FALSE; + } + } + Value retval = new NumericValue(v.getBoolean()); + return (cc, tt) -> retval; + }); + addUnaryFunction("number", v -> { + if (v instanceof NumericValue) + return v; + double res = v.readNumber(); + if (Double.isNaN(res)) + return Value.NULL; + return new NumericValue(v.readNumber()); + }); + addFunction("str", lv -> + { + if (lv.size() == 0) + throw new InternalExpressionException("str requires at least one argument"); + String format = lv.get(0).getString(); + if (lv.size() == 1) + return new StringValue(format); + List args = new ArrayList<>(); + Matcher m = formatPattern.matcher(format); + int argIndex = 1; + for (int i = 0, len = format.length(); i < len; ) { + if (m.find(i)) { + // Anything between the start of the string and the beginning + // of the format specifier is either fixed text or contains + // an invalid format string. + // [[scarpet]] but we skip it and let the String.format fail + char fmt = m.group(6).toLowerCase().charAt(0); + if (fmt == 's') + { + if (argIndex >= lv.size()) + throw new InternalExpressionException("Not enough arguments for "+m.group(0)); + args.add(lv.get(argIndex).getString()); + argIndex++; + } + else if (fmt == 'd' || fmt == 'o' || fmt == 'x') + { + if (argIndex >= lv.size()) + throw new InternalExpressionException("Not enough arguments for "+m.group(0)); + args.add(lv.get(argIndex).readInteger()); + argIndex++; + } + else if (fmt == 'a' || fmt == 'e' || fmt == 'f' || fmt == 'g') + { + if (argIndex >= lv.size()) + throw new InternalExpressionException("Not enough arguments for "+m.group(0)); + args.add(lv.get(argIndex).readNumber()); + argIndex++; + } + else if (fmt == 'b') + { + if (argIndex >= lv.size()) + throw new InternalExpressionException("Not enough arguments for "+m.group(0)); + args.add(lv.get(argIndex).getBoolean()); + argIndex++; + } + else if (fmt == '%') + { + //skip /%% + ; + } + else + { + throw new InternalExpressionException("format not supported: "+m.group(6)); + } + + i = m.end(); + } else { + // No more valid format specifiers. Check for possible invalid + // format specifiers. + // [[scarpet]] but we skip it and let the String.format fail + break; + } + } + try + { + return new StringValue(String.format(format, args.toArray())); + } + catch (IllegalFormatException ife) + { + throw new InternalExpressionException("Illegal string format: "+ife.getMessage()); + } + }); + + addUnaryFunction("length", v -> new NumericValue(v.length())); + addLazyFunction("rand", 1, (c, t, lv) -> { + Value argument = lv.get(0).evalValue(c); + if (argument instanceof ListValue) + { + List list = ((ListValue) argument).getItems(); + Value retval = list.get(randomizer.nextInt(list.size())); + return (cc, tt) -> retval; + } + if (t == Context.BOOLEAN) + { + double rv = NumericValue.asNumber(argument).getDouble()*randomizer.nextFloat(); + Value retval = rv<1.0D?Value.FALSE:Value.TRUE; + return (cc, tt) -> retval; + } + Value retval = new NumericValue(NumericValue.asNumber(argument).getDouble()*randomizer.nextDouble()); + return (cc, tt) -> retval; + }); + + addUnaryFunction("print", (v) -> + { + System.out.println(v.getString()); + return v; // pass through for variables + }); + addUnaryFunction("sleep", (v) -> + { + long time = NumericValue.asNumber(v).getLong(); + try + { + Thread.sleep(time); + Thread.yield(); + } + catch (InterruptedException ignored) { } + return v; // pass through for variables + }); + addLazyFunction("time", 0, (c, t, lv) -> + { + Value time = new NumericValue((System.nanoTime() / 1000) / 1000.0); + return (cc, tt) -> time; + }); + + addLazyFunction("var", 1, (c, t, lv) -> { + String varname = lv.get(0).evalValue(c).getString(); + if (!c.isAVariable(varname)) + c.setVariable(varname, (_c, _t ) -> Value.ZERO.reboundedTo(varname)); + return c.getVariable(varname); + }); + + addLazyFunction("undef", 1, (c, t, lv) -> + { + String varname = lv.get(0).evalValue(c).getString(); + if (varname.startsWith("_")) + throw new InternalExpressionException("Cannot replace local built-in variables, i.e. those that start with '_'"); + if (varname.endsWith("*")) + { + varname = varname.replaceAll("\\*+$", ""); + for (String key: c.host.globalFunctions.keySet()) + { + if (key.startsWith(varname)) c.host.globalFunctions.remove(key); + } + for (String key: c.host.globalVariables.keySet()) + { + if (key.startsWith(varname)) c.host.globalVariables.remove(key); + } + c.clearAll(varname); + } + else + { + c.host.globalFunctions.remove(varname); + c.delVariable(varname); + } + return (cc, tt) -> Value.NULL; + }); + + + addLazyFunction("vars", 1, (c, t, lv) -> { + String prefix = lv.get(0).evalValue(c).getString(); + List values = new ArrayList<>(); + if (prefix.startsWith("global")) + { + for (String k: c.host.globalVariables.keySet()) + { + if (k.startsWith(prefix)) + values.add(new StringValue(k)); + } + } + else + { + for (String k: c.getAllVariableNames()) + { + if (k.startsWith(prefix)) + values.add(new StringValue(k)); + } + } + Value retval = ListValue.wrap(values); + return (cc, tt) -> retval; + }); + + } + + static Expression none = new Expression("null"); + /** + * @param expression . + */ + public Expression(String expression) + { + expression = expression.trim().replaceAll(";+$", ""); + this.expression = expression.replaceAll("\\$", "\n").trim(); + VariablesAndConstants(); + UserDefinedFunctionsAndControlFlow(); + Operators(); + ArithmeticOperations(); + SystemFunctions(); + ListsLoopsAndHigherOrderFunctions(); + } + + private List shuntingYard(String expression) + { + List outputQueue = new ArrayList<>(); + Stack stack = new Stack<>(); + + Tokenizer tokenizer = new Tokenizer(this, expression); + + Tokenizer.Token lastFunction = null; + Tokenizer.Token previousToken = null; + while (tokenizer.hasNext()) + { + Tokenizer.Token token; + try + { + token = tokenizer.next(); + } + catch (StringIndexOutOfBoundsException e) + { + throw new ExpressionException("Script ended prematurely"); + } + switch (token.type) + { + case STRINGPARAM: + //stack.push(token); // changed that so strings are treated like literals + //break; + case LITERAL: + case HEX_LITERAL: + if (previousToken != null && ( + previousToken.type == Tokenizer.Token.TokenType.LITERAL || + previousToken.type == Tokenizer.Token.TokenType.HEX_LITERAL || + previousToken.type == Tokenizer.Token.TokenType.STRINGPARAM)) + { + throw new ExpressionException(this, token, "Missing operator"); + } + outputQueue.add(token); + break; + case VARIABLE: + outputQueue.add(token); + break; + case FUNCTION: + stack.push(token); + lastFunction = token; + break; + case COMMA: + if (previousToken != null && previousToken.type == Tokenizer.Token.TokenType.OPERATOR) + { + throw new ExpressionException(this, previousToken, "Missing parameter(s) for operator "); + } + while (!stack.isEmpty() && stack.peek().type != Tokenizer.Token.TokenType.OPEN_PAREN) + { + outputQueue.add(stack.pop()); + } + if (stack.isEmpty()) + { + if (lastFunction == null) + { + throw new ExpressionException(this, token, "Unexpected comma"); + } + else + { + throw new ExpressionException(this, lastFunction, "Parse error for function"); + } + } + break; + case OPERATOR: + { + if (previousToken != null + && (previousToken.type == Tokenizer.Token.TokenType.COMMA || previousToken.type == Tokenizer.Token.TokenType.OPEN_PAREN)) + { + throw new ExpressionException(this, token, "Missing parameter(s) for operator '" + token+"'"); + } + ILazyOperator o1 = operators.get(token.surface); + if (o1 == null) + { + throw new ExpressionException(this, token, "Unknown operator '" + token + "'"); + } + + shuntOperators(outputQueue, stack, o1); + stack.push(token); + break; + } + case UNARY_OPERATOR: + { + if (previousToken != null && previousToken.type != Tokenizer.Token.TokenType.OPERATOR + && previousToken.type != Tokenizer.Token.TokenType.COMMA && previousToken.type != Tokenizer.Token.TokenType.OPEN_PAREN) + { + throw new ExpressionException(this, token, "Invalid position for unary operator " + token ); + } + ILazyOperator o1 = operators.get(token.surface); + if (o1 == null) + { + throw new ExpressionException(this, token, "Unknown unary operator '" + token.surface.substring(0, token.surface.length() - 1) + "'"); + } + + shuntOperators(outputQueue, stack, o1); + stack.push(token); + break; + } + case OPEN_PAREN: + if (previousToken != null) + { + if (previousToken.type == Tokenizer.Token.TokenType.LITERAL || previousToken.type == Tokenizer.Token.TokenType.CLOSE_PAREN + || previousToken.type == Tokenizer.Token.TokenType.VARIABLE + || previousToken.type == Tokenizer.Token.TokenType.HEX_LITERAL) + { + // Implicit multiplication, e.g. 23(a+b) or (a+b)(a-b) + Tokenizer.Token multiplication = new Tokenizer.Token(); + multiplication.append("*"); + multiplication.type = Tokenizer.Token.TokenType.OPERATOR; + stack.push(multiplication); + } + // if the ( is preceded by a valid function, then it + // denotes the start of a parameter list + if (previousToken.type == Tokenizer.Token.TokenType.FUNCTION) + { + outputQueue.add(token); + } + } + stack.push(token); + break; + case CLOSE_PAREN: + if (previousToken != null && previousToken.type == Tokenizer.Token.TokenType.OPERATOR) + { + throw new ExpressionException(this, previousToken, "Missing parameter(s) for operator " + previousToken); + } + while (!stack.isEmpty() && stack.peek().type != Tokenizer.Token.TokenType.OPEN_PAREN) + { + outputQueue.add(stack.pop()); + } + if (stack.isEmpty()) + { + throw new ExpressionException("Mismatched parentheses"); + } + stack.pop(); + if (!stack.isEmpty() && stack.peek().type == Tokenizer.Token.TokenType.FUNCTION) + { + outputQueue.add(stack.pop()); + } + } + previousToken = token; + } + + while (!stack.isEmpty()) + { + Tokenizer.Token element = stack.pop(); + if (element.type == Tokenizer.Token.TokenType.OPEN_PAREN || element.type == Tokenizer.Token.TokenType.CLOSE_PAREN) + { + throw new ExpressionException(this, element, "Mismatched parentheses"); + } + outputQueue.add(element); + } + return outputQueue; + } + + private void shuntOperators(List outputQueue, Stack stack, ILazyOperator o1) + { + Tokenizer.Token nextToken = stack.isEmpty() ? null : stack.peek(); + while (nextToken != null + && (nextToken.type == Tokenizer.Token.TokenType.OPERATOR + || nextToken.type == Tokenizer.Token.TokenType.UNARY_OPERATOR) + && ((o1.isLeftAssoc() && o1.getPrecedence() <= operators.get(nextToken.surface).getPrecedence()) + || (o1.getPrecedence() < operators.get(nextToken.surface).getPrecedence()))) + { + outputQueue.add(stack.pop()); + nextToken = stack.isEmpty() ? null : stack.peek(); + } + } + + Value eval(Context c) + { + return eval(c, Context.NONE); + } + private Value eval(Context c, Integer expectedType) + { + if (ast == null) + { + ast = getAST(); + } + return evalValue(() -> ast, c, expectedType); + } + + static Value evalValue(Supplier exprProvider, Context c, Integer expectedType) + { + try + { + return exprProvider.get().evalValue(c, expectedType); + } + catch (ExitStatement exit) + { + return exit.retval; + } + catch (StackOverflowError ignored) + { + throw new ExpressionException("Your thoughts are too deep"); + } + catch (InternalExpressionException exc) + { + throw new ExpressionException("Your expression result is incorrect:"+exc.getMessage()); + } + catch (ArithmeticException exc) + { + throw new ExpressionException("The final result is incorrect, "+exc.getMessage()); + } + } + + private LazyValue getAST() + { + Stack stack = new Stack<>(); + List rpn = shuntingYard(this.expression); + validate(rpn); + for (final Tokenizer.Token token : rpn) + { + switch (token.type) + { + case UNARY_OPERATOR: + { + final LazyValue value = stack.pop(); + LazyValue result = (c, t) -> operators.get(token.surface).lazyEval(c, t, this, token, value, null).evalValue(c); + stack.push(result); + break; + } + case OPERATOR: + final LazyValue v1 = stack.pop(); + final LazyValue v2 = stack.pop(); + LazyValue result = (c,t) -> operators.get(token.surface).lazyEval(c, t,this, token, v2, v1).evalValue(c); + stack.push(result); + break; + case VARIABLE: + stack.push((c, t) -> + { + if (!c.isAVariable(token.surface)) // new variable + { + c.setVariable(token.surface, (cc, tt ) -> Value.ZERO.reboundedTo(token.surface)); + } + LazyValue lazyVariable = c.getVariable(token.surface); + return lazyVariable.evalValue(c); + }); + break; + case FUNCTION: + String name = token.surface.toLowerCase(Locale.ROOT); + ILazyFunction f; + ArrayList p; + boolean isKnown = functions.containsKey(name); // globals will be evaluated lazily, not at compile time via . + if (isKnown) + { + f = functions.get(name); + p = new ArrayList<>(!f.numParamsVaries() ? f.getNumParams() : 0); + } + else // potentially unknown function or just unknown function + { + f = functions.get("."); + p = new ArrayList<>(); + } + // pop parameters off the stack until we hit the start of + // this function's parameter list + while (!stack.isEmpty() && stack.peek() != LazyValue.PARAMS_START) + { + p.add(0, stack.pop()); + } + if (!isKnown) p.add( (c, t) -> new StringValue(name)); + + if (stack.peek() == LazyValue.PARAMS_START) + { + stack.pop(); + } + + stack.push((c, t) -> f.lazyEval(c, t, this, token, p).evalValue(c)); + break; + case OPEN_PAREN: + stack.push(LazyValue.PARAMS_START); + break; + case LITERAL: + stack.push((c, t) -> + { + try + { + return new NumericValue(token.surface); + } + catch (NumberFormatException exception) + { + throw new ExpressionException(this, token, "Not a number"); + } + }); + break; + case STRINGPARAM: + stack.push((c, t) -> new StringValue(token.surface) ); // was originally null + break; + case HEX_LITERAL: + stack.push((c, t) -> new NumericValue(new BigInteger(token.surface.substring(2), 16).doubleValue())); + break; + default: + throw new ExpressionException(this, token, "Unexpected token '" + token.surface + "'"); + } + } + return stack.pop(); + } + + private void validate(List rpn) + { + /*- + * Thanks to Norman Ramsey: + * http://http://stackoverflow.com/questions/789847/postfix-notation-validation + */ + // each push on to this stack is a new function scope, with the value of + // each + // layer on the stack being the count of the number of parameters in + // that scope + Stack stack = new Stack<>(); + + // push the 'global' scope + stack.push(0); + + for (final Tokenizer.Token token : rpn) + { + switch (token.type) + { + case UNARY_OPERATOR: + if (stack.peek() < 1) + { + throw new ExpressionException(this, token, "Missing parameter(s) for operator " + token); + } + break; + case OPERATOR: + if (stack.peek() < 2) + { + if (token.surface.equalsIgnoreCase(";")) + { + throw new ExpressionException(this, token, "Unnecessary semicolon"); + } + throw new ExpressionException(this, token, "Missing parameter(s) for operator " + token); + } + // pop the operator's 2 parameters and add the result + stack.set(stack.size() - 1, stack.peek() - 2 + 1); + break; + case FUNCTION: + ILazyFunction f = functions.get(token.surface.toLowerCase(Locale.ROOT));// don't validate global - userdef functions + int numParams = stack.pop(); + if (f != null && !f.numParamsVaries() && numParams != f.getNumParams()) + { + throw new ExpressionException(this, token, "Function " + token + " expected " + f.getNumParams() + " parameters, got " + numParams); + } + if (stack.size() <= 0) + { + throw new ExpressionException(this, token, "Too many function calls, maximum scope exceeded"); + } + // push the result of the function + stack.set(stack.size() - 1, stack.peek() + 1); + break; + case OPEN_PAREN: + stack.push(0); + break; + default: + stack.set(stack.size() - 1, stack.peek() + 1); + } + } + + if (stack.size() > 1) + { + throw new ExpressionException("Too many unhandled function parameter lists"); + } + else if (stack.peek() > 1) + { + throw new ExpressionException("Too many numbers or variables"); + } + else if (stack.peek() < 1) + { + throw new ExpressionException("Empty expression"); + } + } + +} diff --git a/src/main/java/carpet/script/ExpressionInspector.java b/src/main/java/carpet/script/ExpressionInspector.java new file mode 100644 index 00000000..a9edc887 --- /dev/null +++ b/src/main/java/carpet/script/ExpressionInspector.java @@ -0,0 +1,20 @@ +package carpet.script; + +import java.util.List; + +/** + * sole purpose of this package is to provide public access to package-private methods of Expression and CarpetExpression + * classes so they don't leave garbage in javadocs + */ +public class ExpressionInspector +{ + public static List Expression_getExpressionSnippet(Tokenizer.Token token, Expression expr) + { + return Expression.getExpressionSnippet(token, expr); + } + + public static String Expression_getName(Expression e) + { + return e.getName(); + } +} diff --git a/src/main/java/carpet/script/Fluff.java b/src/main/java/carpet/script/Fluff.java new file mode 100644 index 00000000..c8d55378 --- /dev/null +++ b/src/main/java/carpet/script/Fluff.java @@ -0,0 +1,195 @@ +package carpet.script; + +import carpet.script.exception.ExpressionException; +import carpet.script.exception.InternalExpressionException; +import carpet.script.value.Value; + +import java.util.ArrayList; +import java.util.List; + +public abstract class Fluff +{ + @FunctionalInterface + public interface TriFunction { R apply(A a, B b, C c); } + + @FunctionalInterface + public interface QuadFunction { R apply(A a, B b, C c, D d);} + + @FunctionalInterface + public interface QuinnFunction { R apply(A a, B b, C c, D d, E e);} + + @FunctionalInterface + public interface SexFunction { R apply(A a, B b, C c, D d, E e, F f);} + + public interface ILazyFunction + { + int getNumParams(); + + boolean numParamsVaries(); + + LazyValue lazyEval(Context c, Integer type, Expression expr, Tokenizer.Token token, List lazyParams); + // lazy function has a chance to change execution based on contxt + } + + public interface IFunction extends ILazyFunction + { + Value eval(List parameters); + } + + public interface ILazyOperator + { + int getPrecedence(); + + boolean isLeftAssoc(); + + LazyValue lazyEval(Context c, Integer type, Expression e, Tokenizer.Token t, LazyValue v1, LazyValue v2); + } + + public interface IOperator extends ILazyOperator + { + Value eval(Value v1, Value v2); + } + + public abstract static class AbstractLazyFunction implements ILazyFunction + { + protected String name; + int numParams; + + AbstractLazyFunction(int numParams) + { + this.numParams = numParams; + } + + + public String getName() { + return name; + } + + public int getNumParams() { + return numParams; + } + + public boolean numParamsVaries() { + return numParams < 0; + } + } + + public abstract static class AbstractFunction extends AbstractLazyFunction implements IFunction + { + AbstractFunction(int numParams) { + super(numParams); + } + + @Override + public LazyValue lazyEval(Context cc, Integer type, Expression e, Tokenizer.Token t, final List lazyParams) + { + try + { + return new LazyValue() + { // eager evaluation always ignores the required type and evals params by none default + private List params; + + public Value evalValue(Context c, Integer type) { + return AbstractFunction.this.eval(getParams(c)); + } + + private List getParams(Context c) { + if (params == null) { + params = new ArrayList<>(); + for (LazyValue lazyParam : lazyParams) { + params.add(lazyParam.evalValue(c)); // none type default by design + } + } + return params; + } + }; + } + catch (InternalExpressionException exc) + { + throw new ExpressionException(e, t, exc.getMessage()); + } + } + } + + public abstract static class AbstractLazyOperator implements ILazyOperator + { + int precedence; + + boolean leftAssoc; + + AbstractLazyOperator(int precedence, boolean leftAssoc) { + this.precedence = precedence; + this.leftAssoc = leftAssoc; + } + + public int getPrecedence() { + return precedence; + } + + public boolean isLeftAssoc() { + return leftAssoc; + } + + } + + public abstract static class AbstractOperator extends AbstractLazyOperator implements IOperator + { + + AbstractOperator(int precedence, boolean leftAssoc) { + super(precedence, leftAssoc); + } + + @Override + public LazyValue lazyEval(Context c_ignored, Integer type, Expression e, Tokenizer.Token t, final LazyValue v1, final LazyValue v2) + { + try + { + return (c, type_ignored) -> AbstractOperator.this.eval(v1.evalValue(c), v2.evalValue(c)); + } + catch (InternalExpressionException exc) + { + throw new ExpressionException(e, t, exc.getMessage()); + } + catch (ArithmeticException exc) + { + throw new ExpressionException(e, t, "Your math is wrong, "+exc.getMessage()); + } + } + } + + public abstract static class AbstractUnaryOperator extends AbstractOperator + { + AbstractUnaryOperator(int precedence, boolean leftAssoc) { + super(precedence, leftAssoc); + } + + @Override + public LazyValue lazyEval(Context cc, Integer type, Expression e, Tokenizer.Token t, final LazyValue v1, final LazyValue v2) + { + try + { + if (v2 != null) + { + throw new ExpressionException(e, t, "Did not expect a second parameter for unary operator"); + } + return (c, ignored_type) -> AbstractUnaryOperator.this.evalUnary(v1.evalValue(c)); + } + catch (InternalExpressionException exc) // might not actually throw it + { + throw new ExpressionException(e, t, exc.getMessage()); + } + catch (ArithmeticException exc) + { + throw new ExpressionException(e, t, "Your math is wrong, "+exc.getMessage()); + } + } + + @Override + public Value eval(Value v1, Value v2) + { + throw new ExpressionException("Shouldn't end up here"); + } + + public abstract Value evalUnary(Value v1); + } +} diff --git a/src/main/java/carpet/script/LazyValue.java b/src/main/java/carpet/script/LazyValue.java new file mode 100644 index 00000000..8a7d8f43 --- /dev/null +++ b/src/main/java/carpet/script/LazyValue.java @@ -0,0 +1,24 @@ +package carpet.script; + +import carpet.script.value.Value; + +/** LazyNumber interface created for lazily evaluated functions */ +@FunctionalInterface +public interface LazyValue +{ + LazyValue FALSE = (c, t) -> Value.FALSE; + LazyValue TRUE = (c, t) -> Value.TRUE; + LazyValue NULL = (c, t) -> Value.NULL; + LazyValue ZERO = (c, t) -> Value.ZERO; + /** + * The Value representation of the left parenthesis, used for parsing + * varying numbers of function parameters. + */ + LazyValue PARAMS_START = (c, t) -> null; + + Value evalValue(Context c, Integer type); + + default Value evalValue(Context c){ + return evalValue(c, 0); + } +} diff --git a/src/main/java/carpet/script/ScriptHost.java b/src/main/java/carpet/script/ScriptHost.java new file mode 100644 index 00000000..14882d95 --- /dev/null +++ b/src/main/java/carpet/script/ScriptHost.java @@ -0,0 +1,158 @@ +package carpet.script; + +import carpet.CarpetServer; +import carpet.script.exception.ExpressionException; +import carpet.script.value.NumericValue; +import carpet.script.value.StringValue; +import carpet.script.value.Value; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.util.math.BlockPos; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; + +import static java.lang.Math.max; + +public class ScriptHost +{ + + public final Map globalFunctions = new HashMap<>(); + + public final Map globalVariables = new HashMap<>(); + + private String name; + public String getName() {return name;} + + public ScriptHost(String name) + { + this.name = name; + globalVariables.put("euler", (c, t) -> Expression.euler); + globalVariables.put("pi", (c, t) -> Expression.PI); + globalVariables.put("null", (c, t) -> Value.NULL); + globalVariables.put("true", (c, t) -> Value.TRUE); + globalVariables.put("false", (c, t) -> Value.FALSE); + + //special variables for second order functions so we don't need to check them all the time + globalVariables.put("_", (c, t) -> Value.ZERO); + globalVariables.put("_i", (c, t) -> Value.ZERO); + globalVariables.put("_a", (c, t) -> Value.ZERO); + } + public Expression getExpressionForFunction(String name) + { + return globalFunctions.get(name).getExpression(); + } + public Tokenizer.Token getTokenForFunction(String name) + { + return globalFunctions.get(name).getToken(); + } + + public List getPublicFunctions() + { + return globalFunctions.keySet().stream().filter((str) -> !str.startsWith("_")).collect(Collectors.toList()); + } + public List getAvailableFunctions() + { + return globalFunctions.keySet().stream().filter((str) -> !str.startsWith("__")).collect(Collectors.toList()); + } + + public String call(ServerCommandSource source, String call, List coords, String arg) + { + if (CarpetServer.scriptServer.stopAll) + return "SCRIPTING PAUSED"; + UserDefinedFunction acf = globalFunctions.get(call); + if (acf == null) + return "UNDEFINED"; + List argv = new ArrayList<>(); + if (coords != null) + for (Integer i: coords) + argv.add( (c, t) -> new NumericValue(i)); + String sign = ""; + for (Tokenizer.Token tok : Tokenizer.simplepass(arg)) + { + switch (tok.type) + { + case VARIABLE: + if (globalVariables.containsKey(tok.surface.toLowerCase(Locale.ROOT))) + { + argv.add(globalVariables.get(tok.surface.toLowerCase(Locale.ROOT))); + break; + } + case STRINGPARAM: + argv.add((c, t) -> new StringValue(tok.surface)); + sign = ""; + break; + + case LITERAL: + try + { + String finalSign = sign; + argv.add((c, t) ->new NumericValue(finalSign+tok.surface)); + sign = ""; + } + catch (NumberFormatException exception) + { + return "Fail: "+sign+tok.surface+" seems like a number but it is not a number. Use quotes to ensure its a string"; + } + break; + case HEX_LITERAL: + try + { + String finalSign = sign; + argv.add((c, t) -> new NumericValue(new BigInteger(finalSign+tok.surface.substring(2), 16).doubleValue())); + sign = ""; + } + catch (NumberFormatException exception) + { + return "Fail: "+sign+tok.surface+" seems like a number but it is not a number. Use quotes to ensure its a string"; + } + break; + case OPERATOR: + case UNARY_OPERATOR: + if ((tok.surface.equals("-") || tok.surface.equals("-u")) && sign.isEmpty()) + { + sign = "-"; + } + else + { + return "Fail: operators, like " + tok.surface + " are not allowed in invoke"; + } + break; + case FUNCTION: + return "Fail: passing functions like "+tok.surface+"() to invoke is not allowed"; + case OPEN_PAREN: + case COMMA: + case CLOSE_PAREN: + return "Fail: "+tok.surface+" is not allowed in invoke"; + } + } + List args = acf.getArguments(); + if (argv.size() != args.size()) + { + String error = "Fail: stored function "+call+" takes "+args.size()+" arguments, not "+argv.size()+ ":\n"; + for (int i = 0; i < max(argv.size(), args.size()); i++) + { + error += (i "+(i acf.lazyEval(context, Context.VOID, acf.expression, acf.token, argv), + context, + Context.VOID + ).getString(); + } + catch (ExpressionException e) + { + return e.getMessage(); + } + } +} diff --git a/src/main/java/carpet/script/Tokenizer.java b/src/main/java/carpet/script/Tokenizer.java new file mode 100644 index 00000000..f9196e21 --- /dev/null +++ b/src/main/java/carpet/script/Tokenizer.java @@ -0,0 +1,314 @@ +package carpet.script; + +import carpet.script.exception.ExpressionException; +import carpet.script.exception.InternalExpressionException; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Expression tokenizer that allows to iterate over a {@link String} + * expression token by token. Blank characters will be skipped. + */ +public class Tokenizer implements Iterator +{ + + /** What character to use for decimal separators. */ + private static final char decimalSeparator = '.'; + /** What character to use for minus sign (negative values). */ + private static final char minusSign = '-'; + /** Actual position in expression string. */ + private int pos = 0; + private int lineno = 0; + private int linepos = 0; + + + /** The original input expression. */ + private String input; + /** The previous token or null if none. */ + private Token previousToken; + + private Expression expression; + + Tokenizer(Expression expr, String input) + { + this.input = input; + this.expression = expr; + } + + + + @Override + public boolean hasNext() + { + return (pos < input.length()); + } + + /** + * Peek at the next character, without advancing the iterator. + * + * @return The next character or character 0, if at end of string. + */ + private char peekNextChar() + { + return (pos < (input.length() - 1)) ? input.charAt(pos + 1) : 0; + } + + private boolean isHexDigit(char ch) + { + return ch == 'x' || ch == 'X' || (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') + || (ch >= 'A' && ch <= 'F'); + } + + public static List simplepass(String input) + { + Tokenizer tok = new Tokenizer(null, input); + List res = new ArrayList<>(); + while (tok.hasNext()) res.add(tok.next()); + return res; + } + + @Override + public Token next() + { + Token token = new Token(); + + if (pos >= input.length()) + { + return previousToken = null; + } + char ch = input.charAt(pos); + while (Character.isWhitespace(ch) && pos < input.length()) + { + linepos++; + if (ch=='\n') + { + lineno++; + linepos = 0; + } + ch = input.charAt(++pos); + } + token.pos = pos; + token.lineno = lineno; + token.linepos = linepos; + + boolean isHex = false; + + if (Character.isDigit(ch) || (ch == decimalSeparator && Character.isDigit(peekNextChar()))) + { + if (ch == '0' && (peekNextChar() == 'x' || peekNextChar() == 'X')) + isHex = true; + while ((isHex + && isHexDigit( + ch)) + || (Character.isDigit(ch) || ch == decimalSeparator || ch == 'e' || ch == 'E' + || (ch == minusSign && token.length() > 0 + && ('e' == token.charAt(token.length() - 1) + || 'E' == token.charAt(token.length() - 1))) + || (ch == '+' && token.length() > 0 + && ('e' == token.charAt(token.length() - 1) + || 'E' == token.charAt(token.length() - 1)))) + && (pos < input.length())) + { + token.append(input.charAt(pos++)); + linepos++; + ch = pos == input.length() ? 0 : input.charAt(pos); + } + token.type = isHex ? Token.TokenType.HEX_LITERAL : Token.TokenType.LITERAL; + } + else if (ch == '\'') + { + pos++; + linepos++; + if (pos == input.length()) + { + token.type = Token.TokenType.STRINGPARAM; + if (expression != null) + throw new ExpressionException(this.expression, token, "Program truncated"); + } + ch = input.charAt(pos); + while (ch != '\'') + { + if (ch == '\\') + { + pos++; + linepos++; + if (pos == input.length()) + { + token.type = Token.TokenType.STRINGPARAM; + if (expression != null) + throw new ExpressionException(this.expression, token, "Program truncated"); + } + } + token.append(input.charAt(pos++)); + linepos++; + ch = pos == input.length() ? 0 : input.charAt(pos); + } + pos++; + linepos++; + token.type = Token.TokenType.STRINGPARAM; + } + else if (Character.isLetter(ch) || "_".indexOf(ch) >= 0) + { + while ((Character.isLetter(ch) || Character.isDigit(ch) || "_".indexOf(ch) >= 0 + || token.length() == 0 && "_".indexOf(ch) >= 0) && (pos < input.length())) + { + token.append(input.charAt(pos++)); + linepos++; + ch = pos == input.length() ? 0 : input.charAt(pos); + } + // Remove optional white spaces after function or variable name + if (Character.isWhitespace(ch)) + { + while (Character.isWhitespace(ch) && pos < input.length()) + { + ch = input.charAt(pos++); + linepos++; + if (ch=='\n') + { + lineno++; + linepos = 0; + } + } + pos--; + linepos--; + } + token.type = ch == '(' ? Token.TokenType.FUNCTION : Token.TokenType.VARIABLE; + } + else if (ch == '(' || ch == ')' || ch == ',') + { + if (ch == '(') + { + token.type = Token.TokenType.OPEN_PAREN; + } + else if (ch == ')') + { + token.type = Token.TokenType.CLOSE_PAREN; + } + else + { + token.type = Token.TokenType.COMMA; + } + token.append(ch); + pos++; + linepos++; + if (expression != null && previousToken != null && previousToken.type == Token.TokenType.OPERATOR && (ch == ')' || ch == ',') ) + { + if (previousToken.surface.equalsIgnoreCase(";")) + throw new ExpressionException(this.expression, previousToken, + "Cannot have semicolon at the end of the expression"); + throw new ExpressionException(this.expression, previousToken, + "Can't have operator "+previousToken.surface+" at the end of a subexpression"); + } + } + else + { + String greedyMatch = ""; + int initialPos = pos; + int initialLinePos = linepos; + ch = input.charAt(pos); + int validOperatorSeenUntil = -1; + while (!Character.isLetter(ch) && !Character.isDigit(ch) && "_".indexOf(ch) < 0 + && !Character.isWhitespace(ch) && ch != '(' && ch != ')' && ch != ',' + && (pos < input.length())) + { + greedyMatch += ch; + pos++; + linepos++; + if (Expression.none.isAnOperator(greedyMatch)) + { + validOperatorSeenUntil = pos; + } + ch = pos == input.length() ? 0 : input.charAt(pos); + } + if (validOperatorSeenUntil != -1) + { + token.append(input.substring(initialPos, validOperatorSeenUntil)); + pos = validOperatorSeenUntil; + linepos = initialLinePos+validOperatorSeenUntil-initialPos; + } + else + { + token.append(greedyMatch); + } + + if (previousToken == null || previousToken.type == Token.TokenType.OPERATOR + || previousToken.type == Token.TokenType.OPEN_PAREN || previousToken.type == Token.TokenType.COMMA) + { + token.surface += "u"; + token.type = Token.TokenType.UNARY_OPERATOR; + } + else + { + token.type = Token.TokenType.OPERATOR; + } + } + if (expression != null && previousToken != null && + ( + token.type == Token.TokenType.LITERAL || + token.type == Token.TokenType.HEX_LITERAL || + token.type == Token.TokenType.VARIABLE || + token.type == Token.TokenType.STRINGPARAM || + token.type == Token.TokenType.FUNCTION + ) &&( + previousToken.type == Token.TokenType.VARIABLE || + previousToken.type == Token.TokenType.FUNCTION || + previousToken.type == Token.TokenType.LITERAL || + previousToken.type == Token.TokenType.CLOSE_PAREN || + previousToken.type == Token.TokenType.HEX_LITERAL || + previousToken.type == Token.TokenType.STRINGPARAM + ) + ) + { + throw new ExpressionException(this.expression, previousToken, "'"+token.surface +"' is not allowed after '"+previousToken.surface+"'"); + } + return previousToken = token; + } + + @Override + public void remove() + { + throw new InternalExpressionException("remove() not supported"); + } + + public static class Token + { + enum TokenType + { + VARIABLE, FUNCTION, LITERAL, OPERATOR, UNARY_OPERATOR, + OPEN_PAREN, COMMA, CLOSE_PAREN, HEX_LITERAL, STRINGPARAM + } + public String surface = ""; + public TokenType type; + public int pos; + public int linepos; + public int lineno; + + public void append(char c) + { + surface += c; + } + + public void append(String s) + { + surface += s; + } + + public char charAt(int pos) + { + return surface.charAt(pos); + } + + public int length() + { + return surface.length(); + } + + @Override + public String toString() + { + return surface; + } + } +} diff --git a/src/main/java/carpet/script/UserDefinedFunction.java b/src/main/java/carpet/script/UserDefinedFunction.java new file mode 100644 index 00000000..c2780903 --- /dev/null +++ b/src/main/java/carpet/script/UserDefinedFunction.java @@ -0,0 +1,29 @@ +package carpet.script; + +import java.util.List; + +public abstract class UserDefinedFunction extends Fluff.AbstractLazyFunction implements Fluff.ILazyFunction +{ + protected List arguments; + protected Expression expression; + protected Tokenizer.Token token; + UserDefinedFunction(List args, Expression expr, Tokenizer.Token t) + { + super(args.size()); + arguments = args; + expression = expr; + token = t; + } + public List getArguments() + { + return arguments; + } + public Expression getExpression() + { + return expression; + } + public Tokenizer.Token getToken() + { + return token; + } +} diff --git a/src/main/java/carpet/script/bundled/CameraPathModule.java b/src/main/java/carpet/script/bundled/CameraPathModule.java new file mode 100644 index 00000000..f2aca079 --- /dev/null +++ b/src/main/java/carpet/script/bundled/CameraPathModule.java @@ -0,0 +1,334 @@ +package carpet.script.bundled; + +public class CameraPathModule implements ModuleInterface +{ + @Override + public String getName() + { + return "camera"; + } + + @Override + public String getCode() + { + return " __command() ->\n" + + " (\n" + + " print('Camera path module.');\n" + + " print('-------------------');\n" + + " print(' \"/camera start\" - set the starting point');\n" + + " print('');\n" + + " print(' \"/camera add \" - add another point frames later');\n" + + " print('');\n" + + " print(' \"/camera select_interpolation < linear | gauss_? >\"');\n" + + " print(' Select interpolation between points:');\n" + + " print(' - linear: straight paths between points.');\n" + + " print(' - gauss_auto: automatic smooth transitions.');\n" + + " print(' - gauss_: custom fixed variance ');\n" + + " print(' (in points) for special effects.');\n" + + " print('');\n" + + " print(' \"/camera repeat \" - ');\n" + + " print(' Repeat existing points configuration n-times');\n" + + " print(' using points to link path ends');\n" + + " print('');\n" + + " print(' \"/camera speed - ');\n" + + " print(' Change number of frames between points');\n" + + " print(' from 25 -> 4 times faster (less points),');\n" + + " print(' to 400 -> 4 times slower (more points)');\n" + + " print('');\n" + + " print(' \"/camera play : run a path with a player');\n" + + " print(' needs to be multiples of 20, (20 tps)');\n" + + " print('');\n" + + " print(' \"/camera show\": shows current path for a moment');\n" + + " print('');\n" + + " print(' \"/script in camera invoke _show_path_tick \":');\n" + + " print(' place this in a repeating commandblock');\n" + + " print(' to display path continuously');\n" + + " print(' with set particle and number of particles per tick');\n" + + " print(' example: \\'dust 0.9 0.1 0.1 1\\' 100 ');\n" + + " ''\n" + + " );\n" + + " start() -> \n" + + "(\n" + + " p = player();\n" + + " global_points = l(l(l(p~'x',p~'y',p~'z',p~'yaw',p~'pitch'),0,'sharp'));\n" + + " undef('global_path_precalculated');\n" + + " str('Started path at %.1f %.1f %.1f', p~'x', p~'y', p~'z')\n" + + ");\n" + + "\n" + + " add(delay) -> \n" + + "( \n" + + " p = player(); \n" + + " mode = 'sharp';\n" + + " 'mode is currently unused, run_path does always sharp, gauss interpolator is always smooth';\n" + + " ' but this option could be used could be used at some point by more robust interpolators';\n" + + " vector = l(p~'x',p~'y',p~'z', p~'yaw', p~'pitch');\n" + + " __add_path_segment(vector, delay, mode);\n" + + " str('Added point %d: %.1f %.1f %.1f', length(global_points), p~'x', p~'y', p~'z')\n" + + ");\n" + + "\n" + + " __add_path_segment(vector, duration, mode) -> \n" + + "(\n" + + " undef('global_path_precalculated');\n" + + " if ( (l('sharp','smooth') ~ mode) == null, exit('use smooth or sharp point')); \n" + + " if(!global_points, exit('Cannot add point to path that didn\\'t started yet!'));\n" + + " l(v, end_time, m) = element(global_points, -1);\n" + + " put(vector,-2, __adjusted_rot(element(v, -2), element(vector, -2)));\n" + + " global_points += l(vector, end_time+duration, mode)\n" + + ");\n" + + "\n" + + "'adjusts current rotation so we don\\'t spin around like crazy';\n" + + " __adjusted_rot(previous_rot, current_rot) -> \n" + + "(\n" + + " while( abs(previous_rot-current_rot) > 180, 1000,\n" + + " current_rot += if(previous_rot < current_rot, -360, 360)\n" + + " );\n" + + " current_rot\n" + + ");\n" + + "\n" + + "\n" + + " repeat(times, last_section_duration) -> \n" + + "(\n" + + " undef('global_path_precalculated');\n" + + " positions = map(global_points, k = element(_, 0));\n" + + " modes = map(global_points, element(_, -1));\n" + + " durations = map(global_points, element(element(global_points, _i+1), 1)-element(_, 1));\n" + + " put(durations, -1, last_section_duration);\n" + + " loop(times,\n" + + " loop( length(positions),\n" + + " __add_path_segment(element(positions, _), element(durations, _), element(modes, _))\n" + + " )\n" + + " );\n" + + " str('Add %d points %d times', length(positions), times)\n" + + ");\n" + + "\n" + + " speed(percentage) ->\n" + + "(\n" + + " undef('global_path_precalculated');\n" + + " if (percentage < 25 || percentage > 400, \n" + + " exit('path speed can only be speed, or slowed down 4 times. Recall command for larger changes')\n" + + " );\n" + + " ratio = percentage/100;\n" + + " previous_path_length = element(element(global_points, -1),1);\n" + + " for(global_points, put(_, 1, element(_, 1)*ratio ) );\n" + + " undef('global_path_precalculated');\n" + + " str('path %s from %d to %d ticks',\n" + + " if(ratio<1,'shortened','extended'),\n" + + " previous_path_length,\n" + + " element(element(global_points, -1),1)\n" + + " )\n" + + ");\n" + + "\n" + + " select_interpolation(method) ->\n" + + "(\n" + + " undef('global_path_precalculated');\n" + + " __prepare_path_if_needed() -> __prepare_path_if_needed_generic();\n" + + " 'each supported method needs to specify its __find_position_for_point to trace the path';\n" + + " 'accepting segment number, and position in the segment';\n" + + " 'or optionally __prepare_path_if_needed, if path is inefficient to compute point by point';\n" + + " if (\n" + + " method == 'linear',\n" + + " (\n" + + " __find_position_for_point(s, p) -> __find_position_for_linear(s, p)\n" + + " ),\n" + + " method ~ '^gauss_',\n" + + " (\n" + + " type = method - 'gauss_';\n" + + " global_interpol_option = if(type=='auto',0,number(type));\n" + + " __find_position_for_point(s, p) -> __find_position_for_gauss(s, p)\n" + + " ),\n" + + " method ~ '^bungee_',\n" + + " (\n" + + " exit('unsupported / planned');\n" + + " type = method - 'bungee_';\n" + + " global_interpol_option = if(type=='auto',80,number(type));\n" + + " __prepare_path_if_needed() -> __prepare_path_if_needed_bungee()\n" + + " ),\n" + + " \n" + + " exit('Choose one of the following methods: linear, gauss:auto, gauss:')\n" + + " );\n" + + " 'Ok'\n" + + ");\n" + + "\n" + + " select_interpolation('linear');\n" + + "\n" + + " __assert_valid_for_motion() ->\n" + + "(\n" + + " if(!global_points, exit('Path not defined yet'));\n" + + " if(length(global_points)<2, exit('Path not complete - add more points'));\n" + + " null \n" + + ");\n" + + "\n" + + " __get_path_at(segment, start, index) ->\n" + + "(\n" + + " v = element(global_path_precalculated, start+index);\n" + + " if(v == null,\n" + + " v = __find_position_for_point(segment, index);\n" + + " put(global_path_precalculated, start+index, v)\n" + + " );\n" + + " v\n" + + ");\n" + + "\n" + + " __invalidate_points_cache() -> global_path_precalculated = map(range(element(element(global_points, -1),1)), null);\n" + + "\n" + + " show() -> \n" + + "(\n" + + " loop(100,\n" + + " _show_path_tick('dust 0.9 0.1 0.1 1', 100);\n" + + " game_tick(50)\n" + + " );\n" + + " 'Done!'\n" + + ");\n" + + "\n" + + " __play(fps) -> \n" + + "(\n" + + " p = player();\n" + + " __assert_valid_for_motion();\n" + + " __prepare_path_if_needed();\n" + + " loop( length(global_points)-1,\n" + + " segment = _;\n" + + " start = element(element(global_points, segment),1);\n" + + " end = element(element(global_points, segment+1),1);\n" + + " loop(end-start,\n" + + " v = __get_path_at(segment, start, _);\n" + + " modify(p, 'location', v);\n" + + " game_tick(1000/fps)\n" + + " )\n" + + " );\n" + + " game_tick(1000);\n" + + " 'Done!'\n" + + ");\n" + + "\n" + + " play(fps) -> \n" + + "(\n" + + " p = player();\n" + + " __assert_valid_for_motion();\n" + + " __prepare_path_if_needed();\n" + + " if ((fps % 20 != 0) || fps < 20, exit('FPS needs to be multiples of 20') );\n" + + "\ttpt = round(fps / 20);\n" + + " mspt = 50 / tpt; \n" + + " start_time = time();\n" + + " point = 0;\n" + + " loop( length(global_points)-1,\n" + + " segment = _;\n" + + " start = element(element(global_points, segment),1);\n" + + " end = element(element(global_points, segment+1),1);\n" + + " loop(end-start,\n" + + " v = __get_path_at(segment, start, _);\n" + + " modify(p, 'location', v);\n" + + " point += 1;\n" + + " if ((point % tpt == 0), game_tick());\n" + + " end_time = time();\n" + + " took = end_time - start_time;\n" + + " if (took < mspt, sleep(mspt-took));\n" + + " start_time = time()\n" + + " )\n" + + " );\n" + + " game_tick(1000);\n" + + " 'Done!'\n" + + ");\n" + + "\n" + + " _show_path_tick(particle_type, total) -> (\n" + + " __assert_valid_for_motion();\n" + + " __prepare_path_if_needed();\n" + + " loop(total,\n" + + " segment = floor(rand(length(global_points)-1));\n" + + " start = element(element(global_points, segment),1);\n" + + " end = element(element(global_points, segment+1),1);\n" + + " index = floor(rand(end-start));\n" + + " l(x, y, z) = slice(__get_path_at(segment, start, index), 0, 3);\n" + + " particle(particle_type, x, y, z, 1, 0, 0)\n" + + " );\n" + + " null\n" + + ");\n" + + "\n" + + " __prepare_path_if_needed_generic() ->\n" + + "(\n" + + " if(!global_path_precalculated, __invalidate_points_cache())\n" + + ");\n" + + "\n" + + "__find_position_for_linear(segment, point) ->\n" + + "(\n" + + " l(va, start, mode_a) = element(global_points,segment);\n" + + " l(vb, end, mode_b) = element(global_points,segment+1);\n" + + " section = end-start;\n" + + " dt = point/section;\n" + + " dt*vb+(1-dt)*va\n" + + ");\n" + + "\n" + + "\n" + + "'(1/sqrt(2*pi*d*d))*euler^(-((x-miu)^2)/(2*d*d)) ';\n" + + " 'but we will be normalizing anyways, so who cares';\n" + + " __norm_prob(x, miu, d) -> euler^(-((x-miu)^2)/(2*d*d));\n" + + "\n" + + " __find_position_for_gauss(from_index, point) -> \n" + + "(\n" + + " dev = global_interpol_option;\n" + + " components = l();\n" + + " path_point = element(element(global_points, from_index),1);\n" + + " \n" + + " try(\n" + + " for(range(from_index+1, length(global_points)),\n" + + " l(v,ptime,mode) = element(global_points, _);\n" + + " dev = if (global_interpol_option > 0, global_interpol_option, \n" + + " devs = l();\n" + + " if (_+1 < length(global_points), devs += element(element(global_points, _+1),1)-ptime);\n" + + " if (_-1 >= 0, devs += ptime-element(element(global_points, _-1),1));\n" + + " 0.6*reduce(devs, _a+_, 0)/length(devs)\n" + + " );\n" + + " impact = __norm_prob(path_point+point, ptime, dev);\n" + + " if(rtotal && impact < 0.000001*rtotal, throw(null));\n" + + " components += l(v, impact);\n" + + " rtotal += impact\n" + + " )\n" + + " ,null\n" + + " );\n" + + " try(\n" + + " for(range(from_index, -1, -1),\n" + + " l(v,ptime,mode) = element(global_points, _);\n" + + " dev = if (global_interpol_option > 0, global_interpol_option, \n" + + " devs = l();\n" + + " if (_+1 < length(global_points), devs += element(element(global_points, _+1),1)-ptime);\n" + + " if (_-1 >= 0, devs += ptime-element(element(global_points, _-1),1));\n" + + " 0.6*reduce(devs, _a+_, 0)/length(devs)\n" + + " );\n" + + " impact = __norm_prob(path_point+point, ptime, dev);\n" + + " if(ltotal && impact < 0.000001*ltotal, throw(null));\n" + + " components += l(v, impact);\n" + + " ltotal += impact\n" + + " )\n" + + " ,null\n" + + " );\n" + + " total = rtotal+ltotal;\n" + + " reduce(components, _a+element(_,0)*(element(_,1)/total), l(0,0,0,0,0))\n" + + ");\n" + + " __prepare_path_if_needed_bungee(fps) ->\n" + + "(\n" + + " exit('not ready!');\n" + + " if(!global_points, exit('Cannot show path that doesn\\'t exist'));\n" + + " if(length(global_points)<2, exit('Path not complete - add more points'));\n" + + " v = element(element(global_points, 0),0);\n" + + " modify(p, 'pos', slice(v,0,3));\n" + + " modify(p, 'yaw', element(v,3));\n" + + " modify(p, 'pitch', element(v,4));\n" + + " current_target = element(element(global_points, 1),0);\n" + + " current_hook = 1;\n" + + " loop(length(global_points)-1, \n" + + " current_base = _;\n" + + " l(va, start, mode_a) = element(global_points,_);\n" + + " l(vb, end, mode_b) = element(global_points,_+1);\n" + + " section = end-start;\n" + + " loop(section,\n" + + " dt = _/section;\n" + + "\n" + + " v = dt*vb+(1-dt)*va;\n" + + " modify(p, 'pos', slice(v,0,3));\n" + + " modify(p, 'yaw', element(v,3));\n" + + " modify(p, 'pitch', element(v,4));\n" + + " game_tick(1000/fps)\n" + + " )\n" + + " );\n" + + " game_tick(1000);\n" + + " 'Done!'\n" + + ")\n"; + } +} diff --git a/src/main/java/carpet/script/bundled/FileModule.java b/src/main/java/carpet/script/bundled/FileModule.java new file mode 100644 index 00000000..f6edef71 --- /dev/null +++ b/src/main/java/carpet/script/bundled/FileModule.java @@ -0,0 +1,45 @@ +package carpet.script.bundled; + +import carpet.CarpetServer; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Locale; + +public class FileModule implements ModuleInterface +{ + private String name; + private String code; + public FileModule(File sourceFile) + { + try + { + name = sourceFile.getName().replaceFirst("\\.sc","").toLowerCase(Locale.ROOT); + code = new String(Files.readAllBytes(sourceFile.toPath())); + } + catch ( IOException e) + { + name = null; + code = null; + } + } + public FileModule fromName(String name) + { + File sourceFile = CarpetServer.minecraft_server.getLevelStorage().resolveFile( + CarpetServer.minecraft_server.getLevelName(), "scripts/"+name+".sc"); + return new FileModule(sourceFile); + } + @Override + public String getName() + { + return name; + } + + @Override + public String getCode() + { + return code; + } +} diff --git a/src/main/java/carpet/script/bundled/ModuleInterface.java b/src/main/java/carpet/script/bundled/ModuleInterface.java new file mode 100644 index 00000000..aaf4bf0b --- /dev/null +++ b/src/main/java/carpet/script/bundled/ModuleInterface.java @@ -0,0 +1,7 @@ +package carpet.script.bundled; + +public interface ModuleInterface +{ + String getName(); + String getCode(); +} diff --git a/src/main/java/carpet/script/exception/CarpetExpressionException.java b/src/main/java/carpet/script/exception/CarpetExpressionException.java new file mode 100644 index 00000000..0bd2df69 --- /dev/null +++ b/src/main/java/carpet/script/exception/CarpetExpressionException.java @@ -0,0 +1,9 @@ +package carpet.script.exception; + +public class CarpetExpressionException extends ExpressionException +{ + public CarpetExpressionException(String message) + { + super(message); + } +} diff --git a/src/main/java/carpet/script/exception/ExpressionException.java b/src/main/java/carpet/script/exception/ExpressionException.java new file mode 100644 index 00000000..bb392052 --- /dev/null +++ b/src/main/java/carpet/script/exception/ExpressionException.java @@ -0,0 +1,60 @@ +package carpet.script.exception; + +import carpet.script.Expression; +import carpet.script.Fluff; +import carpet.script.Tokenizer; + +import java.util.ArrayList; +import java.util.List; + +import static carpet.script.ExpressionInspector.Expression_getExpressionSnippet; +import static carpet.script.ExpressionInspector.Expression_getName; + +/* The expression evaluators exception class. */ +public class ExpressionException extends RuntimeException +{ + public ExpressionException(String message) + { + super(message); + } + + private static Fluff.TriFunction> errorMaker = (expr, token, errmessage) -> + { + + List snippet = Expression_getExpressionSnippet(token, expr); + List errMsg = new ArrayList<>(snippet); + if (snippet.size() != 1) + { + errmessage+= " at line "+(token.lineno+1)+", pos "+(token.linepos+1); + } + else + { + errmessage += " at pos "+(token.pos+1); + } + if (Expression_getName(expr) != null) + { + errmessage += " ("+Expression_getName(expr)+")"; + } + errMsg.add(errmessage); + return errMsg; + }; + public static Fluff.TriFunction> errorSnooper = null; + + static String makeMessage(Expression e, Tokenizer.Token t, String message) throws ExpressionException + { + if (errorSnooper != null) + { + List alternative = errorSnooper.apply(e, t, message); + if (alternative!= null) + { + return String.join("\n", alternative); + } + } + return String.join("\n", errorMaker.apply(e, t, message)); + } + + public ExpressionException(Expression e, Tokenizer.Token t, String message) + { + super(makeMessage(e, t, message)); + } +} diff --git a/src/main/java/carpet/script/exception/InternalExpressionException.java b/src/main/java/carpet/script/exception/InternalExpressionException.java new file mode 100644 index 00000000..eabe7b05 --- /dev/null +++ b/src/main/java/carpet/script/exception/InternalExpressionException.java @@ -0,0 +1,10 @@ +package carpet.script.exception; + +/* The internal expression evaluators exception class. */ +public class InternalExpressionException extends ExpressionException +{ + public InternalExpressionException(String message) + { + super(message); + } +} diff --git a/src/main/java/carpet/script/value/BlockValue.java b/src/main/java/carpet/script/value/BlockValue.java new file mode 100644 index 00000000..31c4e853 --- /dev/null +++ b/src/main/java/carpet/script/value/BlockValue.java @@ -0,0 +1,220 @@ +package carpet.script.value; + +import carpet.script.CarpetContext; +import carpet.script.LazyValue; +import carpet.script.exception.InternalExpressionException; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.command.arguments.BlockArgumentParser; +import net.minecraft.block.Blocks; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.registry.Registry; +import net.minecraft.world.World; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class BlockValue extends Value +{ + public static final BlockValue AIR = new BlockValue(Blocks.AIR.getDefaultState(), null, BlockPos.ORIGIN); + public static final BlockValue NULL = new BlockValue(null, null, null); + private BlockState blockState; + private BlockPos pos; + private World world; + + public static BlockValue fromCoords(CarpetContext c, int x, int y, int z) + { + BlockPos pos = locateBlockPos(c, x,y,z); + return new BlockValue(null, c.s.getWorld(), pos); + } + + public static BlockValue fromString(String str) + { + try + { + Identifier blockId = Identifier.fromCommandInput(new StringReader(str)); + if (Registry.BLOCK.containsId(blockId)) + { + + Block block = Registry.BLOCK.get(blockId); + return new BlockValue(block.getDefaultState(), null, null); + } + } + catch (CommandSyntaxException ignored) + { + } + throw new InternalExpressionException("Unknown block: "+str); + } + + private static Map bvCache= new HashMap<>(); + public static BlockValue fromCommandExpression(String str) + { + try + { + BlockValue bv = bvCache.get(str); + if (bv != null) return bv; + BlockArgumentParser blockstateparser = (new BlockArgumentParser(new StringReader(str), false)).parse(true); + if (blockstateparser.getBlockState() != null) + { + bv = new BlockValue(blockstateparser.getBlockState(), null, null); + bvCache.put(str, bv); + return bv; + } + } + catch (CommandSyntaxException ignored) + { + } + throw new InternalExpressionException("Cannot parse block: "+str); + } + + public static VectorLocator locateVec(CarpetContext c, List params, int offset) + { + try + { + Value v1 = params.get(0 + offset).evalValue(c); + if (v1 instanceof BlockValue) + { + return new VectorLocator(new Vec3d(((BlockValue) v1).getPos()).add(0.5,0.5,0.5), 1+offset); + } + if (v1 instanceof ListValue) + { + List args = ((ListValue) v1).getItems(); + return new VectorLocator( new Vec3d( + NumericValue.asNumber(args.get(0)).getDouble(), + NumericValue.asNumber(args.get(1)).getDouble(), + NumericValue.asNumber(args.get(2)).getDouble()), + offset+1 + ); + } + return new VectorLocator( new Vec3d( + NumericValue.asNumber(v1).getDouble(), + NumericValue.asNumber(params.get(1 + offset).evalValue(c)).getDouble(), + NumericValue.asNumber(params.get(2 + offset).evalValue(c)).getDouble()), + offset+3 + ); + } + catch (IndexOutOfBoundsException e) + { + throw new InternalExpressionException("Position should be defined either by three coordinates, or a block value"); + } + } + + public static BlockPos locateBlockPos(CarpetContext c, int xpos, int ypos, int zpos) + { + return new BlockPos(c.origin.getX() + xpos, c.origin.getY() + ypos, c.origin.getZ() + zpos); + } + + public static LocatorResult fromParams(CarpetContext c, List params, int offset) + { + try + { + Value v1 = params.get(0 + offset).evalValue(c); + if (v1 instanceof BlockValue) + { + return new LocatorResult(((BlockValue) v1), 1+offset); + } + if (v1 instanceof ListValue) + { + List args = ((ListValue) v1).getItems(); + int xpos = (int) NumericValue.asNumber(args.get(0)).getLong(); + int ypos = (int) NumericValue.asNumber(args.get(1)).getLong(); + int zpos = (int) NumericValue.asNumber(args.get(2)).getLong(); + return new LocatorResult( + new BlockValue( + null, + c.s.getWorld(), + new BlockPos(c.origin.getX() + xpos, c.origin.getY() + ypos, c.origin.getZ() + zpos) + ), + 1+offset); + } + int xpos = (int) NumericValue.asNumber(v1).getLong(); + int ypos = (int) NumericValue.asNumber( params.get(1 + offset).evalValue(c)).getLong(); + int zpos = (int) NumericValue.asNumber( params.get(2 + offset).evalValue(c)).getLong(); + return new LocatorResult( + new BlockValue( + null, + c.s.getWorld(), + new BlockPos(c.origin.getX() + xpos, c.origin.getY() + ypos, c.origin.getZ() + zpos) + ), + 3+offset + ); + } + catch (IndexOutOfBoundsException e) + { + throw new InternalExpressionException("Position should be defined either by three coordinates, or a block value"); + } + } + + public BlockState getBlockState() + { + if (blockState != null) + { + return blockState; + } + if (world != null && pos != null) + { + blockState = world.getBlockState(pos); + return blockState; + } + throw new InternalExpressionException("Attemted to fetch blockstate without world or stored blockstate"); + } + + public BlockValue(BlockState state, World world, BlockPos position) + { + this.world = world; + blockState = state; + pos = position; + } + + + @Override + public String getString() + { + return Registry.BLOCK.getId(getBlockState().getBlock()).getPath(); + } + + @Override + public boolean getBoolean() + { + return this != NULL && !getBlockState().isAir(); + } + + @Override + public Value clone() + { + return new BlockValue(blockState, world, pos); + } + + public BlockPos getPos() + { + return pos; + } + + + public static class LocatorResult + { + public BlockValue block; + public int offset; + LocatorResult(BlockValue b, int o) + { + block = b; + offset = o; + } + } + + public static class VectorLocator + { + public Vec3d vec; + public int offset; + VectorLocator(Vec3d v, int o) + { + vec = v; + offset = o; + } + } +} diff --git a/src/main/java/carpet/script/value/EntityValue.java b/src/main/java/carpet/script/value/EntityValue.java new file mode 100644 index 00000000..bd200acd --- /dev/null +++ b/src/main/java/carpet/script/value/EntityValue.java @@ -0,0 +1,606 @@ +package carpet.script.value; + +import carpet.script.exception.InternalExpressionException; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.minecraft.entity.*; +import net.minecraft.entity.effect.StatusEffect; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.nbt.Tag; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.command.EntitySelector; +import net.minecraft.command.EntitySelectorReader; +import net.minecraft.command.arguments.NbtPathArgumentType; +import net.minecraft.entity.mob.MobEntityWithAi; // EntityCreature; +import net.minecraft.entity.mob.MobEntity; //LivingEntity; +import net.minecraft.entity.ai.goal.Goal; +import net.minecraft.entity.ai.goal.GoToWalkTargetGoal; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.client.network.packet.EntityVelocityUpdateS2CPacket; +import net.minecraft.potion.Potion; +import net.minecraft.predicate.entity.EntityPredicates; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.registry.Registry; +import net.minecraft.network.chat.TextComponent; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public class EntityValue extends Value +{ + private Entity entity; + + public EntityValue(Entity e) + { + entity = e; + } + + private static Map selectorCache = new HashMap<>(); + public static Collection getEntitiesFromSelector(ServerCommandSource source, String selector) + { + try + { + EntitySelector entitySelector = selectorCache.get(selector); + if (entitySelector != null) + { + return entitySelector.getEntities(source); + } + entitySelector = new EntitySelectorReader(new StringReader(selector), true).build(); + selectorCache.put(selector, entitySelector); + return entitySelector.getEntities(source); + } + catch (CommandSyntaxException e) + { + throw new InternalExpressionException("Cannot select entities from "+selector); + } + } + + public Entity getEntity() + { + return entity; + } + + @Override + public String getString() + { + return entity.getDisplayName().getString(); + } + + @Override + public boolean getBoolean() + { + return true; + } + + @Override + public boolean equals(Value v) + { + if (v instanceof EntityValue) + { + return entity.getEntityId()==((EntityValue) v).entity.getEntityId(); + } + return super.equals(v); + } + + @Override + public Value in(Value v) + { + String what = v.getString(); + return this.get(what, null); + } + + public static Pair, Predicate> getPredicate(String who) + { + Pair, Predicate> res = entityPredicates.get(who); + if (res != null) return res; + return res; //TODO add more here like search by tags, or type + //if (who.startsWith('tag:')) + } + private static Map, Predicate>> entityPredicates = + new HashMap, Predicate>>() + {{ + put("*", Pair.of(null, EntityPredicates.VALID_ENTITY)); + put("living", Pair.of(null, (e) -> e instanceof LivingEntity && e.isAlive())); + put("items", Pair.of(EntityType.ITEM, EntityPredicates.VALID_ENTITY)); + put("players", Pair.of(EntityType.PLAYER, EntityPredicates.VALID_ENTITY)); + put("!players", Pair.of(null, (e) -> !(e instanceof PlayerEntity) )); + }}; + public Value get(String what, Value arg) + { + if (!(featureAccessors.containsKey(what))) + throw new InternalExpressionException("unknown feature of entity: "+what); + return featureAccessors.get(what).apply(entity, arg); + } + private static Map inventorySlots = new HashMap(){{ + put("main", EquipmentSlot.MAINHAND); + put("offhand", EquipmentSlot.OFFHAND); + put("head", EquipmentSlot.HEAD); + put("chest", EquipmentSlot.CHEST); + put("legs", EquipmentSlot.LEGS); + put("feet", EquipmentSlot.FEET); + }}; + private static Map> featureAccessors = new HashMap>() {{ + put("removed", (entity, arg) -> new NumericValue(entity.removed)); + put("uuid",(e, a) -> new StringValue(e.getUuidAsString())); + put("id",(e, a) -> new NumericValue(e.getEntityId())); + put("pos", (e, a) -> ListValue.of(new NumericValue(e.x), new NumericValue(e.y), new NumericValue(e.z))); + put("location", (e, a) -> ListValue.of(new NumericValue(e.x), new NumericValue(e.y), new NumericValue(e.z), new NumericValue(e.yaw), new NumericValue(e.pitch))); + put("x", (e, a) -> new NumericValue(e.x)); + put("y", (e, a) -> new NumericValue(e.y)); + put("z", (e, a) -> new NumericValue(e.z)); + put("motion", (e, a) -> + { + Vec3d velocity = e.getVelocity(); + return ListValue.of(new NumericValue(velocity.x), new NumericValue(velocity.y), new NumericValue(velocity.z)); + }); + put("motion_x", (e, a) -> new NumericValue(e.getVelocity().x)); + put("motion_y", (e, a) -> new NumericValue(e.getVelocity().y)); + put("motion_z", (e, a) -> new NumericValue(e.getVelocity().z)); + put("name", (e, a) -> new StringValue(e.getDisplayName().getString())); + put("custom_name", (e, a) -> e.hasCustomName()?new StringValue(e.getCustomName().getString()):Value.NULL); + put("type", (e, a) -> new StringValue(Registry.ENTITY_TYPE.getId(e.getType()).toString().replaceFirst("minecraft:",""))); + put("is_riding", (e, a) -> new NumericValue(e.hasVehicle())); + put("is_ridden", (e, a) -> new NumericValue(e.hasPassengers())); + put("passengers", (e, a) -> ListValue.wrap(e.getPassengerList().stream().map(EntityValue::new).collect(Collectors.toList()))); + put("mount", (e, a) -> (e.getVehicle()!=null)?new EntityValue(e.getVehicle()):Value.NULL); + put("tags", (e, a) -> ListValue.wrap(e.getScoreboardTags().stream().map(StringValue::new).collect(Collectors.toList()))); + put("has_tag", (e, a) -> new NumericValue(e.getScoreboardTags().contains(a.getString()))); + put("yaw", (e, a)-> new NumericValue(e.yaw)); + put("pitch", (e, a)-> new NumericValue(e.pitch)); + put("is_burning", (e, a) -> new NumericValue(e.isOnFire())); + //put("fire", (e, a) -> new NumericValue(e.getFire())); needs mixing + put("silent", (e, a)-> new NumericValue(e.isSilent())); + put("gravity", (e, a) -> new NumericValue(!e.hasNoGravity())); + put("immune_to_fire", (e, a) -> new NumericValue(e.isFireImmune())); + + put("invulnerable", (e, a) -> new NumericValue(e.isInvulnerable())); + put("dimension", (e, a) -> new StringValue(e.dimension.toString().replaceFirst("minecraft:",""))); + put("height", (e, a) -> new NumericValue(e.getSize(EntityPose.STANDING).height)); + put("width", (e, a) -> new NumericValue(e.getSize(EntityPose.STANDING).width)); + put("eye_height", (e, a) -> new NumericValue(e.getEyeHeight(EntityPose.STANDING))); + put("age", (e, a) -> new NumericValue(e.age)); + put("item", (e, a) -> (e instanceof ItemEntity)?new StringValue(((ItemEntity) e).getStack().getDisplayName().getString()):Value.NULL); + put("count", (e, a) -> (e instanceof ItemEntity)?new NumericValue(((ItemEntity) e).getStack().getAmount()):Value.NULL); + // ItemEntity -> despawn timer via ssGetAge + put("is_baby", (e, a) -> (e instanceof LivingEntity)?new NumericValue(((LivingEntity) e).isBaby()):Value.NULL); + put("target", (e, a) -> { + if (e instanceof MobEntity) + { + LivingEntity target = ((MobEntity) e).getTarget(); // there is also getAttacking in living.... + if (target != null) + { + return new EntityValue(target); + } + } + return Value.NULL; + }); + /*put("home", (e, a) -> { + if (e instanceof MobEntityWithAi) + { + return ((MobEntityWithAi) e).has hasHome()?new BlockValue(null, e.getEntityWorld(), ((MobEntityWithAi) e).getHomePosition()):Value.FALSE; + } + return Value.NULL; + });*/ + put("sneaking", (e, a) -> e.isSneaking()?Value.TRUE:Value.FALSE); + put("sprinting", (e, a) -> e.isSprinting()?Value.TRUE:Value.FALSE); + put("swimming", (e, a) -> e.isSwimming()?Value.TRUE:Value.FALSE); + /*put("jumping", (e, a) -> { + if (e instanceof LivingEntity) + { + return ((LivingEntity) e).getJumping()?Value.TRUE:Value.FALSE; + } + return Value.NULL; + });*/ //needs mixing + put("gamemode", (e, a) -> { + if (e instanceof ServerPlayerEntity) + { + return new StringValue(((ServerPlayerEntity) e).interactionManager.getGameMode().getName()); + } + return Value.NULL; + }); + put("gamemode_id", (e, a) -> { + if (e instanceof ServerPlayerEntity) + { + return new NumericValue(((ServerPlayerEntity) e).interactionManager.getGameMode().getId()); + } + return Value.NULL; + }); + //spectating_entity + // isGlowing + put("effect", (e, a) -> + { + if (!(e instanceof LivingEntity)) + { + return Value.NULL; + } + if (a == null) + { + List effects = new ArrayList<>(); + for (StatusEffectInstance p : ((LivingEntity) e).getStatusEffects()) + { + effects.add(ListValue.of( + new StringValue(p.getTranslationKey().replaceFirst("^effect\\.minecraft\\.", "")), + new NumericValue(p.getAmplifier()), + new NumericValue(p.getDuration()) + )); + } + return ListValue.wrap(effects); + } + String effectName = a.getString(); + StatusEffect potion = Registry.STATUS_EFFECT.get(new Identifier(effectName)); + if (potion == null) + throw new InternalExpressionException("No such an effect: "+effectName); + if (!((LivingEntity) e).hasStatusEffect(potion)) + return Value.NULL; + StatusEffectInstance pe = ((LivingEntity) e).getStatusEffect(potion); + return ListValue.of( new NumericValue(pe.getAmplifier()), new NumericValue(pe.getDuration()) ); + }); + put("health", (e, a) -> + { + if (e instanceof LivingEntity) + { + return new NumericValue(((LivingEntity) e).getHealth()); + } + //if (e instanceof ItemEntity) + //{ + // e.h consider making item health public + //} + return Value.NULL; + }); + put("holds", (e, a) -> { + EquipmentSlot where = EquipmentSlot.MAINHAND; + if (a != null) + where = inventorySlots.get(a.getString()); + if (where == null) + throw new InternalExpressionException("Unknown inventory slot: "+a.getString()); + if (e instanceof LivingEntity) + { + ItemStack itemstack = ((LivingEntity)e).getEquippedStack(where); + if (!itemstack.isEmpty()) + { + return ListValue.of( + new StringValue(Registry.ITEM.getId(itemstack.getItem()).getPath()), + new NumericValue(itemstack.getAmount()), + new StringValue(itemstack.toTag(new CompoundTag()).toString()) + ); + } + } + return Value.NULL; + + }); + + put("nbt",(e, a) -> { + CompoundTag nbttagcompound = e.toTag((new CompoundTag())); + if (a==null) + return new StringValue(nbttagcompound.toString()); + NbtPathArgumentType.NbtPath path; + try + { + path = NbtPathArgumentType.create().method_9362(new StringReader(a.getString())); + } + catch (CommandSyntaxException exc) + { + throw new InternalExpressionException("Incorrect path: "+a.getString()); + } + String res = null; + try + { + List tags = path.get(nbttagcompound); + if (tags.size()==0) + return Value.NULL; + if (tags.size()==1) + return new StringValue(tags.get(0).toTextComponent().getString()); + return ListValue.wrap(tags.stream().map((t) -> new StringValue(t.toTextComponent().getString())).collect(Collectors.toList())); + } + catch (CommandSyntaxException ignored) { } + return new StringValue(res); + }); + }}; + private static Req assertEntityArgType(Class klass, Value arg) + { + if (!(arg instanceof EntityValue)) + { + return null; + } + Entity e = ((EntityValue) arg).getEntity(); + if (!(klass.isAssignableFrom(e.getClass()))) + { + return null; + } + return (Req)e; + } + + public void set(String what, Value toWhat) + { + if (!(featureModifiers.containsKey(what))) + throw new InternalExpressionException("unknown action on entity: " + what); + featureModifiers.get(what).accept(entity, toWhat); + } + + private static Map> featureModifiers = new HashMap>() {{ + put("remove", (entity, value) -> entity.remove()); + put("health", (e, v) -> { if (e instanceof LivingEntity) ((LivingEntity) e).setHealth((float) NumericValue.asNumber(v).getDouble()); }); + put("kill", (e, v) -> e.kill()); + put("location", (e, v) -> + { + if (!(v instanceof ListValue)) + { + throw new InternalExpressionException("expected a list of 5 parameters as second argument"); + } + List coords = ((ListValue) v).getItems(); + e.x = NumericValue.asNumber(coords.get(0)).getDouble(); + e.y = NumericValue.asNumber(coords.get(1)).getDouble(); + e.z = NumericValue.asNumber(coords.get(2)).getDouble(); + e.pitch = (float) NumericValue.asNumber(coords.get(4)).getDouble(); + e.prevPitch = e.pitch; + e.yaw = (float) NumericValue.asNumber(coords.get(3)).getDouble(); + e.prevYaw = e.yaw; + e.setPosition(e.x, e.y, e.z); + if (e instanceof ServerPlayerEntity) + ((ServerPlayerEntity)e).networkHandler.requestTeleport(e.x, e.y, e.z, e.yaw, e.pitch); + }); + put("pos", (e, v) -> + { + if (!(v instanceof ListValue)) + { + throw new InternalExpressionException("expected a list of 3 parameters as second argument"); + } + List coords = ((ListValue) v).getItems(); + e.x = NumericValue.asNumber(coords.get(0)).getDouble(); + e.y = NumericValue.asNumber(coords.get(1)).getDouble(); + e.z = NumericValue.asNumber(coords.get(2)).getDouble(); + e.setPosition(e.x, e.y, e.z); + if (e instanceof ServerPlayerEntity) + ((ServerPlayerEntity)e).networkHandler.requestTeleport(e.x, e.y, e.z, e.yaw, e.pitch); + }); + put("x", (e, v) -> + { + e.x = NumericValue.asNumber(v).getDouble(); + e.setPosition(e.x, e.y, e.z); + if (e instanceof ServerPlayerEntity) + ((ServerPlayerEntity)e).networkHandler.requestTeleport(e.x, e.y, e.z, e.yaw, e.pitch); + }); + put("y", (e, v) -> + { + e.y = NumericValue.asNumber(v).getDouble(); + e.setPosition(e.x, e.y, e.z); + if (e instanceof ServerPlayerEntity) + ((ServerPlayerEntity)e).networkHandler.requestTeleport(e.x, e.y, e.z, e.yaw, e.pitch); + }); + put("z", (e, v) -> + { + e.z = NumericValue.asNumber(v).getDouble(); + e.setPosition(e.x, e.y, e.z); + if (e instanceof ServerPlayerEntity) + ((ServerPlayerEntity)e).networkHandler.requestTeleport(e.x, e.y, e.z, e.yaw, e.pitch); + }); + put("pitch", (e, v) -> + { + e.pitch = (float) NumericValue.asNumber(v).getDouble(); + e.prevPitch = e.pitch; + if (e instanceof ServerPlayerEntity) + ((ServerPlayerEntity)e).networkHandler.requestTeleport(e.x, e.y, e.z, e.yaw, e.pitch); + }); + put("yaw", (e, v) -> + { + e.yaw = (float) NumericValue.asNumber(v).getDouble(); + e.prevYaw = e.yaw; + if (e instanceof ServerPlayerEntity) + ((ServerPlayerEntity)e).networkHandler.requestTeleport(e.x, e.y, e.z, e.yaw, e.pitch); + }); + //"look" + //"turn" + //"nod" + + put("move", (e, v) -> + { + if (!(v instanceof ListValue)) + { + throw new InternalExpressionException("expected a list of 3 parameters as second argument"); + } + List coords = ((ListValue) v).getItems(); + e.x += NumericValue.asNumber(coords.get(0)).getDouble(); + e.y += NumericValue.asNumber(coords.get(1)).getDouble(); + e.z += NumericValue.asNumber(coords.get(2)).getDouble(); + e.setPosition(e.x, e.y, e.z); + if (e instanceof ServerPlayerEntity) + ((ServerPlayerEntity)e).networkHandler.requestTeleport(e.y, e.y, e.z, e.yaw, e.pitch); + }); + + put("motion", (e, v) -> + { + if (!(v instanceof ListValue)) + { + throw new InternalExpressionException("expected a list of 3 parameters as second argument"); + } + List coords = ((ListValue) v).getItems(); + e.setVelocity( + NumericValue.asNumber(coords.get(0)).getDouble(), + NumericValue.asNumber(coords.get(1)).getDouble(), + NumericValue.asNumber(coords.get(2)).getDouble() + ); + if (e instanceof ServerPlayerEntity) + ((ServerPlayerEntity)e).networkHandler.sendPacket(new EntityVelocityUpdateS2CPacket(e)); + }); + put("motion_x", (e, v) -> + { + Vec3d velocity = e.getVelocity(); + e.setVelocity(NumericValue.asNumber(v).getDouble(), velocity.y, velocity.z); + if (e instanceof ServerPlayerEntity) + ((ServerPlayerEntity)e).networkHandler.sendPacket(new EntityVelocityUpdateS2CPacket(e)); + }); + put("motion_y", (e, v) -> + { + Vec3d velocity = e.getVelocity(); + e.setVelocity(velocity.x, NumericValue.asNumber(v).getDouble(), velocity.z); + if (e instanceof ServerPlayerEntity) + ((ServerPlayerEntity)e).networkHandler.sendPacket(new EntityVelocityUpdateS2CPacket(e)); + }); + put("motion_z", (e, v) -> + { + Vec3d velocity = e.getVelocity(); + e.setVelocity(velocity.x, velocity.y, NumericValue.asNumber(v).getDouble()); + if (e instanceof ServerPlayerEntity) + ((ServerPlayerEntity)e).networkHandler.sendPacket(new EntityVelocityUpdateS2CPacket(e)); + }); + + put("accelerate", (e, v) -> + { + if (!(v instanceof ListValue)) + { + throw new InternalExpressionException("expected a list of 3 parameters as second argument"); + } + List coords = ((ListValue) v).getItems(); + e.addVelocity( + NumericValue.asNumber(coords.get(0)).getDouble(), + NumericValue.asNumber(coords.get(1)).getDouble(), + NumericValue.asNumber(coords.get(2)).getDouble() + ); + if (e instanceof ServerPlayerEntity) + ((ServerPlayerEntity)e).networkHandler.sendPacket(new EntityVelocityUpdateS2CPacket(e)); + + }); + put("custom_name", (e, v) -> { + String name = v.getString(); + if (name.isEmpty()) + e.setCustomName(null); + e.setCustomName(new TextComponent(v.getString())); + }); + put("dismount", (e, v) -> e.stopRiding() ); + put("mount", (e, v) -> { + if (v instanceof EntityValue) + { + e.startRiding(((EntityValue) v).getEntity(),true); + } + }); + put("drop_passengers", (e, v) -> e.removeAllPassengers()); + put("mount_passengers", (e, v) -> { + if (v==null) + throw new InternalExpressionException("mount_passengers needs entities to ride"); + if (v instanceof EntityValue) + ((EntityValue) v).getEntity().startRiding(e); + else if (v instanceof ListValue) + for (Value element : ((ListValue) v).getItems()) + if (element instanceof EntityValue) + ((EntityValue) element).getEntity().startRiding(e); + }); + put("tag", (e, v) -> { + if (v==null) + throw new InternalExpressionException("tag requires parameters"); + if (v instanceof ListValue) + for (Value element : ((ListValue) v).getItems()) e.addScoreboardTag(element.getString()); + else + e.addScoreboardTag(v.getString()); + }); + put("clear_tag", (e, v) -> { + if (v==null) + throw new InternalExpressionException("clear_tag requires parameters"); + if (v instanceof ListValue) + for (Value element : ((ListValue) v).getItems()) e.removeScoreboardTag(element.getString()); + else + e.removeScoreboardTag(v.getString()); + }); + //put("target", (e, v) -> { + // // attacks indefinitely - might need to do it through tasks + // if (e instanceof MobEntity) + // { + // LivingEntity elb = assertEntityArgType(LivingEntity.class, v); + // ((MobEntity) e).setAttackTarget(elb); + // } + //}); + put("talk", (e, v) -> { + // attacks indefinitely + if (e instanceof MobEntity) + { + ((MobEntity) e).playAmbientSound(); + } + }); + /* + put("home", (e, v) -> { + if (!(e instanceof MobEntityWithAi)) + return; + MobEntityWithAi ec = (MobEntityWithAi)e; + if (v == null) + throw new InternalExpressionException("home requires at least one position argument, and optional distance, or null to cancel"); + if (v instanceof NullValue) + { + ec.detachHome(); + ec.getAI(false).removeTask(ec.temporaryTasks.get("home")); + ec.temporaryTasks.remove("home"); + return; + } + + BlockPos pos; + int distance = 16; + + if (v instanceof BlockValue) + { + pos = ((BlockValue) v).getPos(); + if (pos == null) throw new InternalExpressionException("block is not positioned in the world"); + } + else if (v instanceof ListValue) + { + List lv = ((ListValue) v).getItems(); + if (lv.get(0) instanceof BlockValue) + { + pos = ((BlockValue) lv.get(0)).getPos(); + if (lv.size()>1) + { + distance = (int) NumericValue.asNumber(lv.get(1)).getLong(); + } + } + else if (lv.size()>=3) + { + pos = new BlockPos(NumericValue.asNumber(lv.get(0)).getLong(), + NumericValue.asNumber(lv.get(1)).getLong(), + NumericValue.asNumber(lv.get(2)).getLong()); + if (lv.size()>3) + { + distance = (int) NumericValue.asNumber(lv.get(4)).getLong(); + } + } + else throw new InternalExpressionException("home requires at least one position argument, and optional distance"); + + } + else throw new InternalExpressionException("home requires at least one position argument, and optional distance"); + + ec.setHomePosAndDistance(pos, distance); + if (!ec.temporaryTasks.containsKey("home")) + { + Goal task = new GoToWalkTargetGoal(ec, 1.0D); + ec.temporaryTasks.put("home", task); + ec.getAI(false).addTask(10, task); + } + });*/ //requires mixing + + // gamemode + // spectate + // "fire" + // "extinguish" + // "silent" + // "gravity" + // "invulnerable" + // "dimension" + // "item" + // "count", + // "age", + // "effect_"name + // "hold" + // "hold_offhand" + // "jump" + // "nbt" <-big one, for now use run('data merge entity ... + }}; +} diff --git a/src/main/java/carpet/script/value/FunctionSignatureValue.java b/src/main/java/carpet/script/value/FunctionSignatureValue.java new file mode 100644 index 00000000..2fefaf05 --- /dev/null +++ b/src/main/java/carpet/script/value/FunctionSignatureValue.java @@ -0,0 +1,48 @@ +package carpet.script.value; + +import carpet.script.exception.ExpressionException; +import carpet.script.value.Value; + +import java.util.List; + +public class FunctionSignatureValue extends Value +{ + private String identifier; + private List arguments; + private List globals; + + @Override + public String getString() + { + throw new ExpressionException("Function "+identifier+" is not defined yet"); + } + + @Override + public boolean getBoolean() + { + throw new ExpressionException("Function "+identifier+" is not defined yet"); + } + + @Override + public Value clone() + { + throw new ExpressionException("Function "+identifier+" is not defined yet"); + } + public FunctionSignatureValue(String name, List args, List globals) + { + this.identifier = name; + this.arguments = args; + this.globals = globals; + } + public String getName() + { + return identifier; + } + public List getArgs() + { + return arguments; + } + public List getGlobals() {return globals;} + + +} diff --git a/src/main/java/carpet/script/value/GlobalValue.java b/src/main/java/carpet/script/value/GlobalValue.java new file mode 100644 index 00000000..ae51b0e3 --- /dev/null +++ b/src/main/java/carpet/script/value/GlobalValue.java @@ -0,0 +1,24 @@ +package carpet.script.value; + +import carpet.script.value.Value; + +public class GlobalValue extends Value +{ + public GlobalValue(Value variable) + { + variable.assertAssignable(); + this.boundVariable = variable.boundVariable; + } + + @Override + public String getString() + { + return boundVariable; + } + + @Override + public boolean getBoolean() + { + return false; + } +} diff --git a/src/main/java/carpet/script/value/LazyListValue.java b/src/main/java/carpet/script/value/LazyListValue.java new file mode 100644 index 00000000..2f1af01e --- /dev/null +++ b/src/main/java/carpet/script/value/LazyListValue.java @@ -0,0 +1,103 @@ +package carpet.script.value; + +import carpet.script.exception.InternalExpressionException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +public abstract class LazyListValue extends ListValue implements Iterator +{ + public static LazyListValue range(long from, long to, long step) + { + return new LazyListValue() + { + { + if (step == 0) + throw new InternalExpressionException("range will never end with zero step"); + this.current = from; + this.limit = to; + this.stepp = step; + } + private long current; + private long limit; + private long stepp; + @Override + public Value next() + { + Value val = new NumericValue(current); + current += stepp; + return val; + } + + @Override + public boolean hasNext() + { + return stepp > 0?(current < limit):(current > limit); + } + }; + } + + public LazyListValue() + { + super(Collections.emptyList()); + } + + @Override + public String getString() + { + return "[...]"; + } + + @Override + public boolean getBoolean() + { + return hasNext(); + } + public abstract boolean hasNext(); + + public abstract Value next(); + + @Override + public Iterator iterator() {return this;} + + public List unroll() + { + List result = new ArrayList<>(); + this.forEachRemaining(result::add); + return result; + } + + @Override + public Value slice(long from, long to) + { + if (to < 0) to = Integer.MAX_VALUE; + if (from < 0) from = 0; + if (from > to) + return ListValue.of(); + List result = new ArrayList<>(); + int i; + for (i = 0; i < from; i++) + { + if (hasNext()) + next(); + else + return ListValue.wrap(result); + } + for (i = (int)from; i < to; i++) + { + if (hasNext()) + result.add(next()); + else + return ListValue.wrap(result); + } + return ListValue.wrap(result); + } + @Override + public Value add(Value other) + { + throw new InternalExpressionException("Cannot add to iterators"); + } + +} diff --git a/src/main/java/carpet/script/value/ListValue.java b/src/main/java/carpet/script/value/ListValue.java new file mode 100644 index 00000000..e8cfe02f --- /dev/null +++ b/src/main/java/carpet/script/value/ListValue.java @@ -0,0 +1,297 @@ +package carpet.script.value; + +import carpet.script.exception.InternalExpressionException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +import static java.lang.Math.abs; + +public class ListValue extends Value +{ + protected List items; + @Override + public String getString() + { + return "["+items.stream().map(Value::getString).collect(Collectors.joining(", "))+"]"; + } + + @Override + public String getPrettyString() + { + if (items.size()<8) + return "["+items.stream().map(Value::getPrettyString).collect(Collectors.joining(", "))+"]"; + return "["+items.get(0).getPrettyString()+", "+items.get(1).getPrettyString()+", ..., "+ + items.get(items.size()-2).getPrettyString()+", "+items.get(items.size()-1).getPrettyString()+"]"; + } + + @Override + public boolean getBoolean() { + return !items.isEmpty(); + } + + @Override + public Value clone() + { + return new ListValue(items); + } + public ListValue(Collection list) + { + items = new ArrayList<>(); + items.addAll(list); + } + public static ListValue wrap(List list) + { + ListValue created = new ListValue(); + created.items = list; + return created; + } + public static ListValue of(Value ... list) + { + return ListValue.wrap(Arrays.asList(list)); + } + + private ListValue() + { + items = new ArrayList<>(); + } + + @Override + public Value add(Value other) { + ListValue output = new ListValue(); + if (other instanceof ListValue) + { + List other_list = ((ListValue) other).items; + if (other_list.size() == items.size()) + { + for(int i = 0, size = items.size(); i < size; i++) + { + output.items.add(items.get(i).add(other_list.get(i))); + } + } + else + { + throw new InternalExpressionException("Cannot subtract two lists of uneven sizes"); + } + } + else + { + for (Value v : items) + { + output.items.add(v.add(other)); + } + } + return output; + } + public void append(Value v) + { + items.add(v); + } + public Value subtract(Value other) + { + ListValue output = new ListValue(); + if (other instanceof ListValue) + { + List other_list = ((ListValue) other).items; + if (other_list.size() == items.size()) + { + for(int i = 0, size = items.size(); i < size; i++) + { + output.items.add(items.get(i).subtract(other_list.get(i))); + } + } + else + { + throw new InternalExpressionException("Cannot subtract two lists of uneven sizes"); + } + } + else + { + for (Value v : items) + { + output.items.add(v.subtract(other)); + } + } + return output; + } + public void subtractFrom(Value v) // if I ever do -= then it wouod remove items + { + throw new UnsupportedOperationException(); // TODO + } + + + public Value multiply(Value other) + { + ListValue output = new ListValue(); + if (other instanceof ListValue) + { + List other_list = ((ListValue) other).items; + if (other_list.size() == items.size()) + { + for(int i = 0, size = items.size(); i < size; i++) + { + output.items.add(items.get(i).multiply(other_list.get(i))); + } + } + else + { + throw new InternalExpressionException("Cannot subtract two lists of uneven sizes"); + } + } + else + { + for (Value v : items) + { + output.items.add(v.multiply(other)); + } + } + return output; + } + public Value divide(Value other) + { + ListValue output = new ListValue(); + if (other instanceof ListValue) + { + List other_list = ((ListValue) other).items; + if (other_list.size() == items.size()) + { + for(int i = 0, size = items.size(); i < size; i++) + { + output.items.add(items.get(i).divide(other_list.get(i))); + } + } + else + { + throw new InternalExpressionException("Cannot subtract two lists of uneven sizes"); + } + } + else + { + for (Value v : items) + { + output.items.add(v.divide(other)); + } + } + return output; + } + + @Override + public int compareTo(Value o) + { + if (o instanceof ListValue) + { + ListValue ol = (ListValue)o; + int this_size = this.getItems().size(); + int o_size = ol.getItems().size(); + if (this_size != o_size) + { + return this_size - o_size; + } + if (this_size == 0) + { + return 0; + } + for (int i = 0; i < this_size; i++) + { + int res = this.items.get(i).compareTo(ol.items.get(i)); + if (res != 0) + { + return res; + } + } + return 0; + } + return super.compareTo(o); + } + + public List getItems() + { + return items; + } + + public Iterator iterator() + { + return items.iterator(); + } + public void fatality() + { + } + + public void extend(List subList) + { + for (Value v: subList) + items.add(v); + } + + public void addAtIndex(int index, List subList) + { + int numitems = items.size(); + long range = abs(index)/numitems; + index += (range+2)*numitems; + index = index % numitems; + for (Value v: subList) + { + if (index < numitems) + { + items.set(index, v); + } + else + { + items.add(v); + } + index++; + } + } + + + public static class ListConstructorValue extends ListValue + { + public ListConstructorValue(Collection list) + { + super(list); + } + } + public int length() + { + return items.size(); + } + + @Override + public Value in(Value value1) + { + for (int i = 0; i < items.size(); i++) + { + Value v = items.get(i); + if (v.equals(value1)) + { + return new NumericValue(i); + } + } + return Value.NULL; + } + + @Override + public Value slice(long from, long to) + { + List items = getItems(); + int size = items.size(); + if (to < 0 || to > size) to = size; + if (from < 0 || from > size) from = size; + if (from > to) + return ListValue.of(); + return new ListValue(getItems().subList((int)from, (int) to)); + } + + @Override + public double readNumber() + { + return (double)items.size(); + } + + + +} diff --git a/src/main/java/carpet/script/value/NullValue.java b/src/main/java/carpet/script/value/NullValue.java new file mode 100644 index 00000000..f7d9858b --- /dev/null +++ b/src/main/java/carpet/script/value/NullValue.java @@ -0,0 +1,41 @@ +package carpet.script.value; + +public class NullValue extends NumericValue // TODO check nonsingleton code +{ + @Override + public String getString() + { + return "null"; + } + + @Override + public String getPrettyString() + { + return "null"; + } + + @Override + public boolean getBoolean() + { + return false; + } + + @Override + public Value clone() + { + return new NullValue(); + } + public NullValue() {super(0.0D);} + + @Override + public boolean equals(final Value o) + { + return o instanceof NullValue; + } + + @Override + public int compareTo(Value o) + { + return o instanceof NullValue ? 0 : -1; + } +} diff --git a/src/main/java/carpet/script/value/NumericValue.java b/src/main/java/carpet/script/value/NumericValue.java new file mode 100644 index 00000000..2a87b334 --- /dev/null +++ b/src/main/java/carpet/script/value/NumericValue.java @@ -0,0 +1,170 @@ +package carpet.script.value; + +import carpet.script.exception.InternalExpressionException; +import org.apache.commons.lang3.StringUtils; + +import java.math.BigDecimal; + +import static java.lang.Math.abs; + +public class NumericValue extends Value +{ + private Double value; + final static double epsilon = 1024*Double.MIN_VALUE; + + public static NumericValue asNumber(Value v1) + { + if (!(v1 instanceof NumericValue)) + throw new InternalExpressionException("Operand has to be of a numeric type"); + return ((NumericValue) v1); + } + + + @Override + public String getString() + { + try + { + return BigDecimal.valueOf(value).stripTrailingZeros().toPlainString(); + } + catch (NumberFormatException exc) + { + throw new ArithmeticException("Incorrect number format for "+value); + } + } + + @Override + public String getPrettyString() + { + if (getDouble() == (double)getLong()) + { + return Long.toString(getLong()); + } + else + { + return String.format("%.3f..", getDouble()); + } + } + + @Override + public boolean getBoolean() + { + return value != null && abs(value) > epsilon; + } + public double getDouble() + { + return value; + } + public long getLong() + { + return (long)(value+epsilon); + } + + @Override + public Value add(Value v) + { // TODO test if definintn add(NumericVlaue) woud solve the casting + if (v instanceof NumericValue) + { + return new NumericValue(getDouble() + ((NumericValue) v).getDouble() ); + } + return super.add(v); + } + public Value subtract(Value v) { // TODO test if definintn add(NumericVlaue) woud solve the casting + if (v instanceof NumericValue) + { + return new NumericValue(getDouble() - (((NumericValue) v).getDouble())); + } + return super.subtract(v); + } + public Value multiply(Value v) + { + if (v instanceof NumericValue) + { + return new NumericValue(getDouble() * ((NumericValue) v).getDouble() ); + } + if (v instanceof ListValue) + { + return v.multiply(this); + } + return new StringValue(StringUtils.repeat(v.getString(), (int) getLong())); + } + public Value divide(Value v) + { + if (v instanceof NumericValue) + { + return new NumericValue(getDouble() / ((NumericValue) v).getDouble() ); + } + return super.divide(v); + } + + + + @Override + public Value clone() + { + return new NumericValue(value); + } + + @Override + public int compareTo(Value o) + { + if (o instanceof NullValue) + { + return -o.compareTo(this); + } + if (o instanceof NumericValue) + { + return value.compareTo(((NumericValue) o).getDouble()); + } + return super.compareTo(o); + } + @Override + public boolean equals(Value o) + { + if (o instanceof NullValue) + { + return o.equals(this); + } + if (o instanceof NumericValue) + { + return !this.subtract(o).getBoolean(); + } + return super.equals(o); + } + + public NumericValue(double value) + { + this.value = value; + } + public NumericValue(String value) + { + this(new BigDecimal(value).doubleValue()); + } + public NumericValue(long value) + { + this.value = (double)value; + } + public NumericValue(boolean boolval) + { + this(boolval?1.0D:0.0D); + } + + @Override + public int length() + { + return Integer.toString(value.intValue()).length(); + } + + @Override + public double readNumber() + { + return value; + } + + @Override + public long readInteger() + { + return getLong(); + } + +} diff --git a/src/main/java/carpet/script/value/StringValue.java b/src/main/java/carpet/script/value/StringValue.java new file mode 100644 index 00000000..650d6211 --- /dev/null +++ b/src/main/java/carpet/script/value/StringValue.java @@ -0,0 +1,27 @@ +package carpet.script.value; + +public class StringValue extends Value +{ + private String str; + + @Override + public String getString() { + return str; + } + + @Override + public boolean getBoolean() { + return str != null && !str.isEmpty(); + } + + @Override + public Value clone() + { + return new StringValue(str); + } + + public StringValue(String str) + { + this.str = str; + } +} diff --git a/src/main/java/carpet/script/value/Value.java b/src/main/java/carpet/script/value/Value.java new file mode 100644 index 00000000..24a3d26c --- /dev/null +++ b/src/main/java/carpet/script/value/Value.java @@ -0,0 +1,175 @@ +package carpet.script.value; + +import carpet.script.exception.InternalExpressionException; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public abstract class Value implements Comparable, Cloneable +{ + public static Value FALSE = new NumericValue(0); + public static Value TRUE = new NumericValue(1); + public static Value ZERO = FALSE; + public static Value NULL = new NullValue(); + + public String boundVariable; + + public static T assertNotNull(T t) + { + if (t == null) + throw new InternalExpressionException("Operand may not be null"); + return t; + } + + public static void assertNotNull(T t1, T t2) + { + if (t1 == null) + throw new InternalExpressionException("First operand may not be null"); + if (t2 == null) + throw new InternalExpressionException("Second operand may not be null"); + } + + public boolean isBound() + { + return boundVariable != null; + } + public String getVariable() + { + return boundVariable; + } + public Value reboundedTo(String var) + { + Value copy = null; + try + { + copy = (Value)clone(); + } + catch (CloneNotSupportedException e) + { + // should not happen + e.printStackTrace(); + } + copy.boundVariable = var; + return copy; + } + public Value bindTo(String var) + { + this.boundVariable = var; + return this; + } + + public abstract String getString(); + + public String getPrettyString() + { + return getString(); + } + + + public abstract boolean getBoolean(); + + public Value add(Value o) { + String lstr = this.getString(); + if (lstr == null) // null + return new StringValue(o.getString()); + String rstr = o.getString(); + if (rstr == null) + { + return new StringValue(lstr); + } + return new StringValue(lstr+rstr); + } + public Value subtract(Value v) + { + return new StringValue(this.getString().replace(v.getString(),"")); + } + public Value multiply(Value v) + { + if (v instanceof NumericValue || v instanceof ListValue) + { + return v.multiply(this); + } + return new StringValue(this.getString()+"."+v.getString()); + } + public Value divide(Value v) + { + if (v instanceof NumericValue) + { + String lstr = getString(); + return new StringValue(lstr.substring(0, (int)(lstr.length()/ ((NumericValue) v).getDouble()))); + } + return new StringValue(getString()+"/"+v.getString()); + } + + public Value() + { + this.boundVariable = null; + } + + @Override + public int compareTo(final Value o) + { + if (o instanceof NumericValue || o instanceof ListValue) + { + return -o.compareTo(this); + } + return getString().compareTo(o.getString()); + } + public boolean equals(final Value o) + { + return this.compareTo(o)==0; + } + + public void assertAssignable() + { + if (boundVariable == null || boundVariable.startsWith("_")) + { + if (boundVariable != null) + { + throw new InternalExpressionException(boundVariable+ " cannot be assigned a new value"); + } + throw new InternalExpressionException(getString()+ " is not a variable"); + + } + } + + public Value in(Value value1) + { + final Pattern p = Pattern.compile(value1.getString()); + final Matcher m = p.matcher(this.getString()); + return m.find()?new StringValue(m.group()):Value.NULL; + } + public int length() + { + return getString().length(); + } + + public Value slice(long from, long to) + { + String value = this.getString(); + int size = value.length(); + if (to > size) to = -1; + if (from < 0) from = 0; + if (from > size) from = size; + if (to>=0) + return new StringValue(value.substring((int)from, (int)to)); + return new StringValue(value.substring((int)from)); + } + public double readNumber() + { + String s = getString(); + try + { + return Double.valueOf(s); + } + catch (NumberFormatException e) + { + return Double.NaN; + } + } + + public long readInteger() + { + return (long)readNumber(); + } +} diff --git a/src/main/java/carpet/utils/BlockInfo.java b/src/main/java/carpet/utils/BlockInfo.java index e43d334b..cfcae6a2 100644 --- a/src/main/java/carpet/utils/BlockInfo.java +++ b/src/main/java/carpet/utils/BlockInfo.java @@ -170,7 +170,7 @@ public class BlockInfo //lst.add(Messenger.s(String.format(" - Full block: %s", block.isShapeFullCube()))); // isFullCube() ))); lst.add(Messenger.s(String.format(" - Normal cube: %s", state.isSimpleFullBlock(world, pos)))); //isNormalCube()))); lst.add(Messenger.s(String.format(" - Is liquid: %s", material.isLiquid()))); - lst.add(Messenger.s(String.format(" - Is solid: %s", material.isSolid()))); + lst.add(Messenger.s(String.format(" - Is solid: %s", state.isFullBoundsCubeForCulling()))); lst.add(Messenger.s("")); lst.add(Messenger.s(String.format(" - Light in: %d, above: %d", world.getLightLevel(pos), world.getLightLevel(pos.up())))); lst.add(Messenger.s(String.format(" - Brightness in: %.2f, above: %.2f", world.getBrightness(pos), world.getBrightness(pos.up())))); diff --git a/src/main/resources/carpet.mixins.json b/src/main/resources/carpet.mixins.json index f57c26cd..51b7d3f1 100644 --- a/src/main/resources/carpet.mixins.json +++ b/src/main/resources/carpet.mixins.json @@ -6,6 +6,7 @@ "CommandManagerMixin", "MinecraftServer_coreMixin", "MinecraftServer_tickspeedMixin", + "MinecraftServer_scarpetMixin", "PlayerListHeaderS2CPacketMixin", "WeightedPickerEntryMixin", "ExperienceOrbEntityMixin",