diff --git a/.gitignore b/.gitignore index 78ee2b5035..00861b9243 100644 --- a/.gitignore +++ b/.gitignore @@ -24,5 +24,6 @@ run classes logs run-data +run-test gource.sh .noai diff --git a/build.gradle b/build.gradle index 3162cab990..12199e5c39 100644 --- a/build.gradle +++ b/build.gradle @@ -98,6 +98,21 @@ subprojects { version = project(":").version } +sourceSets { + gametest { + compileClasspath += main.compileClasspath + main.output + + java { + srcDirs += ['src/main/java', 'src/gametest/java'] + } + resources { + srcDirs += ['src/gametest/generated'] + } + } +} + +project.configurations.maybeCreate(sourceSets.gametest.getTaskName(null, 'implementation')).extendsFrom(project.configurations.named('implementation').get()) + neoForge { // we need the subprojects to evaluate first so that the sourceSets are properly constructed evaluationDependsOnChildren() @@ -107,6 +122,8 @@ neoForge { validateAccessTransformers = true + addModdingDependenciesTo sourceSets.gametest + mods { "${mod_id}" { it.sourceSet this.sourceSets.main @@ -115,6 +132,11 @@ neoForge { 'tf-asm' { it.sourceSet project(":tf-asm").sourceSets.main } + + 'gametest' { + sourceSet sourceSets.main + sourceSet sourceSets.gametest + } } def mainMod = mods."${project.mod_id}" @@ -133,13 +155,11 @@ neoForge { client { client() - systemProperty 'forge.enabledGameTestNamespaces', mod_id programArguments.addAll '--username', secrets.getProperty("username") ?: 'Dev', secrets.getProperty("uuid") ? '--uuid' : '', secrets.getProperty("uuid") ?: '' } server { server() - systemProperty 'forge.enabledGameTestNamespaces', mod_id programArgument '--nogui' } @@ -148,6 +168,14 @@ neoForge { gameDirectory = project.file('run-data') programArguments.addAll '--mod', mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath() } + + 'gametest-server' { + type = 'gameTestServer' + gameDirectory = project.file('run-test') + systemProperty 'neoforge.enabledGameTestNamespaces', mod_id + sourceSet = sourceSets.gametest + loadedMods = [mods.'tf-asm', mods.'gametest'] + } } } @@ -206,12 +234,18 @@ repositories { dependencies { jarJar implementation(project(":tf-asm")) + gametestImplementation("net.neoforged:testframework:${neo_version}") { + transitive = false + } + def beanification = "tamaized:beanification:${project.beanification_version}" implementation beanification shade beanification testImplementation "${beanification}:tests" testCompileOnly "${beanification}:test-sources" + gametestImplementation(beanification) + //make sure to only pick one of these when testing (switch others to compileOnly) implementation "mezz.jei:jei-${project.base_minecraft_version}-neoforge:${project.jei_version}" compileOnly "me.shedaniel:RoughlyEnoughItems-neoforge:${project.rei_version}" diff --git a/gradle.properties b/gradle.properties index acf5497b57..561f5c7d7e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ minecraft_version=1.21.1 # NeoForge: https://projects.neoforged.net/neoforged/neoforge neo_version=21.1.83 -mdg_version=2.0.76 +mdg_version=2.0.107 # Deps beanification_version=1.6.108 diff --git a/src/gametest/java/twilightforest/TFBlockTests.java b/src/gametest/java/twilightforest/TFBlockTests.java new file mode 100644 index 0000000000..65c31c653a --- /dev/null +++ b/src/gametest/java/twilightforest/TFBlockTests.java @@ -0,0 +1,160 @@ +package twilightforest; + +import com.google.common.collect.Streams; +import net.minecraft.commands.arguments.EntityAnchorArgument; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.gametest.framework.TestFunction; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; +import net.neoforged.neoforge.gametest.GameTestHolder; +import net.neoforged.neoforge.registries.DeferredHolder; +import twilightforest.init.TFBlocks; + +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Stream; + +@GameTestHolder(TwilightForestMod.ID) +public class TFBlockTests { + + public static List generateBlockRegistryTests() { + Collection> tfBlocks = TFBlocks.BLOCKS.getEntries(); + + Stream setBlocks = tfBlocks.stream() + .filter(Predicate.not(EXCLUDE_SETBLOCKS::contains)) + .map(TFBlockTests::setBlockTest); + + Stream placeBlocks = tfBlocks.stream() + .filter(Predicate.not(EXCLUDE_PLACEBLOCKS::contains)) + .map(DeferredHolder::value) + .map(Block::asItem) + .filter(blockItem -> !blockItem.getDefaultInstance().isEmpty()) + .map(TFBlockTests::placeBlockTest); + + return Streams.concat(setBlocks, placeBlocks).toList(); + } + + /** + * Fundamental test that sets every single block. + *
+ * Helps catch any fundamental problems such as using clientside-specific code that would crash on a dedicated server. + */ + private static TestFunction setBlockTest(DeferredHolder blockHolder) { + Consumer setBlockTest = test -> { + for (BlockState state : blockHolder.value().getStateDefinition().getPossibleStates()) { + test.setBlock(BlockPos.ZERO, Blocks.AIR); + test.setBlock(BlockPos.ZERO, state); + test.assertBlockState(BlockPos.ZERO, state::equals, () -> "Expected placement of " + state + ", detected " + test.getBlockState(BlockPos.ZERO)); + } + test.succeed(); // Assertion passed + }; + return new TestFunction("setBlock", blockHolder.getId().toString(), "twilightforest:empty", Rotation.NONE, 1000, 0, true, false, 1, 1, false, setBlockTest); + } + + private static final Set> EXCLUDE_SETBLOCKS = Set.of( + TFBlocks.TWILIGHT_PORTAL, // Twilight Portal instantly reverts without supporting blocks + TFBlocks.UNCRAFTING_TABLE, // FIXME What's going on with its powered on state? + TFBlocks.CANDELABRA, // FIXME Candle property behavior + TFBlocks.CINDER_FURNACE // Unimplemented block + ); + + /** + * Fundamental test that places every single block with a mock player. + *
+ * Triggers `Block#getStateForPlacement` + *
+ * Helps catch any fundamental problems such as using clientside-specific code that would crash on a dedicated server. + */ + private static TestFunction placeBlockTest(Item item) { + Consumer placeBlockTest = test -> { + test.setBlock(BlockPos.ZERO.below(), Blocks.DIRT); // Plant support + //test.setBlock(BlockPos.ZERO, Blocks.AIR); + ItemStack stack = new ItemStack(item); + + Player player = test.makeMockPlayer(GameType.CREATIVE); + { // Positions the player and makes them look at the block + Vec3 worldVecPos = Vec3.atBottomCenterOf(test.absolutePos(BlockPos.ZERO)); + player.teleportTo(worldVecPos.x + 1, worldVecPos.y, worldVecPos.z + 1); + player.lookAt(EntityAnchorArgument.Anchor.EYES, worldVecPos); + } + player.setItemInHand(InteractionHand.MAIN_HAND, stack); + + test.placeAt(player, stack, BlockPos.ZERO.above(), Direction.DOWN); + test.assertBlockState(BlockPos.ZERO, state -> !state.is(Blocks.AIR), () -> "Expected placement of " + item + ", detected " + test.getBlockState(BlockPos.ZERO)); + + test.succeed(); // All assertions passed + }; + + return new TestFunction( + "placeBlock", + item.toString(), + "twilightforest:empty", + Rotation.NONE, + 1000, + 0, + true, + false, + 1, + 1, + false, + placeBlockTest); + } + + private static final Set> EXCLUDE_PLACEBLOCKS = Set.of( + // TODO Requires dirt above + TFBlocks.TORCHBERRY_PLANT, + TFBlocks.ROOT_STRAND, + TFBlocks.TROLLVIDR, + TFBlocks.UNRIPE_TROLLBER, + TFBlocks.TROLLBER, + // TODO Requires water below + TFBlocks.HUGE_LILY_PAD, + TFBlocks.HUGE_WATER_LILY, + // TODO Requires adjacent support + TFBlocks.IRON_LADDER, + TFBlocks.ROPE, + TFBlocks.THORN_ROSE, + // TODO Requires block above + TFBlocks.TWILIGHT_OAK_HANGING_SIGN, + TFBlocks.CANOPY_HANGING_SIGN, + TFBlocks.MANGROVE_HANGING_SIGN, + TFBlocks.DARK_HANGING_SIGN, + TFBlocks.TIME_HANGING_SIGN, + TFBlocks.TRANSFORMATION_HANGING_SIGN, + TFBlocks.MINING_HANGING_SIGN, + TFBlocks.SORTING_HANGING_SIGN, + // TODO Requires block sideways + TFBlocks.TWILIGHT_OAK_WALL_HANGING_SIGN, + TFBlocks.CANOPY_WALL_HANGING_SIGN, + TFBlocks.MANGROVE_WALL_HANGING_SIGN, + TFBlocks.DARK_WALL_HANGING_SIGN, + TFBlocks.TIME_WALL_HANGING_SIGN, + TFBlocks.TRANSFORMATION_WALL_HANGING_SIGN, + TFBlocks.MINING_WALL_HANGING_SIGN, + TFBlocks.SORTING_WALL_HANGING_SIGN, + // FIXME Crashes game, cherry-pick 44c2d5650e41ade9cbca70be7ce9b5b9e402b5ac into 1.21.1 + TFBlocks.TROLLSTEINN, + // FIXME why do these fail? + TFBlocks.FALLEN_LEAVES, + TFBlocks.GIANT_COBBLESTONE, + TFBlocks.GIANT_LOG, + TFBlocks.GIANT_LEAVES, + TFBlocks.GIANT_OBSIDIAN, + // NYI + TFBlocks.CINDER_FURNACE + ); + +} diff --git a/src/gametest/java/twilightforest/TFGameTests.java b/src/gametest/java/twilightforest/TFGameTests.java new file mode 100644 index 0000000000..31900004d2 --- /dev/null +++ b/src/gametest/java/twilightforest/TFGameTests.java @@ -0,0 +1,30 @@ +package twilightforest; + +import net.minecraft.gametest.framework.GameTestGenerator; +import net.minecraft.gametest.framework.TestFunction; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.event.RegisterGameTestsEvent; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.Collection; + +/** + * Entrypoint for Twilight Forest's game-integrated tests + */ +@EventBusSubscriber(bus = EventBusSubscriber.Bus.MOD, modid = TwilightForestMod.ID) +public final class TFGameTests { + public static final Logger LOGGER = LogManager.getLogger(TwilightForestMod.ID + "tests"); + + @SubscribeEvent + public static void registerBlockTests(RegisterGameTestsEvent event) { + LOGGER.info("Starting registerBlockTests"); + event.register(TFGameTests.class); + } + + @GameTestGenerator + public static Collection generateBlockTests() { + return TFBlockTests.generateBlockRegistryTests(); + } +} diff --git a/src/main/resources/data/twilightforest/structure/empty.nbt b/src/main/resources/data/twilightforest/structure/empty.nbt new file mode 100644 index 0000000000..f886b5790d Binary files /dev/null and b/src/main/resources/data/twilightforest/structure/empty.nbt differ