/* * Decompiled with CFR 0.152. * * Could not load the following classes: * com.mojang.logging.LogUtils * it.unimi.dsi.fastutil.objects.Object2IntMap * it.unimi.dsi.fastutil.objects.Object2IntMaps * it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap * org.jspecify.annotations.Nullable * org.slf4j.Logger */ package net.minecraft.world.level; import com.mojang.logging.LogUtils; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntMaps; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; import java.util.stream.Stream; import net.minecraft.SharedConstants; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.Holder; import net.minecraft.core.QuartPos; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; import net.minecraft.server.level.ServerLevel; import net.minecraft.tags.BiomeTags; import net.minecraft.tags.BlockTags; import net.minecraft.util.Mth; import net.minecraft.util.RandomSource; import net.minecraft.util.VisibleForDebug; import net.minecraft.util.profiling.Profiler; import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.util.random.WeightedList; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntitySpawnReason; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.Mob; import net.minecraft.world.entity.MobCategory; import net.minecraft.world.entity.SpawnGroupData; import net.minecraft.world.entity.SpawnPlacements; import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelReader; import net.minecraft.world.level.LocalMobCapCalculator; import net.minecraft.world.level.PotentialCalculator; import net.minecraft.world.level.ServerLevelAccessor; import net.minecraft.world.level.StructureManager; import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.biome.MobSpawnSettings; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.chunk.ChunkAccess; import net.minecraft.world.level.chunk.ChunkGenerator; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.level.levelgen.structure.BuiltinStructures; import net.minecraft.world.level.levelgen.structure.Structure; import net.minecraft.world.level.levelgen.structure.structures.NetherFortressStructure; import net.minecraft.world.level.material.FluidState; import net.minecraft.world.level.storage.LevelData; import net.minecraft.world.phys.Vec3; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; public final class NaturalSpawner { private static final Logger LOGGER = LogUtils.getLogger(); private static final int MIN_SPAWN_DISTANCE = 24; public static final int SPAWN_DISTANCE_CHUNK = 8; public static final int SPAWN_DISTANCE_BLOCK = 128; public static final int INSCRIBED_SQUARE_SPAWN_DISTANCE_CHUNK = Mth.floor(8.0f / Mth.SQRT_OF_TWO); private static final int MAGIC_NUMBER = (int)Math.pow(17.0, 2.0); private static final MobCategory[] SPAWNING_CATEGORIES = (MobCategory[])Stream.of(MobCategory.values()).filter(c -> c != MobCategory.MISC).toArray(MobCategory[]::new); private NaturalSpawner() { } public static SpawnState createState(int spawnableChunkCount, Iterable entities, ChunkGetter chunkGetter, LocalMobCapCalculator localMobCapCalculator) { PotentialCalculator spawnPotential = new PotentialCalculator(); Object2IntOpenHashMap mobCounts = new Object2IntOpenHashMap(); for (Entity entity : entities) { MobCategory category; Mob mob; if (entity instanceof Mob && ((mob = (Mob)entity).isPersistenceRequired() || mob.requiresCustomPersistence()) || (category = entity.getType().getCategory()) == MobCategory.MISC) continue; BlockPos pos = entity.blockPosition(); chunkGetter.query(ChunkPos.asLong(pos), chunk -> { MobSpawnSettings.MobSpawnCost mobSpawnCost = NaturalSpawner.getRoughBiome(pos, chunk).getMobSettings().getMobSpawnCost(entity.getType()); if (mobSpawnCost != null) { spawnPotential.addCharge(entity.blockPosition(), mobSpawnCost.charge()); } if (entity instanceof Mob) { localMobCapCalculator.addMob(chunk.getPos(), category); } mobCounts.addTo((Object)category, 1); }); } return new SpawnState(spawnableChunkCount, (Object2IntOpenHashMap)mobCounts, spawnPotential, localMobCapCalculator); } private static Biome getRoughBiome(BlockPos pos, ChunkAccess chunk) { return chunk.getNoiseBiome(QuartPos.fromBlock(pos.getX()), QuartPos.fromBlock(pos.getY()), QuartPos.fromBlock(pos.getZ())).value(); } public static List getFilteredSpawningCategories(SpawnState state, boolean spawnFriendlies, boolean spawnEnemies, boolean spawnPersistent) { ArrayList spawningCategories = new ArrayList(SPAWNING_CATEGORIES.length); for (MobCategory mobCategory : SPAWNING_CATEGORIES) { if (!spawnFriendlies && mobCategory.isFriendly() || !spawnEnemies && !mobCategory.isFriendly() || !spawnPersistent && mobCategory.isPersistent() || !state.canSpawnForCategoryGlobal(mobCategory)) continue; spawningCategories.add(mobCategory); } return spawningCategories; } public static void spawnForChunk(ServerLevel level, LevelChunk chunk, SpawnState state, List spawningCategories) { ProfilerFiller profiler = Profiler.get(); profiler.push("spawner"); for (MobCategory mobCategory : spawningCategories) { if (!state.canSpawnForCategoryLocal(mobCategory, chunk.getPos())) continue; NaturalSpawner.spawnCategoryForChunk(mobCategory, level, chunk, state::canSpawn, state::afterSpawn); } profiler.pop(); } public static void spawnCategoryForChunk(MobCategory mobCategory, ServerLevel level, LevelChunk chunk, SpawnPredicate extraTest, AfterSpawnCallback spawnCallback) { BlockPos start = NaturalSpawner.getRandomPosWithin(level, chunk); if (start.getY() < level.getMinY() + 1) { return; } NaturalSpawner.spawnCategoryForPosition(mobCategory, level, chunk, start, extraTest, spawnCallback); } @VisibleForDebug public static void spawnCategoryForPosition(MobCategory mobCategory, ServerLevel level, BlockPos start) { NaturalSpawner.spawnCategoryForPosition(mobCategory, level, level.getChunk(start), start, (type, chunk, pos) -> true, (mob, chunk) -> {}); } public static void spawnCategoryForPosition(MobCategory mobCategory, ServerLevel level, ChunkAccess chunk, BlockPos start, SpawnPredicate extraTest, AfterSpawnCallback spawnCallback) { StructureManager structureManager = level.structureManager(); ChunkGenerator generator = level.getChunkSource().getGenerator(); int yStart = start.getY(); BlockState state = chunk.getBlockState(start); if (state.isRedstoneConductor(chunk, start)) { return; } BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); int clusterSize = 0; block0: for (int groupCount = 0; groupCount < 3; ++groupCount) { int x = start.getX(); int z = start.getZ(); int ss = 6; MobSpawnSettings.SpawnerData currentSpawnData = null; SpawnGroupData groupData = null; int max = Mth.ceil(level.random.nextFloat() * 4.0f); int groupSize = 0; for (int ll = 0; ll < max; ++ll) { double nearestPlayerDistanceSqr; pos.set(x += level.random.nextInt(6) - level.random.nextInt(6), yStart, z += level.random.nextInt(6) - level.random.nextInt(6)); double xx = (double)x + 0.5; double zz = (double)z + 0.5; Player nearestPlayer = level.getNearestPlayer(xx, (double)yStart, zz, -1.0, false); if (nearestPlayer == null || !NaturalSpawner.isRightDistanceToPlayerAndSpawnPoint(level, chunk, pos, nearestPlayerDistanceSqr = nearestPlayer.distanceToSqr(xx, yStart, zz))) continue; if (currentSpawnData == null) { Optional nextSpawnData = NaturalSpawner.getRandomSpawnMobAt(level, structureManager, generator, mobCategory, level.random, pos); if (nextSpawnData.isEmpty()) continue block0; currentSpawnData = nextSpawnData.get(); max = currentSpawnData.minCount() + level.random.nextInt(1 + currentSpawnData.maxCount() - currentSpawnData.minCount()); } if (!NaturalSpawner.isValidSpawnPostitionForType(level, mobCategory, structureManager, generator, currentSpawnData, pos, nearestPlayerDistanceSqr) || !extraTest.test(currentSpawnData.type(), pos, chunk)) continue; Mob mob = NaturalSpawner.getMobForSpawn(level, currentSpawnData.type()); if (mob == null) { return; } mob.snapTo(xx, yStart, zz, level.random.nextFloat() * 360.0f, 0.0f); if (!NaturalSpawner.isValidPositionForMob(level, mob, nearestPlayerDistanceSqr)) continue; groupData = mob.finalizeSpawn(level, level.getCurrentDifficultyAt(mob.blockPosition()), EntitySpawnReason.NATURAL, groupData); ++groupSize; level.addFreshEntityWithPassengers(mob); spawnCallback.run(mob, chunk); if (++clusterSize >= mob.getMaxSpawnClusterSize()) { return; } if (mob.isMaxGroupSizeReached(groupSize)) continue block0; } } } private static boolean isRightDistanceToPlayerAndSpawnPoint(ServerLevel level, ChunkAccess chunk, BlockPos.MutableBlockPos pos, double nearestPlayerDistanceSqr) { if (nearestPlayerDistanceSqr <= 576.0) { return false; } LevelData.RespawnData respawnData = level.getRespawnData(); if (respawnData.dimension() == level.dimension() && respawnData.pos().closerToCenterThan(new Vec3((double)pos.getX() + 0.5, pos.getY(), (double)pos.getZ() + 0.5), 24.0)) { return false; } ChunkPos chunkPos = new ChunkPos(pos); return Objects.equals(chunkPos, chunk.getPos()) || level.canSpawnEntitiesInChunk(chunkPos); } private static boolean isValidSpawnPostitionForType(ServerLevel level, MobCategory mobCategory, StructureManager structureManager, ChunkGenerator generator, MobSpawnSettings.SpawnerData currentSpawnData, BlockPos.MutableBlockPos pos, double nearestPlayerDistanceSqr) { EntityType type = currentSpawnData.type(); if (type.getCategory() == MobCategory.MISC) { return false; } if (!type.canSpawnFarFromPlayer() && nearestPlayerDistanceSqr > (double)(type.getCategory().getDespawnDistance() * type.getCategory().getDespawnDistance())) { return false; } if (!type.canSummon() || !NaturalSpawner.canSpawnMobAt(level, structureManager, generator, mobCategory, currentSpawnData, pos)) { return false; } if (!SpawnPlacements.isSpawnPositionOk(type, level, pos)) { return false; } if (!SpawnPlacements.checkSpawnRules(type, level, EntitySpawnReason.NATURAL, pos, level.random)) { return false; } return level.noCollision(type.getSpawnAABB((double)pos.getX() + 0.5, pos.getY(), (double)pos.getZ() + 0.5)); } private static @Nullable Mob getMobForSpawn(ServerLevel level, EntityType type) { try { Object obj = type.create(level, EntitySpawnReason.NATURAL); if (obj instanceof Mob) { Mob mob = (Mob)obj; return mob; } LOGGER.warn("Can't spawn entity of type: {}", (Object)BuiltInRegistries.ENTITY_TYPE.getKey(type)); } catch (Exception e) { LOGGER.warn("Failed to create mob", (Throwable)e); } return null; } private static boolean isValidPositionForMob(ServerLevel level, Mob mob, double nearestPlayerDistanceSqr) { if (nearestPlayerDistanceSqr > (double)(mob.getType().getCategory().getDespawnDistance() * mob.getType().getCategory().getDespawnDistance()) && mob.removeWhenFarAway(nearestPlayerDistanceSqr)) { return false; } return mob.checkSpawnRules(level, EntitySpawnReason.NATURAL) && mob.checkSpawnObstruction(level); } private static Optional getRandomSpawnMobAt(ServerLevel level, StructureManager structureManager, ChunkGenerator generator, MobCategory mobCategory, RandomSource random, BlockPos pos) { Holder biome = level.getBiome(pos); if (mobCategory == MobCategory.WATER_AMBIENT && biome.is(BiomeTags.REDUCED_WATER_AMBIENT_SPAWNS) && random.nextFloat() < 0.98f) { return Optional.empty(); } return NaturalSpawner.mobsAt(level, structureManager, generator, mobCategory, pos, biome).getRandom(random); } private static boolean canSpawnMobAt(ServerLevel level, StructureManager structureManager, ChunkGenerator generator, MobCategory mobCategory, MobSpawnSettings.SpawnerData spawnerData, BlockPos pos) { return NaturalSpawner.mobsAt(level, structureManager, generator, mobCategory, pos, null).contains(spawnerData); } private static WeightedList mobsAt(ServerLevel level, StructureManager structureManager, ChunkGenerator generator, MobCategory mobCategory, BlockPos pos, @Nullable Holder biome) { if (NaturalSpawner.isInNetherFortressBounds(pos, level, mobCategory, structureManager)) { return NetherFortressStructure.FORTRESS_ENEMIES; } return generator.getMobsAt(biome != null ? biome : level.getBiome(pos), structureManager, mobCategory, pos); } public static boolean isInNetherFortressBounds(BlockPos pos, ServerLevel level, MobCategory category, StructureManager structureManager) { if (category != MobCategory.MONSTER || !level.getBlockState(pos.below()).is(Blocks.NETHER_BRICKS)) { return false; } Structure fortress = structureManager.registryAccess().lookupOrThrow(Registries.STRUCTURE).getValue(BuiltinStructures.FORTRESS); if (fortress == null) { return false; } return structureManager.getStructureAt(pos, fortress).isValid(); } private static BlockPos getRandomPosWithin(Level level, LevelChunk chunk) { ChunkPos pos = chunk.getPos(); int x = pos.getMinBlockX() + level.random.nextInt(16); int z = pos.getMinBlockZ() + level.random.nextInt(16); int topEmptyY = chunk.getHeight(Heightmap.Types.WORLD_SURFACE, x, z) + 1; int y = Mth.randomBetweenInclusive(level.random, level.getMinY(), topEmptyY); return new BlockPos(x, y, z); } public static boolean isValidEmptySpawnBlock(BlockGetter level, BlockPos pos, BlockState blockState, FluidState fluidState, EntityType type) { if (blockState.isCollisionShapeFullBlock(level, pos)) { return false; } if (blockState.isSignalSource()) { return false; } if (!fluidState.isEmpty()) { return false; } if (blockState.is(BlockTags.PREVENT_MOB_SPAWNING_INSIDE)) { return false; } return !type.isBlockDangerous(blockState); } public static void spawnMobsForChunkGeneration(ServerLevelAccessor level, Holder biome, ChunkPos chunkPos, RandomSource random) { MobSpawnSettings mobSettings = biome.value().getMobSettings(); WeightedList mobs = mobSettings.getMobs(MobCategory.CREATURE); if (mobs.isEmpty()) { return; } int xo = chunkPos.getMinBlockX(); int zo = chunkPos.getMinBlockZ(); while (random.nextFloat() < mobSettings.getCreatureProbability()) { Optional nextSpawnerData = mobs.getRandom(random); if (nextSpawnerData.isEmpty()) continue; MobSpawnSettings.SpawnerData spawnerData = nextSpawnerData.get(); int count = spawnerData.minCount() + random.nextInt(1 + spawnerData.maxCount() - spawnerData.minCount()); SpawnGroupData groupSpawnData = null; int x = xo + random.nextInt(16); int z = zo + random.nextInt(16); int startX = x; int startZ = z; for (int i = 0; i < count; ++i) { boolean success = false; for (int attempts = 0; !success && attempts < 4; ++attempts) { BlockPos pos = NaturalSpawner.getTopNonCollidingPos(level, spawnerData.type(), x, z); if (spawnerData.type().canSummon() && SpawnPlacements.isSpawnPositionOk(spawnerData.type(), level, pos)) { Mob mob; Object entity; float width = spawnerData.type().getWidth(); double fx = Mth.clamp((double)x, (double)xo + (double)width, (double)xo + 16.0 - (double)width); double fz = Mth.clamp((double)z, (double)zo + (double)width, (double)zo + 16.0 - (double)width); if (!level.noCollision(spawnerData.type().getSpawnAABB(fx, pos.getY(), fz)) || !SpawnPlacements.checkSpawnRules(spawnerData.type(), level, EntitySpawnReason.CHUNK_GENERATION, BlockPos.containing(fx, pos.getY(), fz), level.getRandom())) continue; try { entity = spawnerData.type().create(level.getLevel(), EntitySpawnReason.NATURAL); } catch (Exception e) { LOGGER.warn("Failed to create mob", (Throwable)e); continue; } if (entity == null) continue; ((Entity)entity).snapTo(fx, pos.getY(), fz, random.nextFloat() * 360.0f, 0.0f); if (entity instanceof Mob && (mob = (Mob)entity).checkSpawnRules(level, EntitySpawnReason.CHUNK_GENERATION) && mob.checkSpawnObstruction(level)) { groupSpawnData = mob.finalizeSpawn(level, level.getCurrentDifficultyAt(mob.blockPosition()), EntitySpawnReason.CHUNK_GENERATION, groupSpawnData); level.addFreshEntityWithPassengers(mob); success = true; } } x += random.nextInt(5) - random.nextInt(5); z += random.nextInt(5) - random.nextInt(5); while (x < xo || x >= xo + 16 || z < zo || z >= zo + 16) { x = startX + random.nextInt(5) - random.nextInt(5); z = startZ + random.nextInt(5) - random.nextInt(5); } } } } } private static BlockPos getTopNonCollidingPos(LevelReader level, EntityType type, int x, int z) { int levelHeight = level.getHeight(SpawnPlacements.getHeightmapType(type), x, z); BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(x, levelHeight, z); if (level.dimensionType().hasCeiling()) { do { pos.move(Direction.DOWN); } while (!level.getBlockState(pos).isAir()); do { pos.move(Direction.DOWN); } while (level.getBlockState(pos).isAir() && pos.getY() > level.getMinY()); } return SpawnPlacements.getPlacementType(type).adjustSpawnPosition(level, pos.immutable()); } @FunctionalInterface public static interface ChunkGetter { public void query(long var1, Consumer var3); } public static class SpawnState { private final int spawnableChunkCount; private final Object2IntOpenHashMap mobCategoryCounts; private final PotentialCalculator spawnPotential; private final Object2IntMap unmodifiableMobCategoryCounts; private final LocalMobCapCalculator localMobCapCalculator; private @Nullable BlockPos lastCheckedPos; private @Nullable EntityType lastCheckedType; private double lastCharge; private SpawnState(int spawnableChunkCount, Object2IntOpenHashMap mobCategoryCounts, PotentialCalculator spawnPotential, LocalMobCapCalculator localMobCapCalculator) { this.spawnableChunkCount = spawnableChunkCount; this.mobCategoryCounts = mobCategoryCounts; this.spawnPotential = spawnPotential; this.localMobCapCalculator = localMobCapCalculator; this.unmodifiableMobCategoryCounts = Object2IntMaps.unmodifiable(mobCategoryCounts); } private boolean canSpawn(EntityType type, BlockPos testPos, ChunkAccess chunk) { double charge; this.lastCheckedPos = testPos; this.lastCheckedType = type; MobSpawnSettings.MobSpawnCost mobSpawnCost = NaturalSpawner.getRoughBiome(testPos, chunk).getMobSettings().getMobSpawnCost(type); if (mobSpawnCost == null) { this.lastCharge = 0.0; return true; } this.lastCharge = charge = mobSpawnCost.charge(); double energyChange = this.spawnPotential.getPotentialEnergyChange(testPos, charge); return energyChange <= mobSpawnCost.energyBudget(); } private void afterSpawn(Mob mob, ChunkAccess chunk) { MobSpawnSettings.MobSpawnCost mobSpawnCost; EntityType type = mob.getType(); BlockPos pos = mob.blockPosition(); double charge = pos.equals(this.lastCheckedPos) && type == this.lastCheckedType ? this.lastCharge : ((mobSpawnCost = NaturalSpawner.getRoughBiome(pos, chunk).getMobSettings().getMobSpawnCost(type)) != null ? mobSpawnCost.charge() : 0.0); this.spawnPotential.addCharge(pos, charge); MobCategory category = type.getCategory(); this.mobCategoryCounts.addTo((Object)category, 1); this.localMobCapCalculator.addMob(new ChunkPos(pos), category); } public int getSpawnableChunkCount() { return this.spawnableChunkCount; } public Object2IntMap getMobCategoryCounts() { return this.unmodifiableMobCategoryCounts; } private boolean canSpawnForCategoryGlobal(MobCategory mobCategory) { int maxMobCount = mobCategory.getMaxInstancesPerChunk() * this.spawnableChunkCount / MAGIC_NUMBER; return this.mobCategoryCounts.getInt((Object)mobCategory) < maxMobCount; } private boolean canSpawnForCategoryLocal(MobCategory mobCategory, ChunkPos chunkPos) { return this.localMobCapCalculator.canSpawn(mobCategory, chunkPos) || SharedConstants.DEBUG_IGNORE_LOCAL_MOB_CAP; } } @FunctionalInterface public static interface SpawnPredicate { public boolean test(EntityType var1, BlockPos var2, ChunkAccess var3); } @FunctionalInterface public static interface AfterSpawnCallback { public void run(Mob var1, ChunkAccess var2); } }