From 87dfad026ed17fde83cfe7f1115bfb928cd12f0e Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Wed, 14 Aug 2024 22:41:31 +0100 Subject: [PATCH] Add a test for exploding turtles There's been a couple of bug reports in the past where the game would crash if a turtle is destroyed while breaking a block (typically due to the block exploding). This commit adds a test, to ensure that this is handled gracefully. I'm not entirely sure this is testing the right thing. Looking at the issues in question, it doesn't look like I ever managed to reproduce the bug. However, it's hopefully at least a quick sanity test to check we never break this case. --- .../computercraft/gametest/Turtle_Test.kt | 14 ++ .../computercraft/gametest/core/TestHooks.kt | 25 ++++ .../turtle_test.breaks_exploding_block.snbt | 139 ++++++++++++++++++ .../computercraft/gametest/core/TestMod.java | 2 + .../computercraft/gametest/core/TestMod.java | 5 + 5 files changed, 185 insertions(+) create mode 100644 projects/common/src/testMod/resources/data/cctest/structures/turtle_test.breaks_exploding_block.snbt diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt index c02d84013..227ece5d0 100644 --- a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt +++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Turtle_Test.kt @@ -693,6 +693,20 @@ class Turtle_Test { } } + /** + * Tests a turtle can break a block that explodes, causing the turtle itself to explode. + * + * This attempts to test [#585](https://github.com/cc-tweaked/CC-Tweaked/issues/585) and other similar issues. It's + * not clear if this is a good test case, as that bug does not seem reliably reproducible, but it's at least a good + * sanity check. + */ + @GameTest + fun Breaks_exploding_block(context: GameTestHelper) = context.sequence { + thenOnComputer { turtle.dig(Optional.empty()) } + thenIdle(2) + thenExecute { context.assertItemEntityPresent(ModRegistry.Items.TURTLE_NORMAL.get(), BlockPos(2, 2, 2), 1.0) } + } + /** * Render turtles as an item. */ diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/core/TestHooks.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/core/TestHooks.kt index e6ac3895d..149f947ce 100644 --- a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/core/TestHooks.kt +++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/core/TestHooks.kt @@ -13,7 +13,13 @@ import dan200.computercraft.shared.computer.core.ServerContext import net.minecraft.core.BlockPos import net.minecraft.gametest.framework.* import net.minecraft.server.MinecraftServer +import net.minecraft.server.level.ServerLevel import net.minecraft.world.level.GameRules +import net.minecraft.world.level.Level +import net.minecraft.world.level.LevelAccessor +import net.minecraft.world.level.block.Blocks +import net.minecraft.world.level.block.state.BlockState +import net.minecraft.world.phys.Vec3 import org.slf4j.Logger import org.slf4j.LoggerFactory import java.io.File @@ -173,4 +179,23 @@ object TestHooks { throw RuntimeException(e) } } + + /** + * Adds a hook that makes breaking a bone block spawn an explosion. + * + * It would be more Correct to register a custom block, but that's quite a lot of work, and doesn't seem worth it + * for test code. + * + * See also [Turtle_Test.Breaks_exploding_block]. + */ + @JvmStatic + fun onBeforeDestroyBlock(level: LevelAccessor, pos: BlockPos, state: BlockState): Boolean { + if (state.block === Blocks.BONE_BLOCK && level is ServerLevel) { + val explosionPos = Vec3.atCenterOf(pos) + level.explode(null, explosionPos.x, explosionPos.y, explosionPos.z, 4.0f, Level.ExplosionInteraction.TNT) + return true + } + + return false + } } diff --git a/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.breaks_exploding_block.snbt b/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.breaks_exploding_block.snbt new file mode 100644 index 000000000..2c20fc77a --- /dev/null +++ b/projects/common/src/testMod/resources/data/cctest/structures/turtle_test.breaks_exploding_block.snbt @@ -0,0 +1,139 @@ +{ + DataVersion: 3465, + size: [5, 5, 5], + data: [ + {pos: [0, 0, 0], state: "minecraft:obsidian"}, + {pos: [0, 0, 1], state: "minecraft:obsidian"}, + {pos: [0, 0, 2], state: "minecraft:obsidian"}, + {pos: [0, 0, 3], state: "minecraft:obsidian"}, + {pos: [0, 0, 4], state: "minecraft:obsidian"}, + {pos: [1, 0, 0], state: "minecraft:obsidian"}, + {pos: [1, 0, 1], state: "minecraft:obsidian"}, + {pos: [1, 0, 2], state: "minecraft:obsidian"}, + {pos: [1, 0, 3], state: "minecraft:obsidian"}, + {pos: [1, 0, 4], state: "minecraft:obsidian"}, + {pos: [2, 0, 0], state: "minecraft:obsidian"}, + {pos: [2, 0, 1], state: "minecraft:obsidian"}, + {pos: [2, 0, 2], state: "minecraft:obsidian"}, + {pos: [2, 0, 3], state: "minecraft:obsidian"}, + {pos: [2, 0, 4], state: "minecraft:obsidian"}, + {pos: [3, 0, 0], state: "minecraft:obsidian"}, + {pos: [3, 0, 1], state: "minecraft:obsidian"}, + {pos: [3, 0, 2], state: "minecraft:obsidian"}, + {pos: [3, 0, 3], state: "minecraft:obsidian"}, + {pos: [3, 0, 4], state: "minecraft:obsidian"}, + {pos: [4, 0, 0], state: "minecraft:obsidian"}, + {pos: [4, 0, 1], state: "minecraft:obsidian"}, + {pos: [4, 0, 2], state: "minecraft:obsidian"}, + {pos: [4, 0, 3], state: "minecraft:obsidian"}, + {pos: [4, 0, 4], state: "minecraft:obsidian"}, + {pos: [0, 1, 0], state: "minecraft:barrier"}, + {pos: [0, 1, 1], state: "minecraft:barrier"}, + {pos: [0, 1, 2], state: "minecraft:barrier"}, + {pos: [0, 1, 3], state: "minecraft:barrier"}, + {pos: [0, 1, 4], state: "minecraft:barrier"}, + {pos: [1, 1, 0], state: "minecraft:barrier"}, + {pos: [1, 1, 1], state: "minecraft:air"}, + {pos: [1, 1, 2], state: "minecraft:air"}, + {pos: [1, 1, 3], state: "minecraft:air"}, + {pos: [1, 1, 4], state: "minecraft:barrier"}, + {pos: [2, 1, 0], state: "minecraft:barrier"}, + {pos: [2, 1, 1], state: "minecraft:air"}, + {pos: [2, 1, 2], state: "computercraft:turtle_normal{facing:south,waterlogged:false}", nbt: {ComputerId: 1, Fuel: 0, Items: [], Label: "turtle_test.breaks_exploding_block", LeftUpgrade: "minecraft:diamond_pickaxe", LeftUpgradeNbt: {Tag: {Damage: 0}}, On: 1b, Owner: {LowerId: -5670393268852517359L, Name: "Player172", UpperId: 3578583684139923613L}, Slot: 0, id: "computercraft:turtle_normal"}}, + {pos: [2, 1, 3], state: "minecraft:bone_block{axis:y}"}, + {pos: [2, 1, 4], state: "minecraft:barrier"}, + {pos: [3, 1, 0], state: "minecraft:barrier"}, + {pos: [3, 1, 1], state: "minecraft:air"}, + {pos: [3, 1, 2], state: "minecraft:air"}, + {pos: [3, 1, 3], state: "minecraft:air"}, + {pos: [3, 1, 4], state: "minecraft:barrier"}, + {pos: [4, 1, 0], state: "minecraft:barrier"}, + {pos: [4, 1, 1], state: "minecraft:barrier"}, + {pos: [4, 1, 2], state: "minecraft:barrier"}, + {pos: [4, 1, 3], state: "minecraft:barrier"}, + {pos: [4, 1, 4], state: "minecraft:barrier"}, + {pos: [0, 2, 0], state: "minecraft:barrier"}, + {pos: [0, 2, 1], state: "minecraft:barrier"}, + {pos: [0, 2, 2], state: "minecraft:air"}, + {pos: [0, 2, 3], state: "minecraft:barrier"}, + {pos: [0, 2, 4], state: "minecraft:barrier"}, + {pos: [1, 2, 0], state: "minecraft:barrier"}, + {pos: [1, 2, 1], state: "minecraft:air"}, + {pos: [1, 2, 2], state: "minecraft:air"}, + {pos: [1, 2, 3], state: "minecraft:air"}, + {pos: [1, 2, 4], state: "minecraft:barrier"}, + {pos: [2, 2, 0], state: "minecraft:barrier"}, + {pos: [2, 2, 1], state: "minecraft:air"}, + {pos: [2, 2, 2], state: "minecraft:air"}, + {pos: [2, 2, 3], state: "minecraft:air"}, + {pos: [2, 2, 4], state: "minecraft:barrier"}, + {pos: [3, 2, 0], state: "minecraft:barrier"}, + {pos: [3, 2, 1], state: "minecraft:air"}, + {pos: [3, 2, 2], state: "minecraft:air"}, + {pos: [3, 2, 3], state: "minecraft:air"}, + {pos: [3, 2, 4], state: "minecraft:barrier"}, + {pos: [4, 2, 0], state: "minecraft:barrier"}, + {pos: [4, 2, 1], state: "minecraft:barrier"}, + {pos: [4, 2, 2], state: "minecraft:barrier"}, + {pos: [4, 2, 3], state: "minecraft:barrier"}, + {pos: [4, 2, 4], state: "minecraft:barrier"}, + {pos: [0, 3, 0], state: "minecraft:air"}, + {pos: [0, 3, 1], state: "minecraft:air"}, + {pos: [0, 3, 2], state: "minecraft:air"}, + {pos: [0, 3, 3], state: "minecraft:air"}, + {pos: [0, 3, 4], state: "minecraft:air"}, + {pos: [1, 3, 0], state: "minecraft:air"}, + {pos: [1, 3, 1], state: "minecraft:air"}, + {pos: [1, 3, 2], state: "minecraft:air"}, + {pos: [1, 3, 3], state: "minecraft:air"}, + {pos: [1, 3, 4], state: "minecraft:air"}, + {pos: [2, 3, 0], state: "minecraft:air"}, + {pos: [2, 3, 1], state: "minecraft:air"}, + {pos: [2, 3, 2], state: "minecraft:air"}, + {pos: [2, 3, 3], state: "minecraft:air"}, + {pos: [2, 3, 4], state: "minecraft:air"}, + {pos: [3, 3, 0], state: "minecraft:air"}, + {pos: [3, 3, 1], state: "minecraft:air"}, + {pos: [3, 3, 2], state: "minecraft:air"}, + {pos: [3, 3, 3], state: "minecraft:air"}, + {pos: [3, 3, 4], state: "minecraft:air"}, + {pos: [4, 3, 0], state: "minecraft:air"}, + {pos: [4, 3, 1], state: "minecraft:air"}, + {pos: [4, 3, 2], state: "minecraft:air"}, + {pos: [4, 3, 3], state: "minecraft:air"}, + {pos: [4, 3, 4], state: "minecraft:air"}, + {pos: [0, 4, 0], state: "minecraft:air"}, + {pos: [0, 4, 1], state: "minecraft:air"}, + {pos: [0, 4, 2], state: "minecraft:air"}, + {pos: [0, 4, 3], state: "minecraft:air"}, + {pos: [0, 4, 4], state: "minecraft:air"}, + {pos: [1, 4, 0], state: "minecraft:air"}, + {pos: [1, 4, 1], state: "minecraft:air"}, + {pos: [1, 4, 2], state: "minecraft:air"}, + {pos: [1, 4, 3], state: "minecraft:air"}, + {pos: [1, 4, 4], state: "minecraft:air"}, + {pos: [2, 4, 0], state: "minecraft:air"}, + {pos: [2, 4, 1], state: "minecraft:air"}, + {pos: [2, 4, 2], state: "minecraft:air"}, + {pos: [2, 4, 3], state: "minecraft:air"}, + {pos: [2, 4, 4], state: "minecraft:air"}, + {pos: [3, 4, 0], state: "minecraft:air"}, + {pos: [3, 4, 1], state: "minecraft:air"}, + {pos: [3, 4, 2], state: "minecraft:air"}, + {pos: [3, 4, 3], state: "minecraft:air"}, + {pos: [3, 4, 4], state: "minecraft:air"}, + {pos: [4, 4, 0], state: "minecraft:air"}, + {pos: [4, 4, 1], state: "minecraft:air"}, + {pos: [4, 4, 2], state: "minecraft:air"}, + {pos: [4, 4, 3], state: "minecraft:air"}, + {pos: [4, 4, 4], state: "minecraft:air"} + ], + entities: [], + palette: [ + "minecraft:obsidian", + "minecraft:barrier", + "minecraft:bone_block{axis:y}", + "minecraft:air", + "computercraft:turtle_normal{facing:south,waterlogged:false}" + ] +} diff --git a/projects/fabric/src/testMod/java/dan200/computercraft/gametest/core/TestMod.java b/projects/fabric/src/testMod/java/dan200/computercraft/gametest/core/TestMod.java index 4a2ba819d..ac9068030 100644 --- a/projects/fabric/src/testMod/java/dan200/computercraft/gametest/core/TestMod.java +++ b/projects/fabric/src/testMod/java/dan200/computercraft/gametest/core/TestMod.java @@ -14,6 +14,7 @@ import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; import net.fabricmc.fabric.api.event.Event; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents; import net.minecraft.gametest.framework.GameTestRegistry; import net.minecraft.resources.ResourceLocation; @@ -26,6 +27,7 @@ public class TestMod implements ModInitializer, ClientModInitializer { ServerLifecycleEvents.SERVER_STARTED.addPhaseOrdering(Event.DEFAULT_PHASE, phase); ServerLifecycleEvents.SERVER_STARTED.register(phase, TestHooks::onServerStarted); CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> CCTestCommand.register(dispatcher)); + PlayerBlockBreakEvents.BEFORE.register((level, player, pos, state, blockEntity) -> !TestHooks.onBeforeDestroyBlock(level, pos, state)); TestHooks.loadTests(GameTestRegistry::register); } diff --git a/projects/forge/src/testMod/java/dan200/computercraft/gametest/core/TestMod.java b/projects/forge/src/testMod/java/dan200/computercraft/gametest/core/TestMod.java index ad295ffd1..67cf999bd 100644 --- a/projects/forge/src/testMod/java/dan200/computercraft/gametest/core/TestMod.java +++ b/projects/forge/src/testMod/java/dan200/computercraft/gametest/core/TestMod.java @@ -12,6 +12,7 @@ import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.RegisterCommandsEvent; import net.minecraftforge.event.RegisterGameTestsEvent; import net.minecraftforge.event.TickEvent; +import net.minecraftforge.event.level.BlockEvent; import net.minecraftforge.event.server.ServerStartedEvent; import net.minecraftforge.eventbus.api.EventPriority; import net.minecraftforge.fml.DistExecutor; @@ -26,6 +27,10 @@ public class TestMod { var bus = MinecraftForge.EVENT_BUS; bus.addListener(EventPriority.LOW, (ServerStartedEvent e) -> TestHooks.onServerStarted(e.getServer())); bus.addListener((RegisterCommandsEvent e) -> CCTestCommand.register(e.getDispatcher())); + bus.addListener((BlockEvent.BreakEvent e) -> { + if (TestHooks.onBeforeDestroyBlock(e.getLevel(), e.getPos(), e.getState())) e.setCanceled(true); + }); + DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> TestMod::onInitializeClient); var modBus = FMLJavaModLoadingContext.get().getModEventBus();