package org.torch.server; import java.util.List; import java.util.Random; import lombok.Getter; import org.bukkit.craftbukkit.util.LongHash; import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason; import org.spigotmc.AsyncCatcher; import org.torch.api.TorchReactor; import com.destroystokyo.paper.exception.ServerInternalException; import com.koloboke.collect.set.hash.HashLongSet; import com.koloboke.collect.set.hash.HashLongSets; import net.minecraft.server.*; import net.minecraft.server.BiomeBase.BiomeMeta; import net.minecraft.server.BlockPosition.MutableBlockPosition; import net.minecraft.server.EntityInsentient.EnumEntityPositionType; @Getter public final class TorchCreatureSpawner implements TorchReactor { /** The legacy */ private final SpawnerCreature servant; // public static final int MOB_COUNT_DIV = (int) Math.pow(17.0D, 2.0D); /** The 17x17 area around the player where mobs can spawn */ private final HashLongSet spawnableChunks = HashLongSets.newMutableSet(); /** Collect the chunks that will actually be eligible for spawning, vanilla 8 */ private static int RANGE; /** The chunk area covered by each player, vanilla 17x17 */ private static int CHUNKS_PER_PLAYER = (RANGE * 2 + 1) * (RANGE * 2 + 1); public TorchCreatureSpawner(SpawnerCreature legacy) { servant = legacy; } /** Returns entity count only from chunks being processed in spawnableChunks */ public int getEntityCount(WorldServer server, Class<?> creatureType) { // Paper - use entire world, not just active chunks. Spigot broke vanilla expectations. return server .getChunkProviderServer().getReactor() .chunks.values() .stream() .collect(java.util.stream.Collectors.summingInt(chunk -> chunk.entityCount.getOrDefault(creatureType, 0))); } /** * Adds all chunks within the spawn radius of the players to spawnableChunks * Returns number of spawnable chunks */ public int findChunksForSpawning(WorldServer world, boolean spawnHostileCreatures, boolean spawnPassiveCreatures, boolean spawnAnimals) { // Paper - At least until we figure out what is calling this async AsyncCatcher.catchOp("check for eligible spawn chunks"); if ((!spawnHostileCreatures && !spawnPassiveCreatures) || world.players.isEmpty()) return 0; this.spawnableChunks.clear(); int foundChunks = 0; if (RANGE == 0) { RANGE = world.spigotConfig.mobSpawnRange; RANGE = (RANGE > world.spigotConfig.viewDistance) ? world.spigotConfig.viewDistance : RANGE; RANGE = (RANGE > 8) ? 8 : RANGE; } for (final EntityHuman player : world.players) { if (player.isSpectator() || !player.affectsSpawning) continue; final int centerX = MathHelper.floor(player.locX / 16.0D); final int centerZ = MathHelper.floor(player.locZ / 16.0D); for (int levelX = -RANGE; levelX <= RANGE; levelX++) { for (int levelZ = -RANGE; levelZ <= RANGE; levelZ++) { boolean reachedEdge = levelX == -RANGE || levelX == RANGE || levelZ == -RANGE || levelZ == RANGE; ChunkCoordIntPair chunkPair = new ChunkCoordIntPair(levelX + centerX, levelZ + centerZ); foundChunks++; if (!reachedEdge && world.getWorldBorder().isInBounds(chunkPair)) { PlayerChunk chunk = world.getPlayerChunkMap().getChunk(chunkPair.x, chunkPair.z); if (chunk != null && chunk.isDone()) this.spawnableChunks.add(LongHash.toLong(chunkPair.x, chunkPair.z)); } } } } int spawnableChunks = 0; BlockPosition spawnPoint = world.getSpawn(); for (EnumCreatureType type : EnumCreatureType.values()) { // CraftBukkit - use per-world spawn limits int spawnLimit = type.getMaxNumberOfCreature(); switch (type) { case MONSTER: spawnLimit = world.getWorld().getMonsterSpawnLimit(); break; case CREATURE: spawnLimit = world.getWorld().getAnimalSpawnLimit(); break; case WATER_CREATURE: spawnLimit = world.getWorld().getWaterAnimalSpawnLimit(); break; case AMBIENT: spawnLimit = world.getWorld().getAmbientSpawnLimit(); break; } if (spawnLimit == 0) continue; int mobCount = 0; if ((!type.isPeaceful() || spawnPassiveCreatures) && (type.isPeaceful() || spawnHostileCreatures) && (!type.isAnimal() || spawnAnimals) && ((mobCount = getEntityCount(world, type.a())) <= spawnLimit * foundChunks / CHUNKS_PER_PLAYER)) { MutableBlockPosition currentPos = new BlockPosition.MutableBlockPosition(); int mobLimit = (spawnLimit * foundChunks / 256) - mobCount + 1; // Spigot - up to 1 more than limit SEARCH: for (Long hash : this.spawnableChunks) { if (mobLimit <= 0) return spawnableChunks; BlockPosition randomPos = createRandomPosition(world, LongHash.msw(hash), LongHash.lsw(hash)); int randomX = randomPos.getX(); int randomY = randomPos.getY(); int randomZ = randomPos.getZ(); final IBlockData block = world.getType(randomPos); if (!block.m() && block.getMaterial() == type.getCreatureMaterial()) { int spawnedEntity = 0; int research = 0; while (research < 3) { int cX = randomX; int cY = randomY; int cZ = randomZ; final int noise = 6; BiomeMeta spawnEntry = null; GroupDataEntity entityData = null; int respawn = 0; while (true) { if (respawn < 4) { SPAWN: { cX += world.random.nextInt(noise) - world.random.nextInt(noise); cY += world.random.nextInt(1) - world.random.nextInt(1); cZ += world.random.nextInt(noise) - world.random.nextInt(noise); currentPos.setValues(cX, cY, cZ); if (!world.isPlayerNearby(cZ, cY, cZ, 24.0D) && spawnPoint.distanceSquared(cX + 0.5F, cY, cZ + 0.5F) >= 576.0D) { if (spawnEntry == null) { spawnEntry = world.createRandomSpawnEntry(type, currentPos); if (spawnEntry == null) break SPAWN; } if (world.possibleToSpawn(type, spawnEntry, currentPos) && canCreatureTypeSpawnAtLocation(EntityPositionTypes.a(spawnEntry.entityClass()), world, currentPos)) { EntityInsentient entity; entity = createCreature(world, spawnEntry.entityClass()); if (entity == null) return spawnableChunks; entity.setPositionRotation(cX, cY, cZ, world.random.nextFloat() * 360.0F, 0.0F); if (entity.isNotColliding() && entity.canSpawn()) { entityData = entity.prepare(world.createDamageScaler(new BlockPosition(entity)), entityData); if (entity.canSpawn()) { if (world.addEntity(entity, SpawnReason.NATURAL)) { spawnedEntity++; mobLimit--; } } else { entity.die(); } if (mobLimit <= 0) continue SEARCH; // Spigot - If we're past limit, stop spawn } spawnableChunks += spawnedEntity; } } respawn++; continue; } // SPAWN LABEL END } research++; break; } } } } // SEARCH LABEL END } } return spawnableChunks; } public static BlockPosition createRandomPosition(World world, int chunkX, int chunkZ) { Chunk chunk = world.getChunkAt(chunkX, chunkZ); int x = chunkX * 16 + world.random.nextInt(16); int z = chunkZ * 16 + world.random.nextInt(16); final int y = world.random.nextInt(chunk == null ? world.getActualWorldHeight() : chunk.findFilledTop() + 16 - 1); return new BlockPosition(x, y, z); } public static boolean isValidEmptySpawnBlock(IBlockData blockData) { return blockData.l() ? false : (blockData.n() ? false : (blockData.getMaterial().isLiquid() ? false : !BlockMinecartTrackAbstract.i(blockData))); } /** * Returns whether or not the specified creature type can spawn at the specified location */ public static boolean canCreatureTypeSpawnAtLocation(EnumEntityPositionType type, World world, BlockPosition position) { if (!world.getWorldBorder().a(position)) return false; IBlockData blockType = world.getType(position); if (type == EnumEntityPositionType.IN_WATER) { return blockType.getMaterial() == Material.WATER && world.getType(position.down()).getMaterial() == Material.WATER && !world.getType(position.up()).m(); } BlockPosition down = position.down(); if (!world.getType(down).r()) { return false; } else { Block downBlock = world.getType(down).getBlock(); boolean downVaild = downBlock != Blocks.BEDROCK && downBlock != Blocks.BARRIER; return downVaild && isValidEmptySpawnBlock(blockType) && isValidEmptySpawnBlock(world.getType(position.up())); } } /** * Called during chunk generation to spawn initial creatures */ public static void performWorldGenerateSpawning(World world, BiomeBase biome, int i, int j, int k, int l, Random random) { List<BiomeMeta> spawnableTypes = biome.getMobs(EnumCreatureType.CREATURE); if (spawnableTypes.isEmpty()) return; while (random.nextFloat() < biome.getSpawningChance()) { BiomeMeta randomEntry = WeightedRandom.a(world.random, spawnableTypes); int groupCount = randomEntry.c + random.nextInt(1 + randomEntry.d - randomEntry.c); GroupDataEntity entityData = null; int x = i + random.nextInt(k); int z = j + random.nextInt(l); int l1 = x; int i2 = z; for (int index = 0; index < groupCount; index++) { boolean spawned = false; for (int retry = 0; !spawned && retry < 4; retry++) { BlockPosition topBlock = world.getTopSolidOrLiquidBlock(new BlockPosition(x, 0, z)); if (canCreatureTypeSpawnAtLocation(EnumEntityPositionType.ON_GROUND, world, topBlock)) { EntityInsentient entity = createCreature(world, randomEntry.b); if (entity == null) continue; entity.setPositionRotation(x + 0.5F, topBlock.getY(), z + 0.5F, random.nextFloat() * 360.0F, 0.0F); // CraftBukkit Added a reason for spawning this creature, moved entityinsentient.prepare(groupdataentity) up entityData = entity.prepare(world.createDamageScaler(new BlockPosition(entity)), entityData); world.addEntity(entity, SpawnReason.CHUNK_GEN); spawned = true; } x += random.nextInt(5) - random.nextInt(5); for (z += random.nextInt(5) - random.nextInt(5); x < i || x >= i + k || z < j || z >= j + k; z = i2 + random.nextInt(5) - random.nextInt(5)) { x = l1 + random.nextInt(5) - random.nextInt(5); } } } } } private static EntityInsentient createCreature(final World world, final Class<?> entityClass) { try { return (EntityInsentient) entityClass.getConstructor(World.class).newInstance(world); } catch (final Throwable t) { t.printStackTrace(); ServerInternalException.reportInternalException(t); } return null; } }