From dde31ca7fba0fbfbf6fb968f44ca4beea9bb3ab7 Mon Sep 17 00:00:00 2001 From: poolcritter Date: Wed, 17 Jun 2026 00:53:31 -0400 Subject: [PATCH] feat(phlib): command helpers Change-Id: I653ca39eca479019bfae611482fcfc026a6a6964 No-Tests: can't test commands from hex --- .../scala/org/eu/net/pool/hexic/main.scala | 156 +++---- util/changelog | 1 + .../scala/org/eu/net/pool/phlib/main.scala | 417 +++++++++--------- 3 files changed, 285 insertions(+), 289 deletions(-) diff --git a/project/hexic/src/main/scala/org/eu/net/pool/hexic/main.scala b/project/hexic/src/main/scala/org/eu/net/pool/hexic/main.scala index 7d77a69..b8524eb 100644 --- a/project/hexic/src/main/scala/org/eu/net/pool/hexic/main.scala +++ b/project/hexic/src/main/scala/org/eu/net/pool/hexic/main.scala @@ -47,7 +47,6 @@ import miyucomics.hexical.features.hopper.targets.SidedInventoryEndpoint import miyucomics.hexical.features.hopper.{HopperDestination, HopperEndpoint, HopperEndpointRegistry, HopperEndpointResolver, HopperSource} import miyucomics.hexical.features.pigments.{PigmentIota, PigmentIotaKt} import net.fabricmc.fabric.api.`object`.builder.v1.block.FabricBlockSettings -import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback import net.fabricmc.fabric.api.item.v1.FabricItemSettings import net.fabricmc.fabric.api.transfer.v1.fluid.{FluidConstants, FluidVariant} import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant @@ -1367,96 +1366,75 @@ def init(): Unit = override val getName: Text = Text.translatable("hexcasting.special.hexic:tuple.n", toRoman(size)) case _ => null ): SpecialHandler.Factory[? <: SpecialHandler] - CommandRegistrationCallback.EVENT.register: (d, r, e) => - def planeAction(name: String)(body: MinecraftServer ?=> UUID => Int): Unit = - d.getRoot.addChild(LiteralArgumentBuilder.literal[ServerCommandSource](name).pipe: c => - c.requires(_.hasPermissionLevel(2)) - c.argument("id", UuidArgumentType.uuid()): c => - c.executes: (ctx: CommandContext[ServerCommandSource]) => - given MinecraftServer = ctx.getSource.getServer - body(UuidArgumentType.getUuid(ctx, "id")) - c.build() - ) - planeAction("pin_plane"): id => - summon[MinecraftServer].savedPlanes += id - 1 - planeAction("un_plane"): id => - summon[MinecraftServer].savedPlanes -= id - 1 - planeAction("touch_plane"): id => - planes(id) - 1 - planeAction("unmap_plane"): id => - planeCache.remove(id) match - case Some(h) => - h.unload() - 1 - case None => throw CommandException(Text.literal("Plane not mapped")) - planeAction("delete_plane"): id => - planeCache.remove(id) match - case Some(h) => - h.delete() - 1 - case None => throw CommandException(Text.literal("Plane not mapped")) - d.getRoot.addChild(LiteralArgumentBuilder.literal[ServerCommandSource]("property").pipe: c => + def planeAction(name: String)(body: MinecraftServer ?=> UUID => Int): Unit = + Commands.server.literal(name): c => c.requires(_.hasPermissionLevel(2)) - c.`then`(LiteralArgumentBuilder.literal("get") - .`then`(RequiredArgumentBuilder.argument("property", StringArgumentType.string()) - .executes((c: CommandContext[ServerCommandSource]) => - val prop = StringArgumentType.getString(c, "property") - System.getProperty(prop) match - case null => throw CommandException(t"Property ${prop} is not set") - case s => - c.getSource.sendFeedback(() => t"Property ${prop} is set to ${s}", false) - 1 - ) - .build()) - .build() - ) - c.`then`(LiteralArgumentBuilder.literal("set") - .`then`(RequiredArgumentBuilder.argument("property", StringArgumentType.string()) - .`then`(RequiredArgumentBuilder.argument("value", StringArgumentType.string()) - .executes((c: CommandContext[ServerCommandSource]) => - val prop = StringArgumentType.getString(c, "property") - val value = StringArgumentType.getString(c, "value") - System.setProperty(prop, value) - c.getSource.sendFeedback(() => t"Changed the value of property ${prop}", true) + c.argument("id", UuidArgumentType.uuid()): (uuid, c) => + c.executes: src => + given MinecraftServer = src.getServer + body(uuid) + planeAction("pin_plane"): id => + summon[MinecraftServer].savedPlanes += id + 1 + planeAction("un_plane"): id => + summon[MinecraftServer].savedPlanes -= id + 1 + planeAction("touch_plane"): id => + planes(id) + 1 + planeAction("unmap_plane"): id => + planeCache.remove(id) match + case Some(h) => + h.unload() + 1 + case None => throw CommandException(Text.literal("Plane not mapped")) + planeAction("delete_plane"): id => + planeCache.remove(id) match + case Some(h) => + h.delete() + 1 + case None => throw CommandException(Text.literal("Plane not mapped")) + Commands.server.literal("property"): c => + c.requires(_.hasPermissionLevel(2)) + c.literal("get"): c => + c.argument("property", StringArgumentType.string()): (prop, c) => + c.executes: src => + System.getProperty(prop) match + case null => throw CommandException(t"Property $prop is not set") + case s => + src.sendFeedback(() => t"Property $prop is set to $s", false) 1 - ) - .build()) - .build()) - .build() - ) - c.`then`(LiteralArgumentBuilder.literal("remove") - .`then`(RequiredArgumentBuilder.argument("property", StringArgumentType.string()) - .build()) - .build() - ) - c.`then`(LiteralArgumentBuilder.literal[ServerCommandSource]("reload") - .executes(c => - val out = Files.newBufferedReader(Path.of("config/jvm.properties"), Charsets.UTF_8) - try - System.getProperties.load(out) - catch - case _: FileNotFoundException => throw CommandException("Properties file does not exist") - finally - out.close() - c.getSource.sendFeedback(() => "Reloaded properties from file", true) - 1 - ).build() - ) - c.`then`(LiteralArgumentBuilder.literal[ServerCommandSource]("flush") - .executes(c => - val out = Files.newBufferedWriter(Path.of("config/jvm.properties"), Charsets.UTF_8) - try - System.getProperties.store(out, null) - finally - out.close() - c.getSource.sendFeedback(() => "Saved properties to file", true) - 1 - ).build() - ) - c.build()) + c.literal("set"): c => + c.argument("property", StringArgumentType.string()): (prop, c) => + c.argument("value", StringArgumentType.string()): (value, c) => + c.executes: src => + System.setProperty(prop, value) + src.sendFeedback(() => t"Changed the value of property $prop", true) + 1 + c.literal("remove"): c => + c.argument("property", StringArgumentType.string()): (property, c) => + // nothing? + () + c.literal("reload"): c => + c.executes: src => + val out = Files.newBufferedReader(Path.of("config/jvm.properties"), Charsets.UTF_8) + try + System.getProperties.load(out) + catch + case _: FileNotFoundException => throw CommandException("Properties file does not exist") + finally + out.close() + src.sendFeedback(() => "Reloaded properties from file", true) + 1 + c.literal("flush"): c => + c.executes: src => + val out = Files.newBufferedWriter(Path.of("config/jvm.properties"), Charsets.UTF_8) + try + System.getProperties.store(out, null) + finally + out.close() + src.sendFeedback(() => "Saved properties to file", true) + 1 Registries.BLOCK("void_air") = Interop.VOID_AIR given (env: CastingEnvironment) => MinecraftServer = env.getWorld.getServer ServerLifecycleEvents.SERVER_STARTED.register: server => diff --git a/util/changelog b/util/changelog index 906c6cb..3becd1c 100644 --- a/util/changelog +++ b/util/changelog @@ -6,6 +6,7 @@ 0.1.3 added arm and hand utils 0.1.3 added casting image and cast result updaters 0.1.3 added preempting continuation chaser +0.1.3 added tools for command stuff 0.1.3 fixed Flock's Disintegration iterating maps incorrectly 0.1.3 make map xor actually work 0.1.3 uncons or unappend on empty maps no longer throw an internal exception diff --git a/util/src/main/scala/org/eu/net/pool/phlib/main.scala b/util/src/main/scala/org/eu/net/pool/phlib/main.scala index 5212b1f..2e5c5c8 100644 --- a/util/src/main/scala/org/eu/net/pool/phlib/main.scala +++ b/util/src/main/scala/org/eu/net/pool/phlib/main.scala @@ -21,14 +21,16 @@ import at.petrak.hexcasting.common.lib.HexRegistries import at.petrak.hexcasting.fabric.cc.HexCardinalComponents import com.google.gson.JsonElement import com.mojang.brigadier.StringReader -import com.mojang.brigadier.arguments.StringArgumentType -import com.mojang.brigadier.builder.{LiteralArgumentBuilder, RequiredArgumentBuilder} +import com.mojang.brigadier.arguments.{ArgumentType, StringArgumentType} +import com.mojang.brigadier.builder.{ArgumentBuilder, LiteralArgumentBuilder, RequiredArgumentBuilder} +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.tree.CommandNode import com.mojang.serialization.{Codec, DynamicOps, JsonOps} import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback import net.minecraft.command.argument.{EntityArgumentType, NbtElementArgumentType, RegistryEntryArgumentType} -import net.minecraft.command.{CommandException, EntitySelector} +import net.minecraft.command.{CommandException, CommandRegistryAccess, CommandSource, EntitySelector} import net.minecraft.nbt.* -import net.minecraft.server.command.ServerCommandSource +import net.minecraft.server.command.{CommandManager, ServerCommandSource} import net.minecraft.server.network.ServerPlayerEntity import net.minecraft.server.world.ServerWorld import net.minecraft.util.{Arm, Hand} @@ -101,6 +103,33 @@ given Conversion[CastingVM, CastingEnvironment] = _.getEnv given Conversion[String, NbtString] = NbtString.of given Conversion[NbtString, String] = _.asString +class Commands[S <: CommandSource](val builder: ArgumentBuilder[S, ?]): + def literal(name: String)(body: Commands[S] => Unit): CommandNode[S] = + val b = LiteralArgumentBuilder.literal[S](name) + body(Commands(b)) + val n = b.build() + builder `then` n + n + def argument[T: ClassTag as ct](name: String, t: ArgumentType[T])(body: (CommandContext[S] ?=> T, Commands[S]) => Unit): CommandNode[S] = + val b = RequiredArgumentBuilder.argument[S, T](name, t) + body(ctx ?=> ctx.getArgument(name, ct.runtimeClass.asInstanceOf[Class[T]]), Commands(b)) + val n = b.build() + builder `then` n + n + def requires(b: S => Boolean): Unit = + builder.requires(builder.getRequirement.and(b(_))) + def executes(body: CommandContext[S] ?=> S => Int): Unit = + builder.executes: ctx => + body(using ctx)(ctx.getSource) +object Commands: + object server: + def literal(name: String)(body: (CommandRegistryAccess, CommandManager.RegistrationEnvironment) ?=> Commands[ServerCommandSource] => Unit): Unit = + CommandRegistrationCallback.EVENT.register: (d, r, e) => + given CommandRegistryAccess = r + given CommandManager.RegistrationEnvironment = e + val b = LiteralArgumentBuilder.literal[ServerCommandSource](name) + body(Commands(b)) + d.register(b) extension (e: NbtElement) def downcast[T <: NbtElement: NbtType] = HexUtils.downcast(e, summon[NbtType[T]]) @@ -362,208 +391,196 @@ def init() = case (r, i) if r == hexXplat.getIotaTypeRegistry && i == Identifier.of("hexic", "map") => MapIota Patterns.register("empty_map", e"dqdwdqd"): Patterns.mkLiteral(MapIota()) - CommandRegistrationCallback.EVENT.register: (d, r, e) => - d.getRoot.addChild(LiteralArgumentBuilder.literal[ServerCommandSource]("gimmeiota") - .requires(c => c.hasPermissionLevel(2) || (c.getPlayer != null && c.getPlayer.isCreative)) - .`then`(RequiredArgumentBuilder.argument("type", RegistryEntryArgumentType.registryEntry(r, HexRegistries.IOTA_TYPE)) - .`then`(RequiredArgumentBuilder.argument[ServerCommandSource, NbtElement]("data", NbtElementArgumentType.nbtElement()) - .executes(c => - val t = RegistryEntryArgumentType.getRegistryEntry(c, "type", HexRegistries.IOTA_TYPE) - val d = NbtElementArgumentType.getNbtElement(c, "data") - val p = c.getSource.getPlayer - if p == null then - throw CommandException("Command must be run by a player") - try - t.value.deserialize(d, c.getSource.getWorld) match - case null => throw CommandException("Iota did not accept the given data") - case r: Iota => - p.gimmeIota(r) - c.getSource.sendFeedback(() => Text.translatable("Pushed %s to stack", try r.display catch case x: (Exception | Error) => x.getMessage), true) - 1 - case x => throw CommandException(s"${x} is not an iota") - catch - case x: IllegalArgumentException => throw CommandException(x.getMessage) - ).build() - ).build() - ).`then`( - RequiredArgumentBuilder.argument[ServerCommandSource, EntitySelector]("entity", EntityArgumentType.entity()) - .executes(c => - val p = c.getSource.getPlayer - if p == null then - throw CommandException("Command must be run by a player") - val r = EntityIota(EntityArgumentType getEntity(c, "entity")) tap p.gimmeIota - c.getSource.sendFeedback(() => Text.translatable("Pushed %s to stack", r.display), true) - 1 - ).build() - ).build() - ) - if isDev || java.lang.Boolean.getBoolean("phlib.test.enable") then - d.getRoot.addChild: - LiteralArgumentBuilder.literal[ServerCommandSource]("testHexes") - .`then`: - RequiredArgumentBuilder.argument[ServerCommandSource, String]("dir", StringArgumentType.greedyString) - .`executes`: ctx => - val path = ctx.getArgument[String]("dir", classOf[String]) - val file = Paths.get(path) - val log = Paths.get("logs", "phlib-tests.log") - Files.deleteIfExists(log) - Using.resource(Files.newBufferedWriter(log, StandardCharsets.UTF_8)): log => - def say(msg: String) = log.write(s"$msg\n") - say("===== PHLIB /testHexes REPORT =====") - say("vim: foldmethod=marker") - def walk(d: Path, lvl: Int): (Int, Int) = - val (ok, total) = Using.resource(Files.list(d))(_.toList).toSeq.map(run(_, lvl+1)).unzip - // keep a running tally of success and total - (ok.sum, total.sum) - def run(f: Path, lvl: Int): (Int, Int) = - if Files.isDirectory(f) then - say(s"{{{$lvl ${f.toFile.getName}") - walk(f, lvl) - else if f.toFile.getName.endsWith(".snbt") then - say(s"{{{$lvl ${f.toFile.getName}") - case class State(vm: Option[CastingVM] = None) - case class Scope(breakTags: Map[String, boundary.Label[?]] = Map.empty) - val content = StringNbtReader(StringReader(Files.readString(f, StandardCharsets.UTF_8))).parseElement() - def execute(element: NbtElement, scope: Scope, state: State): (State, Seq[String]) = - def reenter(element: NbtElement, path: String, newScope: Scope = scope, newState: State = state) = - val (newerState, results) = execute(element, newScope, newState) - // prefix the path to the returned errors - (newerState, results.map(s => s"$path/$s")) - def getIota(e: NbtElement): Either[Seq[String], Iota] = e match - case n: AbstractNbtNumber => Right(DoubleIota(n.doubleValue)) - case l: NbtList => - val (errors, iotas) = l.zipWithIndex.partitionMap(p => getIota(p._1).left.map(s => s"${p._2}/$s")) - if errors.nonEmpty then Left(errors.toSeq) else Right(ListIota(iotas)) - case c: NbtCompound => - if c.contains("hexcasting:type") then - IotaType.deserialize(c, ctx.getSource.getWorld) match - case null => Left(Seq("deserialization failed")) - case iota => Right(iota) - else if c.contains("sig") then - Right(PatternIota(HexPattern.fromAnglesUnchecked(c.getString("sig"), if c.contains("dir") then HexDir.valueOf(c.getString("dir")) else HexDir.values()(0)))) - else if c.contains("action") then - Identifier.tryParse(c.getString("action")) match - case null => Left(Seq("action/invalid identifier")) - case id => hexXplat.getActionRegistry.get(id) match - case null => Left(Seq("action/not found")) - case entry => Right(PatternIota(entry.prototype)) - else if c.contains("entries") then - c.get("entries") match - case l: NbtList => - ((Right(ListMap.empty[Iota, Iota]): Either[Seq[String], Map[Iota, Iota]]) /: l.zipWithIndex): (map, p) => - p._1 match - case c: NbtCompound => - val k = c.get("k") match - case null => Left("k/missing, required") - case i => getIota(i).left.map(s => s"${p._2}/k/$s") - val v = c.get("v") match - case null => Left("v/missing, required") - case i => getIota(i).left.map(s => s"${p._2}/v/$s") - (k, v) match - case (Right(k), Right(v)) => map.map(_ + (k -> v)) - case (k, v) => Left(map.left.getOrElse(Seq.empty) ++ k.left.toOption ++ v.left.toOption) - case _ => Left(map.left.getOrElse(Seq.empty) :+ s"entries/${p._2}/not a compound") - .map(MapIota.fromMap(_)(using ctx.getSource.getWorld)) - case _ => Left(Seq("entries/invalid type")) - else if c.contains("value") then getIota(c.get("value")).left.map(_.map(s => s"value/$s")) + Commands.server.literal("gimmeiota"): c => + c.requires(c => c.hasPermissionLevel(2) || (c.getPlayer != null && c.getPlayer.isCreative)) + c.argument("type", RegistryEntryArgumentType.registryEntry(summon, HexRegistries.IOTA_TYPE)): (t, c) => + c.argument("data", NbtElementArgumentType.nbtElement()): (d, c) => + c.executes: s => + val p = s.getPlayer + if p == null then + throw CommandException("Command must be run by a player") + try + t.value.deserialize(d, s.getWorld) match + case null => throw CommandException("Iota did not accept the given data") + case r: Iota => + p.gimmeIota(r) + s.sendFeedback(() => Text.translatable("Pushed %s to stack", try r.display catch case x: (Exception | Error) => x.getMessage), true) + 1 + case x => throw CommandException(s"${x} is not an iota") + catch + case x: IllegalArgumentException => throw CommandException(x.getMessage) + c.argument("entity", EntityArgumentType.entity()): (selector, c) => + c.executes: s => + val p = s.getPlayer + if p == null then + throw CommandException("Command must be run by a player") + val r = EntityIota(EntityArgumentType.getEntity(summon, "entity")) + p.gimmeIota(r) + s.sendFeedback(() => Text.translatable("Pushed %s to stack", r.display), true) + 1 + if isDev || java.lang.Boolean.getBoolean("phlib.test.enable") then + Commands.server.literal("testHexes"): c => + c.argument("dir", StringArgumentType.greedyString): (path, c) => + c.executes: src => + val file = Paths.get(path) + val log = Paths.get("logs", "phlib-tests.log") + Files.deleteIfExists(log) + Using.resource(Files.newBufferedWriter(log, StandardCharsets.UTF_8)): log => + def say(msg: String) = log.write(s"$msg\n") + say("===== PHLIB /testHexes REPORT =====") + say("vim: foldmethod=marker") + def walk(d: Path, lvl: Int): (Int, Int) = + val (ok, total) = Using.resource(Files.list(d))(_.toList).toSeq.map(run(_, lvl+1)).unzip + // keep a running tally of success and total + (ok.sum, total.sum) + def run(f: Path, lvl: Int): (Int, Int) = + if Files.isDirectory(f) then + say(s"{{{$lvl ${f.toFile.getName}") + walk(f, lvl) + else if f.toFile.getName.endsWith(".snbt") then + say(s"{{{$lvl ${f.toFile.getName}") + case class State(vm: Option[CastingVM] = None) + case class Scope(breakTags: Map[String, boundary.Label[?]] = Map.empty) + val content = StringNbtReader(StringReader(Files.readString(f, StandardCharsets.UTF_8))).parseElement() + def execute(element: NbtElement, scope: Scope, state: State): (State, Seq[String]) = + def reenter(element: NbtElement, path: String, newScope: Scope = scope, newState: State = state) = + val (newerState, results) = execute(element, newScope, newState) + // prefix the path to the returned errors + (newerState, results.map(s => s"$path/$s")) + def getIota(e: NbtElement): Either[Seq[String], Iota] = e match + case n: AbstractNbtNumber => Right(DoubleIota(n.doubleValue)) + case l: NbtList => + val (errors, iotas) = l.zipWithIndex.partitionMap(p => getIota(p._1).left.map(s => s"${p._2}/$s")) + if errors.nonEmpty then Left(errors.toSeq) else Right(ListIota(iotas)) + case c: NbtCompound => + if c.contains("hexcasting:type") then + IotaType.deserialize(c, src.getWorld) match + case null => Left(Seq("deserialization failed")) + case iota => Right(iota) + else if c.contains("sig") then + Right(PatternIota(HexPattern.fromAnglesUnchecked(c.getString("sig"), if c.contains("dir") then HexDir.valueOf(c.getString("dir")) else HexDir.values()(0)))) + else if c.contains("action") then + Identifier.tryParse(c.getString("action")) match + case null => Left(Seq("action/invalid identifier")) + case id => hexXplat.getActionRegistry.get(id) match + case null => Left(Seq("action/not found")) + case entry => Right(PatternIota(entry.prototype)) + else if c.contains("entries") then + c.get("entries") match + case l: NbtList => + ((Right(ListMap.empty[Iota, Iota]): Either[Seq[String], Map[Iota, Iota]]) /: l.zipWithIndex): (map, p) => + p._1 match + case c: NbtCompound => + val k = c.get("k") match + case null => Left("k/missing, required") + case i => getIota(i).left.map(s => s"${p._2}/k/$s") + val v = c.get("v") match + case null => Left("v/missing, required") + case i => getIota(i).left.map(s => s"${p._2}/v/$s") + (k, v) match + case (Right(k), Right(v)) => map.map(_ + (k -> v)) + case (k, v) => Left(map.left.getOrElse(Seq.empty) ++ k.left.toOption ++ v.left.toOption) + case _ => Left(map.left.getOrElse(Seq.empty) :+ s"entries/${p._2}/not a compound") + .map(MapIota.fromMap(_)(using src.getWorld)) + case _ => Left(Seq("entries/invalid type")) + else if c.contains("value") then getIota(c.get("value")).left.map(_.map(s => s"value/$s")) + else + Left(Seq("no recognized iota format")) + element match + case c: NbtCompound => + if c.contains("if") then + val (newState, errors) = execute(c.get("if"), scope, state) + if errors.isEmpty then + if c.contains("then") then + reenter(c.get("then"), "then", newState = newState) + else + (newState, Seq.empty) else - Left(Seq("no recognized iota format")) - element match - case c: NbtCompound => - if c.contains("if") then - val (newState, errors) = execute(c.get("if"), scope, state) - if errors.isEmpty then - if c.contains("then") then - reenter(c.get("then"), "then", newState = newState) - else - (newState, Seq.empty) + if c.contains("else") then + reenter(c.get("else"), "else", newState = newState) else - if c.contains("else") then - reenter(c.get("else"), "else", newState = newState) + (newState, Seq.empty) + else if c.contains("throw") then + (state, Seq(c.getString("throw"))) + else if c.contains("vm") then + c.getString("vm") match + case "discard" => state.vm.fold(state, Seq("vm/no CastingVM available"))(vm => (state.copy(vm = None), Seq.empty)) + case "staff" => state.vm.fold(state.copy(vm = Some(CastingVM(CastingImage(), StaffCastEnv(src.getPlayer, if c.contains("alt") then Hand.OFF_HAND else Hand.MAIN_HAND)))), Seq.empty)(vm => (state, Seq("vm/not overwriting existing CastingVM"))) + case "copy" => state.vm.fold(state, Seq("vm/no CastingVM available"))(vm => (state.copy(vm = Some(CastingVM(vm.getImage, vm.getEnv))), Seq.empty)) + case s => (state, Seq(s"vm/bad action $s")) + else if c.contains("push") then + getIota(c.get("push")) match + case Left(err) => (state, err.map(s => s"push/$s")) + case Right(iota) => state.vm.fold(state, Seq("push/no CastingVM available"))(vm => { vm.setImage(vm.getImage.withStack(_ :+ iota)); (state, Seq.empty) }) + else if c.contains("cast") then + getIota(c.get("cast")) match + case Left(err) => (state, err.map(s => s"cast/$s")) + case Right(iota) => state.vm.fold(state, Seq("cast/no CastingVM available")): vm => + val result = vm.queueExecuteAndWrapIota(iota, src.getWorld) + if !result.getResolutionType.getSuccess then + (state, Seq("cast/mishap occurred, see chat for details")) else - (newState, Seq.empty) - else if c.contains("throw") then - (state, Seq(c.getString("throw"))) - else if c.contains("vm") then - c.getString("vm") match - case "discard" => state.vm.fold(state, Seq("vm/no CastingVM available"))(vm => (state.copy(vm = None), Seq.empty)) - case "staff" => state.vm.fold(state.copy(vm = Some(CastingVM(CastingImage(), StaffCastEnv(ctx.getSource.getPlayer, if c.contains("alt") then Hand.OFF_HAND else Hand.MAIN_HAND)))), Seq.empty)(vm => (state, Seq("vm/not overwriting existing CastingVM"))) - case "copy" => state.vm.fold(state, Seq("vm/no CastingVM available"))(vm => (state.copy(vm = Some(CastingVM(vm.getImage, vm.getEnv))), Seq.empty)) - case s => (state, Seq(s"vm/bad action $s")) - else if c.contains("push") then - getIota(c.get("push")) match - case Left(err) => (state, err.map(s => s"push/$s")) - case Right(iota) => state.vm.fold(state, Seq("push/no CastingVM available"))(vm => { vm.setImage(vm.getImage.withStack(_ :+ iota)); (state, Seq.empty) }) - else if c.contains("cast") then - getIota(c.get("cast")) match - case Left(err) => (state, err.map(s => s"cast/$s")) - case Right(iota) => state.vm.fold(state, Seq("cast/no CastingVM available")): vm => - val result = vm.queueExecuteAndWrapIota(iota, ctx.getSource.getWorld) - if !result.getResolutionType.getSuccess then - (state, Seq("cast/mishap occurred, see chat for details")) - else - (state, Seq()) - else if c.contains("check") then - getIota(c.get("check")) match - case Left(err) => (state, err.map(s => s"check/$s")) - case Right(iota) => state.vm.fold(state, Seq("check/no CastingVM available")): vm => - val img = vm.getImage - img.getStack.toSeq match - case init :+ last => - vm.setImage(img(stack = init)) - if if c.contains("structural") then IotaType.serialize(iota) == IotaType.serialize(last) else Iota.tolerates(iota, last) then - (state, Seq.empty) - else - (state, Seq(s"check/invalid iota: ${IotaType.serialize(last)}")) - case Seq() => - (state, Seq(s"check/empty stack")) - else if c.contains("pop") then - c.get("pop") match - case n: AbstractNbtNumber => state.vm.fold(state, Seq("pop/no CastingVM available")): vm => - vm.setImage(vm.getImage.withStack(if n.intValue <= 0 then _.takeRight(-n.intValue) else _.dropRight(n.intValue))) - // if you want an error, use checkStack + (state, Seq()) + else if c.contains("check") then + getIota(c.get("check")) match + case Left(err) => (state, err.map(s => s"check/$s")) + case Right(iota) => state.vm.fold(state, Seq("check/no CastingVM available")): vm => + val img = vm.getImage + img.getStack.toSeq match + case init :+ last => + vm.setImage(img(stack = init)) + if if c.contains("structural") then IotaType.serialize(iota) == IotaType.serialize(last) else Iota.tolerates(iota, last) then + (state, Seq.empty) + else + (state, Seq(s"check/invalid iota: ${IotaType.serialize(last)}")) + case Seq() => + (state, Seq(s"check/empty stack")) + else if c.contains("pop") then + c.get("pop") match + case n: AbstractNbtNumber => state.vm.fold(state, Seq("pop/no CastingVM available")): vm => + vm.setImage(vm.getImage.withStack(if n.intValue <= 0 then _.takeRight(-n.intValue) else _.dropRight(n.intValue))) + // if you want an error, use checkStack + (state, Seq.empty) + case _ => (state, Seq("pop/expected number")) + else if c.contains("checkStack") then + c.get("checkStack") match + case n: AbstractNbtNumber => state.vm.fold(state, Seq("checkStack/no CastingVM available")): vm => + if vm.getImage.getStack.size == n.intValue then (state, Seq.empty) - case _ => (state, Seq("pop/expected number")) - else if c.contains("checkStack") then - c.get("checkStack") match - case n: AbstractNbtNumber => state.vm.fold(state, Seq("checkStack/no CastingVM available")): vm => - if vm.getImage.getStack.size == n.intValue then - (state, Seq.empty) - else - (state, Seq(s"checkStack/stack size mismatch: found ${vm.getImage.getStack.size} elements")) - case _ => (state, Seq("checkStack/expected number")) - else - (state, Seq("no recognized action")) - case s: NbtList => - ((state, Seq.empty[String]) /: s.zipWithIndex): (r, p) => - val t = reenter(p._1, p._2.toString, newState = r._1) - (t._1, r._2 ++ t._2) - val isFailingTest = f.toFile.getName.endsWith(".fail.snbt") - execute(content, Scope(), State())._2.trying match - case Success(errors) => - if errors.nonEmpty then - if !isFailingTest then say("=== FAILED ===") - errors.foreach(say) - else if isFailingTest then - say("=== FAILED ===\nthis test was expected to fail, but no errors occurred") - if errors.nonEmpty != isFailingTest then - (0, 1) + else + (state, Seq(s"checkStack/stack size mismatch: found ${vm.getImage.getStack.size} elements")) + case _ => (state, Seq("checkStack/expected number")) else - (1, 1) - case Failure(exception) => - say("=== FAILED ===\nthrew the following exception:") - val p = PrintWriter(log) - exception.printStackTrace(p) - p.flush() - // never suppress panics + (state, Seq("no recognized action")) + case s: NbtList => + ((state, Seq.empty[String]) /: s.zipWithIndex): (r, p) => + val t = reenter(p._1, p._2.toString, newState = r._1) + (t._1, r._2 ++ t._2) + val isFailingTest = f.toFile.getName.endsWith(".fail.snbt") + execute(content, Scope(), State())._2.trying match + case Success(errors) => + if errors.nonEmpty then + if !isFailingTest then say("=== FAILED ===") + errors.foreach(say) + else if isFailingTest then + say("=== FAILED ===\nthis test was expected to fail, but no errors occurred") + if errors.nonEmpty != isFailingTest then (0, 1) - else - // silently skip non-.snbt files - (0, 0) - val (ok, total) = walk(file, 0) - ctx.getSource.sendMessage(t"$ok/$total tests passed") - ok - .build() + else + (1, 1) + case Failure(exception) => + say("=== FAILED ===\nthrew the following exception:") + val p = PrintWriter(log) + exception.printStackTrace(p) + p.flush() + // never suppress panics + (0, 1) + else + // silently skip non-.snbt files + (0, 0) + val (ok, total) = walk(file, 0) + src.sendMessage(t"$ok/$total tests passed") + ok object Events: def partialEvent[T, R]: Event[PartialFunction[T, R]] = EventFactory.createArrayBacked(classOf, PartialFunction.empty, ary => (PartialFunction.empty /: ary) (_ orElse _)) -- 2.51.2