diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/grower/TreeGrower.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/grower/TreeGrower.java.patch index 3cd7895c2a28..fac91649636a 100644 --- a/paper-server/patches/sources/net/minecraft/world/level/block/grower/TreeGrower.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/level/block/grower/TreeGrower.java.patch @@ -16,68 +16,14 @@ Feature feature = featureHolder.value(); removeSapling(level, pos); if (feature.place(level, generator, random, pos)) { -@@ -229,4 +_,64 @@ +@@ -229,4 +_,10 @@ private record TwoByTwoSaplingPos(int offsetX, int offsetZ, List> groundLevelSurroundingBlocks) { } + + // CraftBukkit start + private void setTreeType(Holder feature) { -+ if (feature.is(TreeFeatures.OAK) || feature.is(TreeFeatures.OAK_BEES_005)) { -+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TREE; -+ } else if (feature.is(TreeFeatures.HUGE_RED_MUSHROOM)) { -+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.RED_MUSHROOM; -+ } else if (feature.is(TreeFeatures.HUGE_BROWN_MUSHROOM)) { -+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BROWN_MUSHROOM; -+ } else if (feature.is(TreeFeatures.JUNGLE_TREE)) { -+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.COCOA_TREE; -+ } else if (feature.is(TreeFeatures.JUNGLE_TREE_NO_VINE)) { -+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.SMALL_JUNGLE; -+ } else if (feature.is(TreeFeatures.PINE)) { -+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_REDWOOD; -+ } else if (feature.is(TreeFeatures.SPRUCE)) { -+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.REDWOOD; -+ } else if (feature.is(TreeFeatures.ACACIA)) { -+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.ACACIA; -+ } else if (feature.is(TreeFeatures.BIRCH) || feature.is(TreeFeatures.BIRCH_BEES_005)) { -+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BIRCH; -+ } else if (feature.is(TreeFeatures.SUPER_BIRCH_BEES_0002)) { -+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_BIRCH; -+ } else if (feature.is(TreeFeatures.SWAMP_OAK)) { -+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.SWAMP; -+ } else if (feature.is(TreeFeatures.FANCY_OAK) || feature.is(TreeFeatures.FANCY_OAK_BEES_005)) { -+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BIG_TREE; -+ } else if (feature.is(TreeFeatures.JUNGLE_BUSH)) { -+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.JUNGLE_BUSH; -+ } else if (feature.is(TreeFeatures.DARK_OAK)) { -+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.DARK_OAK; -+ } else if (feature.is(TreeFeatures.MEGA_SPRUCE)) { -+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MEGA_REDWOOD; -+ } else if (feature.is(TreeFeatures.MEGA_PINE)) { -+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MEGA_PINE; -+ } else if (feature.is(TreeFeatures.MEGA_JUNGLE_TREE)) { -+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.JUNGLE; -+ } else if (feature.is(TreeFeatures.AZALEA_TREE)) { -+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.AZALEA; -+ } else if (feature.is(TreeFeatures.MANGROVE)) { -+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MANGROVE; -+ } else if (feature.is(TreeFeatures.TALL_MANGROVE)) { -+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_MANGROVE; -+ } else if (feature.is(TreeFeatures.CHERRY) || feature.is(TreeFeatures.CHERRY_BEES_005)) { -+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.CHERRY; -+ } else if (feature.is(TreeFeatures.PALE_OAK) || feature.is(TreeFeatures.PALE_OAK_BONEMEAL)) { -+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.PALE_OAK; -+ } else if (feature.is(TreeFeatures.PALE_OAK_CREAKING)) { -+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.PALE_OAK_CREAKING; -+ } else if (feature.is(TreeFeatures.RED_POPLAR)) { -+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.RED_POPLAR; -+ } else if (feature.is(TreeFeatures.ORANGE_POPLAR)) { -+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.ORANGE_POPLAR; -+ } else if (feature.is(TreeFeatures.YELLOW_POPLAR)) { -+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.YELLOW_POPLAR; -+ } else { -+ throw new IllegalArgumentException("Unknown tree generator " + feature); -+ } ++ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.craftbukkit.CraftTreeType.fromFeature(feature.unwrapKey().orElseThrow()); + } + // CraftBukkit end } diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftRegionAccessor.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftRegionAccessor.java index f843ae4ad2b2..124420f4adf0 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftRegionAccessor.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftRegionAccessor.java @@ -10,7 +10,6 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; import net.minecraft.core.registries.Registries; -import net.minecraft.data.worldgen.features.TreeFeatures; import net.minecraft.resources.ResourceKey; import net.minecraft.util.Mth; import net.minecraft.util.RandomSource; @@ -162,98 +161,12 @@ public boolean generateTree(Location location, Random random, TreeType treeType, } public boolean generateTree(WorldGenLevel access, ChunkGenerator chunkGenerator, BlockPos pos, RandomSource random, TreeType treeType) { - ResourceKey gen; - switch (treeType) { - case BIG_TREE: - gen = TreeFeatures.FANCY_OAK; - break; - case BIRCH: - gen = TreeFeatures.BIRCH; - break; - case REDWOOD: - gen = TreeFeatures.SPRUCE; - break; - case TALL_REDWOOD: - gen = TreeFeatures.PINE; - break; - case JUNGLE: - gen = TreeFeatures.MEGA_JUNGLE_TREE; - break; - case SMALL_JUNGLE: - gen = TreeFeatures.JUNGLE_TREE_NO_VINE; - break; - case COCOA_TREE: - gen = TreeFeatures.JUNGLE_TREE; - break; - case JUNGLE_BUSH: - gen = TreeFeatures.JUNGLE_BUSH; - break; - case RED_MUSHROOM: - gen = TreeFeatures.HUGE_RED_MUSHROOM; - break; - case BROWN_MUSHROOM: - gen = TreeFeatures.HUGE_BROWN_MUSHROOM; - break; - case SWAMP: - gen = TreeFeatures.SWAMP_OAK; - break; - case ACACIA: - gen = TreeFeatures.ACACIA; - break; - case DARK_OAK: - gen = TreeFeatures.DARK_OAK; - break; - case MEGA_REDWOOD: - gen = TreeFeatures.MEGA_SPRUCE; - break; - case MEGA_PINE: - gen = TreeFeatures.MEGA_PINE; - break; - case TALL_BIRCH: - gen = TreeFeatures.SUPER_BIRCH_BEES_0002; - break; - case CHORUS_PLANT: - ChorusFlowerBlock.generatePlant(access, pos, random, 8); - return true; - case CRIMSON_FUNGUS: - gen = this.isNormalWorld() ? TreeFeatures.CRIMSON_FUNGUS_PLANTED : TreeFeatures.CRIMSON_FUNGUS; // Paper - Fix async entity add due to fungus trees; if world gen, don't use planted version - break; - case WARPED_FUNGUS: - gen = this.isNormalWorld() ? TreeFeatures.WARPED_FUNGUS_PLANTED : TreeFeatures.WARPED_FUNGUS; // Paper - Fix async entity add due to fungus trees; if world gen, don't use planted version - break; - case AZALEA: - gen = TreeFeatures.AZALEA_TREE; - break; - case MANGROVE: - gen = TreeFeatures.MANGROVE; - break; - case TALL_MANGROVE: - gen = TreeFeatures.TALL_MANGROVE; - break; - case CHERRY: - gen = TreeFeatures.CHERRY; - break; - case PALE_OAK: - gen = TreeFeatures.PALE_OAK; - break; - case PALE_OAK_CREAKING: - gen = TreeFeatures.PALE_OAK_CREAKING; - break; - case RED_POPLAR: - gen = TreeFeatures.RED_POPLAR; - break; - case ORANGE_POPLAR: - gen = TreeFeatures.ORANGE_POPLAR; - break; - case YELLOW_POPLAR: - gen = TreeFeatures.YELLOW_POPLAR; - break; - case TREE: - default: - gen = TreeFeatures.OAK; - break; + if (treeType == TreeType.CHORUS_PLANT) { + ChorusFlowerBlock.generatePlant(access, pos, random, 8); + return true; } + ResourceKey gen = CraftTreeType.toFeature(treeType, this.isNormalWorld()); Holder holder = access.registryAccess().lookupOrThrow(Registries.FEATURE).get(gen).orElse(null); return holder != null && holder.value().place(access, chunkGenerator, random, pos); } diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftTreeType.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftTreeType.java new file mode 100644 index 000000000000..a89a27f6b792 --- /dev/null +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftTreeType.java @@ -0,0 +1,98 @@ +package org.bukkit.craftbukkit; + +import java.util.Map; +import net.minecraft.data.worldgen.features.TreeFeatures; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.levelgen.feature.Feature; +import org.bukkit.TreeType; +import org.jspecify.annotations.Nullable; + +public final class CraftTreeType { + + private static final Map, TreeType> BY_FEATURE = Map.ofEntries( + Map.entry(TreeFeatures.OAK, TreeType.TREE), + Map.entry(TreeFeatures.OAK_BEES_005, TreeType.TREE), + Map.entry(TreeFeatures.FANCY_OAK, TreeType.BIG_TREE), + Map.entry(TreeFeatures.FANCY_OAK_BEES_005, TreeType.BIG_TREE), + Map.entry(TreeFeatures.SPRUCE, TreeType.REDWOOD), + Map.entry(TreeFeatures.PINE, TreeType.TALL_REDWOOD), + Map.entry(TreeFeatures.MEGA_SPRUCE, TreeType.MEGA_REDWOOD), + Map.entry(TreeFeatures.MEGA_PINE, TreeType.MEGA_PINE), + Map.entry(TreeFeatures.BIRCH, TreeType.BIRCH), + Map.entry(TreeFeatures.BIRCH_BEES_005, TreeType.BIRCH), + Map.entry(TreeFeatures.SUPER_BIRCH_BEES_0002, TreeType.TALL_BIRCH), + Map.entry(TreeFeatures.MEGA_JUNGLE_TREE, TreeType.JUNGLE), + Map.entry(TreeFeatures.JUNGLE_TREE_NO_VINE, TreeType.SMALL_JUNGLE), + Map.entry(TreeFeatures.JUNGLE_TREE, TreeType.COCOA_TREE), + Map.entry(TreeFeatures.JUNGLE_BUSH, TreeType.JUNGLE_BUSH), + Map.entry(TreeFeatures.HUGE_RED_MUSHROOM, TreeType.RED_MUSHROOM), + Map.entry(TreeFeatures.HUGE_BROWN_MUSHROOM, TreeType.BROWN_MUSHROOM), + Map.entry(TreeFeatures.SWAMP_OAK, TreeType.SWAMP), + Map.entry(TreeFeatures.ACACIA, TreeType.ACACIA), + Map.entry(TreeFeatures.DARK_OAK, TreeType.DARK_OAK), + Map.entry(TreeFeatures.CRIMSON_FUNGUS, TreeType.CRIMSON_FUNGUS), + Map.entry(TreeFeatures.CRIMSON_FUNGUS_PLANTED, TreeType.CRIMSON_FUNGUS), + Map.entry(TreeFeatures.WARPED_FUNGUS, TreeType.WARPED_FUNGUS), + Map.entry(TreeFeatures.WARPED_FUNGUS_PLANTED, TreeType.WARPED_FUNGUS), + Map.entry(TreeFeatures.AZALEA_TREE, TreeType.AZALEA), + Map.entry(TreeFeatures.MANGROVE, TreeType.MANGROVE), + Map.entry(TreeFeatures.TALL_MANGROVE, TreeType.TALL_MANGROVE), + Map.entry(TreeFeatures.CHERRY, TreeType.CHERRY), + Map.entry(TreeFeatures.CHERRY_BEES_005, TreeType.CHERRY), + Map.entry(TreeFeatures.PALE_OAK, TreeType.PALE_OAK), + Map.entry(TreeFeatures.PALE_OAK_BONEMEAL, TreeType.PALE_OAK), + Map.entry(TreeFeatures.PALE_OAK_CREAKING, TreeType.PALE_OAK_CREAKING), + Map.entry(TreeFeatures.RED_POPLAR, TreeType.RED_POPLAR), + Map.entry(TreeFeatures.ORANGE_POPLAR, TreeType.ORANGE_POPLAR), + Map.entry(TreeFeatures.YELLOW_POPLAR, TreeType.YELLOW_POPLAR) + ); + + private CraftTreeType() { + } + + public static TreeType fromFeature(ResourceKey feature) { + TreeType treeType = BY_FEATURE.get(feature); + if (treeType == null) { + throw new IllegalArgumentException("Unknown tree generator " + feature); + } + return treeType; + } + + /** + * @return the feature for the tree type, or {@code null} for {@link TreeType#CHORUS_PLANT} + * which isn't placed through a feature + */ + public static @Nullable ResourceKey toFeature(TreeType treeType, boolean normalWorld) { + return switch (treeType) { + case TREE -> TreeFeatures.OAK; + case BIG_TREE -> TreeFeatures.FANCY_OAK; + case REDWOOD -> TreeFeatures.SPRUCE; + case TALL_REDWOOD -> TreeFeatures.PINE; + case JUNGLE -> TreeFeatures.MEGA_JUNGLE_TREE; + case SMALL_JUNGLE -> TreeFeatures.JUNGLE_TREE_NO_VINE; + case COCOA_TREE -> TreeFeatures.JUNGLE_TREE; + case JUNGLE_BUSH -> TreeFeatures.JUNGLE_BUSH; + case RED_MUSHROOM -> TreeFeatures.HUGE_RED_MUSHROOM; + case BROWN_MUSHROOM -> TreeFeatures.HUGE_BROWN_MUSHROOM; + case SWAMP -> TreeFeatures.SWAMP_OAK; + case ACACIA -> TreeFeatures.ACACIA; + case DARK_OAK -> TreeFeatures.DARK_OAK; + case MEGA_REDWOOD -> TreeFeatures.MEGA_SPRUCE; + case MEGA_PINE -> TreeFeatures.MEGA_PINE; + case BIRCH -> TreeFeatures.BIRCH; + case TALL_BIRCH -> TreeFeatures.SUPER_BIRCH_BEES_0002; + case CHORUS_PLANT -> null; + case CRIMSON_FUNGUS -> normalWorld ? TreeFeatures.CRIMSON_FUNGUS_PLANTED : TreeFeatures.CRIMSON_FUNGUS; // fix async entity add due to fungus trees; if world gen, don't use planted version + case WARPED_FUNGUS -> normalWorld ? TreeFeatures.WARPED_FUNGUS_PLANTED : TreeFeatures.WARPED_FUNGUS; // fix async entity add due to fungus trees; if world gen, don't use planted version + case AZALEA -> TreeFeatures.AZALEA_TREE; + case MANGROVE -> TreeFeatures.MANGROVE; + case TALL_MANGROVE -> TreeFeatures.TALL_MANGROVE; + case CHERRY -> TreeFeatures.CHERRY; + case PALE_OAK -> TreeFeatures.PALE_OAK; + case PALE_OAK_CREAKING -> TreeFeatures.PALE_OAK_CREAKING; + case RED_POPLAR -> TreeFeatures.RED_POPLAR; + case ORANGE_POPLAR -> TreeFeatures.ORANGE_POPLAR; + case YELLOW_POPLAR -> TreeFeatures.YELLOW_POPLAR; + }; + } +} diff --git a/paper-server/src/test/java/org/bukkit/craftbukkit/CraftTreeTypeTest.java b/paper-server/src/test/java/org/bukkit/craftbukkit/CraftTreeTypeTest.java new file mode 100644 index 000000000000..5de67e1be50c --- /dev/null +++ b/paper-server/src/test/java/org/bukkit/craftbukkit/CraftTreeTypeTest.java @@ -0,0 +1,73 @@ +package org.bukkit.craftbukkit; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import net.minecraft.data.worldgen.features.TreeFeatures; +import net.minecraft.resources.ResourceKey; +import net.minecraft.util.random.Weighted; +import net.minecraft.util.random.WeightedList; +import net.minecraft.world.level.block.grower.TreeGrower; +import net.minecraft.world.level.levelgen.feature.Feature; +import org.bukkit.TreeType; +import org.bukkit.support.environment.Normal; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Normal +public class CraftTreeTypeTest { + + // grower features that intentionally share a tree type with another feature + private static final Map, ResourceKey> ALIASES = Map.of( + TreeFeatures.OAK_BEES_005, TreeFeatures.OAK, + TreeFeatures.FANCY_OAK_BEES_005, TreeFeatures.FANCY_OAK, + TreeFeatures.BIRCH_BEES_005, TreeFeatures.BIRCH, + TreeFeatures.CHERRY_BEES_005, TreeFeatures.CHERRY, + TreeFeatures.PALE_OAK_BONEMEAL, TreeFeatures.PALE_OAK + ); + + @SuppressWarnings("unchecked") + public static Stream> growerFeatures() throws ReflectiveOperationException { + List> features = new ArrayList<>(); + for (Field growerField : TreeGrower.class.getDeclaredFields()) { + if (!Modifier.isStatic(growerField.getModifiers()) || growerField.getType() != TreeGrower.class) { + continue; + } + + TreeGrower grower = (TreeGrower) growerField.get(null); + for (Field listField : TreeGrower.class.getDeclaredFields()) { + if (Modifier.isStatic(listField.getModifiers()) || listField.getType() != WeightedList.class) { + continue; + } + + listField.setAccessible(true); + for (Weighted> weighted : ((WeightedList>) listField.get(grower)).unwrap()) { + features.add(weighted.value()); + } + } + } + return features.stream().distinct(); + } + + @ParameterizedTest + @MethodSource("growerFeatures") + public void growerFeatureRoundTrips(ResourceKey feature) { + TreeType treeType = assertDoesNotThrow(() -> CraftTreeType.fromFeature(feature), "No TreeType for grower feature " + feature); + assertEquals(ALIASES.getOrDefault(feature, feature), CraftTreeType.toFeature(treeType, true), "TreeType " + treeType + " does not generate grower feature " + feature); + } + + @ParameterizedTest + @EnumSource(value = TreeType.class, mode = EnumSource.Mode.EXCLUDE, names = "CHORUS_PLANT") + public void treeTypeRoundTrips(TreeType treeType) { + for (boolean normalWorld : new boolean[] {true, false}) { + assertEquals(treeType, CraftTreeType.fromFeature(CraftTreeType.toFeature(treeType, normalWorld)), "TreeType " + treeType + " does not map back from its feature"); + } + } +}