diff --git a/patches/minecraft/net/minecraft/world/entity/SpawnPlacements.java.patch b/patches/minecraft/net/minecraft/world/entity/SpawnPlacements.java.patch index 191cd28aa6..ebf14c4b9f 100644 --- a/patches/minecraft/net/minecraft/world/entity/SpawnPlacements.java.patch +++ b/patches/minecraft/net/minecraft/world/entity/SpawnPlacements.java.patch @@ -1,6 +1,14 @@ --- a/net/minecraft/world/entity/SpawnPlacements.java +++ b/net/minecraft/world/entity/SpawnPlacements.java -@@ -156,10 +_,26 @@ +@@ -42,6 +_,7 @@ + public class SpawnPlacements { + private static final Map, SpawnPlacements.Data> f_21750_ = Maps.newHashMap(); + ++ @Deprecated // FORGE: use SpawnPlacementRegisterEvent to register and modify spawn placements + public static void m_21754_(EntityType p_21755_, SpawnPlacements.Type p_21756_, Heightmap.Types p_21757_, SpawnPlacements.SpawnPredicate p_21758_) { + SpawnPlacements.Data spawnplacements$data = f_21750_.put(p_21755_, new SpawnPlacements.Data(p_21757_, p_21756_, p_21758_)); + if (spawnplacements$data != null) { +@@ -156,10 +_,35 @@ boolean m_217080_(EntityType p_217081_, ServerLevelAccessor p_217082_, MobSpawnType p_217083_, BlockPos p_217084_, RandomSource p_217085_); } @@ -26,5 +34,14 @@ + if (predicate == null) return net.minecraft.world.level.NaturalSpawner.canSpawnAtBody(this, world, pos, type); + return predicate.test(world, pos, type); + } ++ } ++ ++ // ******* FORGE START. INTERNAL USE ONLY! ****** // ++ public static void fireSpawnPlacementEvent() ++ { ++ Map, net.minecraftforge.event.entity.SpawnPlacementRegisterEvent.MergedSpawnPredicate> map = Maps.newHashMap(); ++ f_21750_.forEach((type, data) -> map.put(type, new net.minecraftforge.event.entity.SpawnPlacementRegisterEvent.MergedSpawnPredicate<>(data.f_21769_, data.f_21768_, data.f_21767_))); ++ net.minecraftforge.fml.ModLoader.get().postEvent(new net.minecraftforge.event.entity.SpawnPlacementRegisterEvent(map)); ++ map.forEach(((entityType, merged) -> f_21750_.put(entityType, new SpawnPlacements.Data(merged.getHeightmapType(), merged.getSpawnType(), merged.build())))); } } diff --git a/src/main/java/net/minecraftforge/event/entity/SpawnPlacementRegisterEvent.java b/src/main/java/net/minecraftforge/event/entity/SpawnPlacementRegisterEvent.java new file mode 100644 index 0000000000..f971191404 --- /dev/null +++ b/src/main/java/net/minecraftforge/event/entity/SpawnPlacementRegisterEvent.java @@ -0,0 +1,212 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.event.entity; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nullable; + +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.SpawnPlacements; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraftforge.eventbus.api.Cancelable; +import net.minecraftforge.eventbus.api.Event; +import net.minecraftforge.eventbus.api.EventPriority; +import net.minecraftforge.fml.event.IModBusEvent; +import net.minecraftforge.registries.ForgeRegistries; + +import org.jetbrains.annotations.ApiStatus; + +/** + * This event allows each {@link EntityType} to have a {@link SpawnPlacements.SpawnPredicate} registered or modified. + * Spawn Predicates are checked whenever an {@link Entity} of the given {@link EntityType} spawns in the world naturally. + * + * If registering your own entity's spawn placements, you should use {@link SpawnPlacementRegisterEvent#register(EntityType, SpawnPlacements.Type, Heightmap.Types, SpawnPlacements.SpawnPredicate, Operation)} + * So that you ensure that your entity has a heightmap type and placement type registered. + * + * If modifying vanilla or another mod's spawn placements, you can use three operations: + * REPLACE: checked first, the last mod to replace the predicate wipes out all other predicates. Listen with a low {@link EventPriority} if you need to do this. + * OR: checked second, only one of these predicates must pass along with the original predicate + * AND: checked third, these predicates must all pass along with the original predicate + * + *

+ * This event is not {@linkplain Cancelable cancellable} and does not {@linkplain Event.HasResult have a result}. + *

+ * + * Fired on the Mod bus {@link IModBusEvent}.
+ */ +public class SpawnPlacementRegisterEvent extends Event implements IModBusEvent +{ + private final Map, MergedSpawnPredicate> map; + + @ApiStatus.Internal + public SpawnPlacementRegisterEvent(Map, MergedSpawnPredicate> map) + { + this.map = map; + } + + /** + * Register an optional spawn placement {@code predicate} for a given {@code entityType} + */ + public void register(EntityType entityType, SpawnPlacements.SpawnPredicate predicate) + { + register(entityType, null, null, predicate, Operation.OR); + } + + /** + * Register a {@code predicate} for a given {@code entityType} with a given {@code operation} for handling + */ + public void register(EntityType entityType, SpawnPlacements.SpawnPredicate predicate, Operation operation) + { + register(entityType, null, null, predicate, operation); + } + + /** + * Register a {@code predicate} for a given {@code entityType} and {@code operation} + * With the option of changing the {@code placementType} and {@code heightmap}. These are only applied if {@link Operation#REPLACE} is used. + * Use {@code null} for the placement or heightmap to leave them as is (which should be done in almost every case) + */ + @SuppressWarnings("unchecked") + public void register(EntityType entityType, @Nullable SpawnPlacements.Type placementType, @Nullable Heightmap.Types heightmap, SpawnPlacements.SpawnPredicate predicate, Operation operation) + { + if (!map.containsKey(entityType)) + { + if (placementType == null) + { + throw new NullPointerException("Registering a new Spawn Predicate requires a nonnull placement type! Entity Type: " + ForgeRegistries.ENTITY_TYPES.getKey(entityType)); + } + if (heightmap == null) + { + throw new NullPointerException("Registering a new Spawn Predicate requires a nonnull heightmap type! Entity Type: "+ ForgeRegistries.ENTITY_TYPES.getKey(entityType)); + } + map.put(entityType, new MergedSpawnPredicate<>(predicate, placementType, heightmap)); + } + else + { + if (operation != Operation.REPLACE && (heightmap != null || placementType != null)) + { + throw new IllegalStateException("Nonnull heightmap types or spawn placement types may only be used with the REPLACE operation. Entity Type: "+ ForgeRegistries.ENTITY_TYPES.getKey(entityType)); + } + ((MergedSpawnPredicate) map.get(entityType)).merge(operation, predicate, placementType, heightmap); + } + } + + public enum Operation + { + /** + * Checked third, these predicates must all pass along with the original predicate + */ + AND, + /** + * Checked second, only one of these predicates must pass along with the original predicate + */ + OR, + /** + * Checked first, the last mod to replace the predicate wipes out all other predicates. Listen with a low {@link EventPriority} if you need to do this. + */ + REPLACE + } + + public static class MergedSpawnPredicate + { + private final SpawnPlacements.SpawnPredicate originalPredicate; + private final List> orPredicates; + private final List> andPredicates; + @Nullable private SpawnPlacements.SpawnPredicate replacementPredicate; + private SpawnPlacements.Type spawnType; + private Heightmap.Types heightmapType; + + public MergedSpawnPredicate(SpawnPlacements.SpawnPredicate originalPredicate, SpawnPlacements.Type spawnType, Heightmap.Types heightmapType) + { + this.originalPredicate = originalPredicate; + this.orPredicates = new ArrayList<>(); + this.andPredicates = new ArrayList<>(); + this.replacementPredicate = null; + this.spawnType = spawnType; + this.heightmapType = heightmapType; + } + + public SpawnPlacements.Type getSpawnType() + { + return spawnType; + } + + public Heightmap.Types getHeightmapType() + { + return heightmapType; + } + + private void merge(Operation operation, SpawnPlacements.SpawnPredicate predicate, @Nullable SpawnPlacements.Type spawnType, @Nullable Heightmap.Types heightmapType) + { + if (operation == Operation.AND) + { + andPredicates.add(predicate); + } + else if (operation == Operation.OR) + { + orPredicates.add(predicate); + } + else if (operation == Operation.REPLACE) + { + replacementPredicate = predicate; + if (spawnType != null) + { + this.spawnType = spawnType; + } + if (heightmapType != null) + { + this.heightmapType = heightmapType; + } + } + } + + @ApiStatus.Internal + public SpawnPlacements.SpawnPredicate build() + { + if (replacementPredicate != null) + { + return replacementPredicate; + } + final var original = originalPredicate; + + final SpawnPlacements.SpawnPredicate compiledOrPredicate = (entityType, level, spawnType, pos, random) -> { + if (original.test(entityType, level, spawnType, pos, random)) + { + return true; + } + for (SpawnPlacements.SpawnPredicate predicate : orPredicates) + { + if (predicate.test(entityType, level, spawnType, pos, random)) + { + return true; + } + } + return false; + }; + + final SpawnPlacements.SpawnPredicate compiledAndPredicate = (entityType, level, spawnType, pos, random) -> { + if (!compiledOrPredicate.test(entityType, level, spawnType, pos, random)) + { + return false; + } + for (SpawnPlacements.SpawnPredicate predicate : andPredicates) + { + if (!predicate.test(entityType, level, spawnType, pos, random)) + { + return false; + } + } + return true; + }; + + return compiledAndPredicate; + } + } + +} diff --git a/src/main/java/net/minecraftforge/registries/GameData.java b/src/main/java/net/minecraftforge/registries/GameData.java index b3031babf5..e1a8516322 100644 --- a/src/main/java/net/minecraftforge/registries/GameData.java +++ b/src/main/java/net/minecraftforge/registries/GameData.java @@ -23,6 +23,7 @@ import net.minecraft.network.syncher.EntityDataSerializer; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.SpawnPlacements; import net.minecraft.world.entity.ai.attributes.Attribute; import net.minecraft.world.entity.ai.attributes.DefaultAttributes; import net.minecraft.world.entity.ai.village.poi.PoiType; @@ -359,6 +360,7 @@ public class GameData } else { ForgeHooks.modifyAttributes(); + SpawnPlacements.fireSpawnPlacementEvent(); } } diff --git a/src/test/java/net/minecraftforge/debug/entity/SpawnPlacementTest.java b/src/test/java/net/minecraftforge/debug/entity/SpawnPlacementTest.java new file mode 100644 index 0000000000..7a2b61ff05 --- /dev/null +++ b/src/test/java/net/minecraftforge/debug/entity/SpawnPlacementTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.debug.entity; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.Difficulty; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.biome.Biomes; +import net.minecraftforge.event.entity.SpawnPlacementRegisterEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; + +import com.mojang.logging.LogUtils; +import org.slf4j.Logger; + +@Mod("spawn_placement_test") +public class SpawnPlacementTest +{ + public static final boolean ENABLED = false; + public static final Logger LOGGER = LogUtils.getLogger(); + + public SpawnPlacementTest() + { + if (ENABLED) + { + FMLJavaModLoadingContext.get().getModEventBus().addListener(this::onSpawnPlacementRegister); + } + } + + private void onSpawnPlacementRegister(SpawnPlacementRegisterEvent event) + { + LOGGER.info("Modifying spawn placements!"); + // AND: require zombies to spawn below y 40 + event.register(EntityType.ZOMBIE, ((entityType, level, spawnType, pos, random) -> pos.getY() < 40 && validMonsterSpawn(level, pos, entityType)), SpawnPlacementRegisterEvent.Operation.AND); + // REPLACE: don't require darkness to spawn creepers + event.register(EntityType.CREEPER, ((entityType, level, spawnType, pos, random) -> validMonsterSpawn(level, pos, entityType)), SpawnPlacementRegisterEvent.Operation.REPLACE); + // OR: allow slimes to spawn in plains + event.register(EntityType.SLIME, (entityType, level, spawnType, pos, random) -> validMonsterSpawn(level, pos, entityType) && level.getBiome(pos).is(Biomes.PLAINS)); + } + + private static boolean validMonsterSpawn(ServerLevelAccessor level, BlockPos pos, EntityType type) + { + return level.getDifficulty() != Difficulty.PEACEFUL && level.getBlockState(pos.below()).isValidSpawn(level, pos.below(), type); + } +} diff --git a/src/test/resources/META-INF/mods.toml b/src/test/resources/META-INF/mods.toml index 56cbc5b701..a0fb47688f 100644 --- a/src/test/resources/META-INF/mods.toml +++ b/src/test/resources/META-INF/mods.toml @@ -173,6 +173,8 @@ modId="tag_based_tool_types" [[mods]] modId="check_spawn_event_test" [[mods]] +modId="spawn_placement_test" +[[mods]] modId="redstone_sided_connectivity_test" [[mods]] modId="permissions_changed_event_test"