From cab1bfde40b5d034c117e809db1097ab40f884b1 Mon Sep 17 00:00:00 2001 From: Ocelot Date: Wed, 20 May 2026 20:17:40 -0600 Subject: [PATCH 1/7] Remove block entities on assembly to prevent duplication This also includes a tag for blocks that depend on being properly removed --- .../sable/api/SubLevelAssemblyHelper.java | 15 ++++++++++----- .../java/dev/ryanhcode/sable/index/SableTags.java | 5 +++++ .../data/sable/tags/block/remove_on_assembly.json | 5 +++++ 3 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 common/src/main/resources/data/sable/tags/block/remove_on_assembly.json diff --git a/common/src/main/java/dev/ryanhcode/sable/api/SubLevelAssemblyHelper.java b/common/src/main/java/dev/ryanhcode/sable/api/SubLevelAssemblyHelper.java index c95b7400..b67435db 100644 --- a/common/src/main/java/dev/ryanhcode/sable/api/SubLevelAssemblyHelper.java +++ b/common/src/main/java/dev/ryanhcode/sable/api/SubLevelAssemblyHelper.java @@ -10,6 +10,7 @@ import dev.ryanhcode.sable.companion.math.BoundingBox3ic; import dev.ryanhcode.sable.companion.math.JOMLConversion; import dev.ryanhcode.sable.companion.math.Pose3d; +import dev.ryanhcode.sable.index.SableTags; import dev.ryanhcode.sable.platform.SableAssemblyPlatform; import dev.ryanhcode.sable.sublevel.ServerSubLevel; import dev.ryanhcode.sable.sublevel.SubLevel; @@ -348,11 +349,15 @@ public static void moveBlocks(final ServerLevel level, final AssemblyTransform t tag.putInt("z", newPos.getZ()); } - if (blockEntity instanceof final RandomizableContainer container) { - container.setLootTable(null); - } - if (blockEntity instanceof final Clearable clearable) { - clearable.clearContent(); + if (state.is(SableTags.REMOVE_ON_ASSEMBLY)) { + if (blockEntity instanceof final RandomizableContainer container) { + container.setLootTable(null); + } + if (blockEntity instanceof final Clearable clearable) { + clearable.clearContent(); + } + } else { + level.removeBlockEntity(block); } final LevelChunk chunk = resultingAccelerator.getChunk(SectionPos.blockToSectionCoord(newPos.getX()), SectionPos.blockToSectionCoord(newPos.getZ())); diff --git a/common/src/main/java/dev/ryanhcode/sable/index/SableTags.java b/common/src/main/java/dev/ryanhcode/sable/index/SableTags.java index 22dad65a..e8f45ab5 100644 --- a/common/src/main/java/dev/ryanhcode/sable/index/SableTags.java +++ b/common/src/main/java/dev/ryanhcode/sable/index/SableTags.java @@ -36,6 +36,11 @@ public class SableTags { Sable.sablePath("paddles") ); + public static final TagKey REMOVE_ON_ASSEMBLY = TagKey.create( + Registries.BLOCK, + Sable.sablePath("remove_on_assembly") + ); + public static void register() { // no-op } diff --git a/common/src/main/resources/data/sable/tags/block/remove_on_assembly.json b/common/src/main/resources/data/sable/tags/block/remove_on_assembly.json new file mode 100644 index 00000000..a22514f6 --- /dev/null +++ b/common/src/main/resources/data/sable/tags/block/remove_on_assembly.json @@ -0,0 +1,5 @@ +{ + "replace": false, + "values": [ + ] +} \ No newline at end of file From 2398f820a145993e28b92b8c34408879332a7efd Mon Sep 17 00:00:00 2001 From: Ocelot Date: Thu, 21 May 2026 00:10:05 -0600 Subject: [PATCH 2/7] Add silent_assembly_removal tag Allows some blocks to be overridden when they do no implement Clearable correctly --- .../sable/api/SubLevelAssemblyHelper.java | 16 ++++++++++------ .../dev/ryanhcode/sable/index/SableTags.java | 4 ++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/common/src/main/java/dev/ryanhcode/sable/api/SubLevelAssemblyHelper.java b/common/src/main/java/dev/ryanhcode/sable/api/SubLevelAssemblyHelper.java index b67435db..b864c07a 100644 --- a/common/src/main/java/dev/ryanhcode/sable/api/SubLevelAssemblyHelper.java +++ b/common/src/main/java/dev/ryanhcode/sable/api/SubLevelAssemblyHelper.java @@ -349,15 +349,19 @@ public static void moveBlocks(final ServerLevel level, final AssemblyTransform t tag.putInt("z", newPos.getZ()); } - if (state.is(SableTags.REMOVE_ON_ASSEMBLY)) { + if (state.is(SableTags.SILENT_ASSEMBLY_REMOVAL)) { + level.removeBlockEntity(block); + } else { + // This is the "correct" way to remove a block from the world, but many mods do not implement + // Clearable correctly. The above tag exists to allow this issue to be "fixed" on a case-by-case + // basis without updating a mod's code + // + // A real solution is to implement Clearable on all block entities that can be cleared in the + // same way as Vanilla MC. See SetBlockCommand if (blockEntity instanceof final RandomizableContainer container) { container.setLootTable(null); } - if (blockEntity instanceof final Clearable clearable) { - clearable.clearContent(); - } - } else { - level.removeBlockEntity(block); + Clearable.tryClear(blockEntity); } final LevelChunk chunk = resultingAccelerator.getChunk(SectionPos.blockToSectionCoord(newPos.getX()), SectionPos.blockToSectionCoord(newPos.getZ())); diff --git a/common/src/main/java/dev/ryanhcode/sable/index/SableTags.java b/common/src/main/java/dev/ryanhcode/sable/index/SableTags.java index e8f45ab5..490f8dcc 100644 --- a/common/src/main/java/dev/ryanhcode/sable/index/SableTags.java +++ b/common/src/main/java/dev/ryanhcode/sable/index/SableTags.java @@ -36,9 +36,9 @@ public class SableTags { Sable.sablePath("paddles") ); - public static final TagKey REMOVE_ON_ASSEMBLY = TagKey.create( + public static final TagKey SILENT_ASSEMBLY_REMOVAL = TagKey.create( Registries.BLOCK, - Sable.sablePath("remove_on_assembly") + Sable.sablePath("silent_assembly_removal") ); public static void register() { From d818a9d5358af649becc004b3f2cf7afb84cb95b Mon Sep 17 00:00:00 2001 From: Ocelot Date: Thu, 21 May 2026 00:10:32 -0600 Subject: [PATCH 3/7] Add game test for assembling all blocks --- .../structure/assemblytest.allblocks.nbt | Bin 0 -> 445 bytes ...mbly.json => silent_assembly_removal.json} | 0 gradle.properties | 2 +- .../sable/neoforge/gametest/AssemblyTest.java | 202 +++++++++++++++++- .../neoforge/gametest/SableTestHelper.java | 11 +- 5 files changed, 204 insertions(+), 11 deletions(-) create mode 100644 common/src/main/resources/data/sable/structure/assemblytest.allblocks.nbt rename common/src/main/resources/data/sable/tags/block/{remove_on_assembly.json => silent_assembly_removal.json} (100%) diff --git a/common/src/main/resources/data/sable/structure/assemblytest.allblocks.nbt b/common/src/main/resources/data/sable/structure/assemblytest.allblocks.nbt new file mode 100644 index 0000000000000000000000000000000000000000..6c9be514262361227b6f994f32fafcff4a298a07 GIT binary patch literal 445 zcmb2|=3oGW|Gm=>dL4EUalOB%^6E0ByqF)l>tc78L?_7@W=x(Wsqy3Mq>D4XGK=#U zESWHQ`r)}d8*^NwHr}Xb*H`+Suc~A=Uplf~@#{+dz-NmmuD-s0XXY=Up^=Kj{ULNgefx~cUo>UI6k+qOkBeKOkh%<`m-B{WK@g7l+vNZy4M>G zjtg5>E2aBCdDA(k_r%Q(7-6}4j>T)%V}dF za<0`8Mr=LL(rnc+hZ~isFD+#M9H%UC<=~(B2?6XAixc5Qoc5yT6FUu7z=$*lF#BEh zhUXK3_CrjzEZui+d%WUOgL=7NTW=Ka=Ki$db^8V1x7AfPHD5Gyb{D=_Hla!6(LawP qi8eW(&C~W)Rqwg=ZF0rV%B skip = Set.of( +// ResourceLocation.fromNamespaceAndPath("create", "millstone"), +// ResourceLocation.fromNamespaceAndPath("create", "brass_tunnel") + ); + final Direction[] capabilityDirections = { + null, + Direction.DOWN, + Direction.UP, + Direction.NORTH, + Direction.SOUTH, + Direction.WEST, + Direction.EAST + }; + + final ServerLevel level = helper.getLevel(); + final ServerSubLevelContainer plotContainer = SubLevelContainer.getContainer(level); + if (plotContainer == null) { + throw new IllegalStateException("Plot container not found in level"); + } + + final SubLevelPhysicsSystem physicsSystem = plotContainer.physicsSystem(); + final ItemStack insertStack = new ItemStack(Items.OCELOT_SPAWN_EGG); + + final BlockPos pos = helper.absolutePos(new BlockPos(2, 3, 2)); + final BlockPos onPos = pos.below(); + final List invalidStates = new LinkedList<>(); + final List failures = new LinkedList<>(); + +// long tick = 0; + for (final Map.Entry, Block> entry : BuiltInRegistries.BLOCK.entrySet()) { + if (skip.contains(entry.getKey().location())) { + continue; + } + + final Block block = entry.getValue(); + for (final BlockState state : block.getStateDefinition().getPossibleStates()) { +// helper.runAtTickTime(tick += 3, () -> { + level.setBlock(onPos, Blocks.STONE.defaultBlockState(), 2); + level.setBlock(pos, state, 2); + + // The block was unstable and can't be placed in this configuration + if (level.getBlockState(pos) != state) { + helper.killAllEntities(); + invalidStates.add(state); + return; + } + + // Bug with lecterns. If the block state is set, but there isn't a book it will try to drop an air item + if (state.is(Blocks.LECTERN) && state.getValue(BlockStateProperties.HAS_BOOK)) { + return; + } + + final List startEntities = level.getEntities(EntityTypeTest.forClass(Entity.class), helper.getBounds(), Entity::isAlive); + + int insertCount = 0; + for (@Nullable final Direction direction : capabilityDirections) { + final IItemHandler inventory = level.getCapability(Capabilities.ItemHandler.BLOCK, pos, state, level.getBlockEntity(pos), direction); + if (inventory != null) { + final int slots = inventory.getSlots(); + if (inventory instanceof final IItemHandlerModifiable modifiable) { + for (int i = 0; i < slots; i++) { + try { + modifiable.setStackInSlot(i, insertStack.copy()); + insertCount++; + } catch (final Throwable ignored) { + if (!inventory.insertItem(i, insertStack.copy(), false).equals(insertStack)) { + insertCount++; + } + } + } + } else { + for (int i = 0; i < slots; i++) { + if (!inventory.insertItem(i, insertStack.copy(), false).equals(insertStack)) { + insertCount++; + } + } + } + } + } + + if (insertCount == 0 && !state.hasBlockEntity()) { + return; + } + +// helper.runAfterDelay(1, () -> { + final ServerSubLevel subLevel = SubLevelAssemblyHelper.assembleBlocks(level, pos, List.of(pos, onPos), new BoundingBox3i( + onPos.getX(), + onPos.getY(), + onPos.getZ(), + pos.getX() + 1, + pos.getY() + 1, + pos.getZ() + 1)); + physicsSystem.getPipeline().teleport(subLevel, + new Vector3d(pos.getX() + 0.5, + pos.getY() + 1.0, + pos.getZ() + 0.5), + helper.getTestRotation().rotation().transformation().getNormalizedRotation(new Quaterniond())); + + final EmbeddedPlotLevelAccessor subLevelAccessor = subLevel.getPlot().getEmbeddedLevelAccessor(); + final BlockEntity sublevelBlockEntity = subLevelAccessor.getBlockEntity(BlockPos.ZERO); + + final List resultEntities = level.getEntities(EntityTypeTest.forClass(Entity.class), helper.getBounds(), Entity::isAlive); + if (startEntities.size() != resultEntities.size()) { + if (failOnFirstError) { + final List names = new ArrayList<>(resultEntities.size()); + for (final Entity entity : resultEntities) { + if (!startEntities.contains(entity)) { + names.add(entity.getDisplayName()); + } + } + final String formattedEntities = ComponentUtils.formatList(names, Component.literal(", ")).getString(); + helper.fail(state + " failed. Expected " + startEntities.size() + " entities to exist, found " + formattedEntities); + } + failures.add(state); + } + + // TODO check if items are the same + +// helper.runAfterDelay(1, () -> { + removeSubLevel(plotContainer, subLevel); +// }); +// }); +// }); + } + } + +// helper.runAtTickTime(tick + 1, () -> { + if (!invalidStates.isEmpty()) { + final List names = new ArrayList<>(invalidStates.size()); + for (final BlockState state : invalidStates) { + names.add(formatBlockState(state)); + } + final String formattedLines = ComponentUtils.formatList(names, Component.literal("\n")).getString(); + Sable.LOGGER.info("Skipped states:\n{}", formattedLines); + } + + if (!failures.isEmpty()) { + final List names = new ArrayList<>(failures.size()); + for (final BlockState state : failures) { + names.add(formatBlockState(state)); + } + final String formattedLines = ComponentUtils.formatList(names, Component.literal("\n")).getString(); + helper.fail(failures.size() + " states failed.\n" + formattedLines); + } + + helper.succeed(); +// }); + } + + private static Component formatBlockState(final BlockState state) { + final MutableComponent name = Component.literal(String.valueOf(BuiltInRegistries.BLOCK.getKey(state.getBlock()))); + + final Collection> properties = state.getProperties(); + if (!properties.isEmpty()) { + final StringBuilder propertiesString = new StringBuilder("["); + for (final Property property : properties) { + final Object value = state.getValue(property); + propertiesString.append(property.getName()).append("=").append(value).append(","); + } + propertiesString.setCharAt(propertiesString.length() - 1, ']'); + name.append(propertiesString.toString()); + } + + return name; + } } diff --git a/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/gametest/SableTestHelper.java b/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/gametest/SableTestHelper.java index 28546106..ba641844 100644 --- a/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/gametest/SableTestHelper.java +++ b/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/gametest/SableTestHelper.java @@ -1,10 +1,11 @@ package dev.ryanhcode.sable.neoforge.gametest; -import dev.ryanhcode.sable.companion.math.Pose3d; import dev.ryanhcode.sable.api.sublevel.SubLevelContainer; +import dev.ryanhcode.sable.companion.math.Pose3d; import dev.ryanhcode.sable.sublevel.ServerSubLevel; import dev.ryanhcode.sable.sublevel.SubLevel; import dev.ryanhcode.sable.sublevel.plot.LevelPlot; +import dev.ryanhcode.sable.sublevel.storage.SubLevelRemovalReason; import net.minecraft.core.BlockPos; import net.minecraft.gametest.framework.GameTestHelper; import net.minecraft.world.level.ChunkPos; @@ -12,9 +13,9 @@ import net.minecraft.world.level.block.Rotation; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.AABB; +import org.joml.Vector2i; import org.joml.Vector3d; import org.joml.Vector3dc; - import java.util.function.Consumer; public final class SableTestHelper { @@ -78,4 +79,10 @@ public static boolean isInBounds(final GameTestHelper helper, final Vector3dc gl final AABB box = helper.getBounds(); return box.contains(globalPosition.x(), globalPosition.y(), globalPosition.z()); } + + public static void removeSubLevel(final SubLevelContainer container, final ServerSubLevel subLevel) { + final LevelPlot plot = subLevel.getPlot(); + final Vector2i origin = container.getOrigin(); + container.removeSubLevel(plot.plotPos.x - origin.x, plot.plotPos.z - origin.y, SubLevelRemovalReason.REMOVED); + } } From 27d8e34be854263b17c6f8be4cb64346136e48aa Mon Sep 17 00:00:00 2001 From: Ocelot Date: Thu, 21 May 2026 15:03:53 -0600 Subject: [PATCH 4/7] Add more extensive testing --- .../sable/neoforge/gametest/AssemblyTest.java | 223 ++++++++++++------ .../neoforge/gametest/TestProgressBar.java | 41 ++++ 2 files changed, 195 insertions(+), 69 deletions(-) create mode 100644 neoforge/src/main/java/dev/ryanhcode/sable/neoforge/gametest/TestProgressBar.java diff --git a/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/gametest/AssemblyTest.java b/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/gametest/AssemblyTest.java index 14c132de..13e33ffb 100644 --- a/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/gametest/AssemblyTest.java +++ b/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/gametest/AssemblyTest.java @@ -8,12 +8,13 @@ import dev.ryanhcode.sable.companion.math.BoundingBox3i; import dev.ryanhcode.sable.companion.math.BoundingBox3ic; import dev.ryanhcode.sable.sublevel.ServerSubLevel; -import dev.ryanhcode.sable.sublevel.plot.EmbeddedPlotLevelAccessor; import dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; +import net.minecraft.core.NonNullList; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.gametest.framework.GameTest; +import net.minecraft.gametest.framework.GameTestAssertException; import net.minecraft.gametest.framework.GameTestAssertPosException; import net.minecraft.gametest.framework.GameTestHelper; import net.minecraft.network.chat.Component; @@ -28,7 +29,6 @@ import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.properties.BlockStateProperties; import net.minecraft.world.level.block.state.properties.Property; @@ -43,10 +43,21 @@ import org.joml.Vector3i; import org.joml.Vector3ic; import java.util.*; +import java.util.concurrent.atomic.AtomicLong; @GameTestHolder(Sable.MOD_ID) public final class AssemblyTest { + private static final Direction[] CAPABILITY_DIRECTIONS = { + null, + Direction.DOWN, + Direction.UP, + Direction.NORTH, + Direction.SOUTH, + Direction.WEST, + Direction.EAST + }; + @GameTest(template = "brittlebreak") public static void testBrittleBreaking(final GameTestHelper helper) { final ServerLevel level = helper.getLevel(); @@ -111,18 +122,12 @@ public static void testBrittleBreaking(final GameTestHelper helper) { public static void testAllBlocks(final GameTestHelper helper) { final boolean failOnFirstError = false; final Set skip = Set.of( -// ResourceLocation.fromNamespaceAndPath("create", "millstone"), -// ResourceLocation.fromNamespaceAndPath("create", "brass_tunnel") + ResourceLocation.fromNamespaceAndPath("copycats", "wrapped_copycat") + ); + final Set illegalInventories = Set.of( + ResourceLocation.fromNamespaceAndPath("create_new_age", "reactor_fuel_acceptor"), + ResourceLocation.fromNamespaceAndPath("farmersdelight", "cooking_pot") // Unsure why this failed ); - final Direction[] capabilityDirections = { - null, - Direction.DOWN, - Direction.UP, - Direction.NORTH, - Direction.SOUTH, - Direction.WEST, - Direction.EAST - }; final ServerLevel level = helper.getLevel(); final ServerSubLevelContainer plotContainer = SubLevelContainer.getContainer(level); @@ -135,66 +140,89 @@ public static void testAllBlocks(final GameTestHelper helper) { final BlockPos pos = helper.absolutePos(new BlockPos(2, 3, 2)); final BlockPos onPos = pos.below(); - final List invalidStates = new LinkedList<>(); - final List failures = new LinkedList<>(); + final Set invalidBlocks = new HashSet<>(); + final Set failures = new HashSet<>(); + + final TestProgressBar progressBar = new TestProgressBar(level.getServer().getPlayerList()); + final AtomicLong completedItems = new AtomicLong(); -// long tick = 0; + long tick = 0; + long tests = 0; for (final Map.Entry, Block> entry : BuiltInRegistries.BLOCK.entrySet()) { - if (skip.contains(entry.getKey().location())) { + final ResourceLocation blockId = entry.getKey().location(); + if (skip.contains(blockId)) { continue; } final Block block = entry.getValue(); for (final BlockState state : block.getStateDefinition().getPossibleStates()) { -// helper.runAtTickTime(tick += 3, () -> { + // Bug with lecterns. If the block state is set, but there isn't a book it will try to drop an air item + if (state.is(Blocks.LECTERN) && state.getValue(BlockStateProperties.HAS_BOOK)) { + continue; + } + + boolean hasInventory = false; + for (@Nullable final Direction direction : CAPABILITY_DIRECTIONS) { + final IItemHandler inventory = level.getCapability(Capabilities.ItemHandler.BLOCK, pos, state, level.getBlockEntity(pos), direction); + if (inventory != null) { + hasInventory = true; + break; + } + } + + if (!hasInventory && !state.hasBlockEntity()) { + continue; + } + + tests++; + helper.runAtTickTime(tick += 2, () -> { level.setBlock(onPos, Blocks.STONE.defaultBlockState(), 2); level.setBlock(pos, state, 2); // The block was unstable and can't be placed in this configuration - if (level.getBlockState(pos) != state) { + if (isInvalidState(level.getBlockState(pos))) { helper.killAllEntities(); - invalidStates.add(state); - return; - } - - // Bug with lecterns. If the block state is set, but there isn't a book it will try to drop an air item - if (state.is(Blocks.LECTERN) && state.getValue(BlockStateProperties.HAS_BOOK)) { + invalidBlocks.add(block); return; } final List startEntities = level.getEntities(EntityTypeTest.forClass(Entity.class), helper.getBounds(), Entity::isAlive); + final NonNullList[] startingInventory = new NonNullList[CAPABILITY_DIRECTIONS.length]; - int insertCount = 0; - for (@Nullable final Direction direction : capabilityDirections) { + for (int i = 0; i < CAPABILITY_DIRECTIONS.length; i++) { + final Direction direction = CAPABILITY_DIRECTIONS[i]; final IItemHandler inventory = level.getCapability(Capabilities.ItemHandler.BLOCK, pos, state, level.getBlockEntity(pos), direction); + if (inventory != null) { - final int slots = inventory.getSlots(); - if (inventory instanceof final IItemHandlerModifiable modifiable) { - for (int i = 0; i < slots; i++) { - try { - modifiable.setStackInSlot(i, insertStack.copy()); - insertCount++; - } catch (final Throwable ignored) { - if (!inventory.insertItem(i, insertStack.copy(), false).equals(insertStack)) { - insertCount++; + try { + final int slots = inventory.getSlots(); + if (inventory instanceof final IItemHandlerModifiable modifiable) { + for (int slot = 0; slot < slots; slot++) { + try { + modifiable.setStackInSlot(slot, insertStack.copy()); + } catch (final Throwable ignored) { + inventory.insertItem(slot, insertStack.copy(), false); } } - } - } else { - for (int i = 0; i < slots; i++) { - if (!inventory.insertItem(i, insertStack.copy(), false).equals(insertStack)) { - insertCount++; + } else { + for (int slot = 0; slot < slots; slot++) { + inventory.insertItem(slot, insertStack.copy(), false); } } + + final NonNullList list = NonNullList.withSize(slots, ItemStack.EMPTY); + for (int slot = 0; slot < slots; slot++) { + list.set(slot, inventory.getStackInSlot(slot).copy()); + } + startingInventory[i] = list; + } catch (final Throwable t) { + t.printStackTrace(); + helper.fail(formatBlockState(state).getString() + " failed. Unable to insert items successfully for face " + direction, pos); } } } - if (insertCount == 0 && !state.hasBlockEntity()) { - return; - } - -// helper.runAfterDelay(1, () -> { + helper.runAfterDelay(1, () -> { final ServerSubLevel subLevel = SubLevelAssemblyHelper.assembleBlocks(level, pos, List.of(pos, onPos), new BoundingBox3i( onPos.getX(), onPos.getY(), @@ -208,8 +236,15 @@ public static void testAllBlocks(final GameTestHelper helper) { pos.getZ() + 0.5), helper.getTestRotation().rotation().transformation().getNormalizedRotation(new Quaterniond())); - final EmbeddedPlotLevelAccessor subLevelAccessor = subLevel.getPlot().getEmbeddedLevelAccessor(); - final BlockEntity sublevelBlockEntity = subLevelAccessor.getBlockEntity(BlockPos.ZERO); + final BlockPos centerBlock = subLevel.getPlot().getCenterBlock(); + + if (isInvalidState(level.getBlockState(centerBlock))) { + invalidBlocks.add(block); + helper.killAllEntities(); + removeSubLevel(plotContainer, subLevel); + progressBar.update(completedItems.incrementAndGet()); + return; + } final List resultEntities = level.getEntities(EntityTypeTest.forClass(Entity.class), helper.getBounds(), Entity::isAlive); if (startEntities.size() != resultEntities.size()) { @@ -221,42 +256,88 @@ public static void testAllBlocks(final GameTestHelper helper) { } } final String formattedEntities = ComponentUtils.formatList(names, Component.literal(", ")).getString(); - helper.fail(state + " failed. Expected " + startEntities.size() + " entities to exist, found " + formattedEntities); + helper.fail(formatBlockState(state).getString() + " failed. Expected " + startEntities.size() + " entities to exist, found " + formattedEntities); + return; } - failures.add(state); + failures.add(block); + helper.killAllEntities(); } - // TODO check if items are the same + if (!illegalInventories.contains(blockId)) { + for (int i = 0; i < CAPABILITY_DIRECTIONS.length; i++) { + final Direction direction = CAPABILITY_DIRECTIONS[i]; + final IItemHandler inventory = level.getCapability(Capabilities.ItemHandler.BLOCK, centerBlock, state, level.getBlockEntity(centerBlock), direction); + final NonNullList starting = startingInventory[i]; -// helper.runAfterDelay(1, () -> { - removeSubLevel(plotContainer, subLevel); -// }); -// }); -// }); + if (inventory != null) { + if (starting == null) { + final String stateString = formatBlockState(state).getString(); + helper.fail(stateString + " failed. Expected no inventory for face " + direction + ", found items"); + return; + } + + if (starting.size() != inventory.getSlots()) { + final String stateString = formatBlockState(state).getString(); + helper.fail(stateString + " failed. Expected " + starting.size() + " inventory slots for face " + direction + ", found " + inventory.getSlots()); + return; + } + + try { + for (int slot = 0; slot < starting.size(); slot++) { + if (!ItemStack.isSameItemSameComponents(starting.get(slot), inventory.getStackInSlot(slot))) { + final String stateString = formatBlockState(state).getString(); + final String expectedStack = starting.get(slot).toString(); + final String foundStack = inventory.getStackInSlot(slot).toString(); + helper.fail(stateString + " failed. Expected slot " + slot + " for face " + direction + " to be " + expectedStack + " found " + foundStack); + return; + } + } + } catch (final GameTestAssertException e) { + throw e; + } catch (final Throwable t) { + t.printStackTrace(); + helper.fail(formatBlockState(state).getString() + " failed. Unable to get items successfully for face " + direction); + } + } else if (starting != null) { + final String stateString = formatBlockState(state).getString(); + helper.fail(stateString + " failed. Expected inventory items for face " + direction + ", found none"); + return; + } + } + } + + removeSubLevel(plotContainer, subLevel); + progressBar.update(completedItems.incrementAndGet()); + }); + }); } } -// helper.runAtTickTime(tick + 1, () -> { - if (!invalidStates.isEmpty()) { - final List names = new ArrayList<>(invalidStates.size()); - for (final BlockState state : invalidStates) { - names.add(formatBlockState(state)); + progressBar.begin(tests); + + helper.runAtTickTime(tick + 1, () -> { + progressBar.end(); + + if (!invalidBlocks.isEmpty()) { + final List names = new ArrayList<>(invalidBlocks.size()); + for (final Block block : invalidBlocks) { + names.add(String.valueOf(BuiltInRegistries.BLOCK.getKey(block))); } - final String formattedLines = ComponentUtils.formatList(names, Component.literal("\n")).getString(); - Sable.LOGGER.info("Skipped states:\n{}", formattedLines); + final String formattedLines = String.join("\n", names); + Sable.LOGGER.info("Skipped blocks:\n{}", formattedLines); } if (!failures.isEmpty()) { - final List names = new ArrayList<>(failures.size()); - for (final BlockState state : failures) { - names.add(formatBlockState(state)); + final List names = new ArrayList<>(failures.size()); + for (final Block block : failures) { + names.add(String.valueOf(BuiltInRegistries.BLOCK.getKey(block))); } - final String formattedLines = ComponentUtils.formatList(names, Component.literal("\n")).getString(); - helper.fail(failures.size() + " states failed.\n" + formattedLines); + final String formattedLines = String.join("\n", names); + helper.fail(failures.size() + " blocks failed.\n" + formattedLines); } helper.succeed(); -// }); + }); } private static Component formatBlockState(final BlockState state) { @@ -275,4 +356,8 @@ private static Component formatBlockState(final BlockState state) { return name; } + + private static boolean isInvalidState(final BlockState state) { + return state.isAir() || state.getFluidState().createLegacyBlock() == state; + } } diff --git a/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/gametest/TestProgressBar.java b/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/gametest/TestProgressBar.java new file mode 100644 index 00000000..5d2da59e --- /dev/null +++ b/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/gametest/TestProgressBar.java @@ -0,0 +1,41 @@ +package dev.ryanhcode.sable.neoforge.gametest; + +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerBossEvent; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.PlayerList; +import net.minecraft.world.BossEvent; + +public class TestProgressBar { + + private final ServerBossEvent bossEvent; + private final PlayerList playerList; + private long maxItems; + + public TestProgressBar(final PlayerList playerList) { + this.bossEvent = new ServerBossEvent(Component.literal("Test Progress"), BossEvent.BossBarColor.RED, BossEvent.BossBarOverlay.PROGRESS); + this.playerList = playerList; + } + + private void updateVisible() { + for (final ServerPlayer player : this.playerList.getPlayers()) { + this.bossEvent.addPlayer(player); + } + } + + public void begin(final long maxItems) { + this.maxItems = maxItems; + this.bossEvent.setName(Component.literal("Test Progress: 0 / " + maxItems)); + this.updateVisible(); + } + + public void update(final long items) { + this.updateVisible(); + this.bossEvent.setName(Component.literal("Test Progress: " + items + " / " + this.maxItems)); + this.bossEvent.setProgress((float) items / this.maxItems); + } + + public void end() { + this.bossEvent.removeAllPlayers(); + } +} From 7dfc7cff2c7bd5361c9460abced70cb5b61e21e7 Mon Sep 17 00:00:00 2001 From: Ocelot Date: Thu, 21 May 2026 15:15:27 -0600 Subject: [PATCH 5/7] Mark test as optional --- .../dev/ryanhcode/sable/neoforge/gametest/AssemblyTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/gametest/AssemblyTest.java b/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/gametest/AssemblyTest.java index 13e33ffb..692791ac 100644 --- a/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/gametest/AssemblyTest.java +++ b/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/gametest/AssemblyTest.java @@ -118,7 +118,7 @@ public static void testBrittleBreaking(final GameTestHelper helper) { }); } - @GameTest(template = "allblocks", manualOnly = true, timeoutTicks = 30_000_000) + @GameTest(template = "allblocks", required = false, manualOnly = true, timeoutTicks = 30_000_000) public static void testAllBlocks(final GameTestHelper helper) { final boolean failOnFirstError = false; final Set skip = Set.of( From 7aaae113b6a2ba109d7fb1445fcd57caf835dd0f Mon Sep 17 00:00:00 2001 From: Ocelot Date: Thu, 21 May 2026 16:01:55 -0600 Subject: [PATCH 6/7] Update silent_assembly_removal.json Each block listed in this tag should have Clearable implemented in the base mod --- .../tags/block/silent_assembly_removal.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/common/src/main/resources/data/sable/tags/block/silent_assembly_removal.json b/common/src/main/resources/data/sable/tags/block/silent_assembly_removal.json index a22514f6..dcce4456 100644 --- a/common/src/main/resources/data/sable/tags/block/silent_assembly_removal.json +++ b/common/src/main/resources/data/sable/tags/block/silent_assembly_removal.json @@ -1,5 +1,22 @@ { "replace": false, "values": [ + { "id": "createaddition:rolling_mill", "required": false }, + { "id": "rubberworks:compressor", "required": false }, + { "id": "dndesires:smart_hopper", "required": false }, + { "id": "dndesires:roll_table", "required": false }, + { "id": "createfood:ration_box", "required": false }, + { "id": "createfood:cloth_sack", "required": false }, + { "id": "mechanical_botany:mechanical_insolator", "required": false }, + { "id": "mechanical_botany:mechanical_composter", "required": false }, + { "id": "create_connected:item_silo", "required": false }, + { "id": "create_integrated_farming:chicken_roost", "required": false }, + { "id": "create_new_age:reactor_fuel_acceptor", "required": false }, + { "id": "trading_floor:trading_depot", "required": false }, + { "id": "create_fantasizing:transporter", "required": false }, + { "id": "createdieselgenerators:bulk_fermenter", "required": false }, + { "id": "createrailwaysnavigator:navigator_lectern", "required": false }, + { "id": "pipeorgans:tracker_bar", "required": false }, + { "id": "dndecor:colored_storage_container", "required": false } ] } \ No newline at end of file From ce5bc7b53b506a2f76712557fbe1959c478a6a30 Mon Sep 17 00:00:00 2001 From: Ocelot Date: Thu, 21 May 2026 16:02:03 -0600 Subject: [PATCH 7/7] Add fast test option --- .../dev/ryanhcode/sable/neoforge/gametest/AssemblyTest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/gametest/AssemblyTest.java b/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/gametest/AssemblyTest.java index 692791ac..5933724c 100644 --- a/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/gametest/AssemblyTest.java +++ b/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/gametest/AssemblyTest.java @@ -121,6 +121,7 @@ public static void testBrittleBreaking(final GameTestHelper helper) { @GameTest(template = "allblocks", required = false, manualOnly = true, timeoutTicks = 30_000_000) public static void testAllBlocks(final GameTestHelper helper) { final boolean failOnFirstError = false; + final boolean fastTest = true; final Set skip = Set.of( ResourceLocation.fromNamespaceAndPath("copycats", "wrapped_copycat") ); @@ -310,6 +311,10 @@ public static void testAllBlocks(final GameTestHelper helper) { progressBar.update(completedItems.incrementAndGet()); }); }); + + if (fastTest) { + break; + } } }