package net.minecraft.world.level.dimension.end; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ContiguousSet; import com.google.common.collect.DiscreteDomain; import com.google.common.collect.Lists; import com.google.common.collect.Range; import com.google.common.collect.Sets; import com.mojang.logging.LogUtils; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; import com.mojang.serialization.codecs.RecordCodecBuilder.Instance; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.function.Predicate; import javax.annotation.Nullable; import net.minecraft.Util; import net.minecraft.advancements.CriteriaTriggers; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.Holder; import net.minecraft.core.Registry; import net.minecraft.core.UUIDUtil; import net.minecraft.core.registries.Registries; import net.minecraft.data.worldgen.features.EndFeatures; import net.minecraft.network.chat.Component; import net.minecraft.server.level.FullChunkStatus; import net.minecraft.server.level.ServerBossEvent; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.TicketType; import net.minecraft.util.Mth; import net.minecraft.util.RandomSource; import net.minecraft.world.BossEvent; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntitySelector; import net.minecraft.world.entity.EntitySpawnReason; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.boss.enderdragon.EndCrystal; import net.minecraft.world.entity.boss.enderdragon.EnderDragon; import net.minecraft.world.entity.boss.enderdragon.phases.EnderDragonPhase; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.TheEndPortalBlockEntity; import net.minecraft.world.level.block.state.pattern.BlockInWorld; import net.minecraft.world.level.block.state.pattern.BlockPattern; import net.minecraft.world.level.block.state.pattern.BlockPatternBuilder; import net.minecraft.world.level.block.state.predicate.BlockPredicate; import net.minecraft.world.level.chunk.ChunkAccess; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.chunk.status.ChunkStatus; import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.level.levelgen.feature.EndPodiumFeature; import net.minecraft.world.level.levelgen.feature.SpikeFeature; import net.minecraft.world.level.levelgen.feature.configurations.FeatureConfiguration; import net.minecraft.world.phys.AABB; import org.slf4j.Logger; public class EndDragonFight { private static final Logger LOGGER = LogUtils.getLogger(); private static final int MAX_TICKS_BEFORE_DRAGON_RESPAWN = 1200; private static final int TIME_BETWEEN_CRYSTAL_SCANS = 100; public static final int TIME_BETWEEN_PLAYER_SCANS = 20; private static final int ARENA_SIZE_CHUNKS = 8; public static final int ARENA_TICKET_LEVEL = 9; private static final int GATEWAY_COUNT = 20; private static final int GATEWAY_DISTANCE = 96; public static final int DRAGON_SPAWN_Y = 128; private final Predicate validPlayer; private final ServerBossEvent dragonEvent = (ServerBossEvent)new ServerBossEvent( Component.translatable("entity.minecraft.ender_dragon"), BossEvent.BossBarColor.PINK, BossEvent.BossBarOverlay.PROGRESS ) .setPlayBossMusic(true) .setCreateWorldFog(true); private final ServerLevel level; private final BlockPos origin; private final ObjectArrayList gateways = new ObjectArrayList<>(); private final BlockPattern exitPortalPattern; private int ticksSinceDragonSeen; private int crystalsAlive; private int ticksSinceCrystalsScanned; private int ticksSinceLastPlayerScan = 21; private boolean dragonKilled; private boolean previouslyKilled; private boolean skipArenaLoadedCheck = false; @Nullable private UUID dragonUUID; private boolean needsStateScanning = true; @Nullable private BlockPos portalLocation; @Nullable private DragonRespawnAnimation respawnStage; private int respawnTime; @Nullable private List respawnCrystals; public EndDragonFight(ServerLevel p_289759_, long p_289805_, EndDragonFight.Data p_289800_) { this(p_289759_, p_289805_, p_289800_, BlockPos.ZERO); } public EndDragonFight(ServerLevel p_289771_, long p_289793_, EndDragonFight.Data p_289768_, BlockPos p_289794_) { this.level = p_289771_; this.origin = p_289794_; this.validPlayer = EntitySelector.ENTITY_STILL_ALIVE.and(EntitySelector.withinDistance(p_289794_.getX(), 128 + p_289794_.getY(), p_289794_.getZ(), 192.0)); this.needsStateScanning = p_289768_.needsStateScanning; this.dragonUUID = p_289768_.dragonUUID.orElse(null); this.dragonKilled = p_289768_.dragonKilled; this.previouslyKilled = p_289768_.previouslyKilled; if (p_289768_.isRespawning) { this.respawnStage = DragonRespawnAnimation.START; } this.portalLocation = p_289768_.exitPortalLocation.orElse(null); this.gateways.addAll(p_289768_.gateways.orElseGet(() -> { ObjectArrayList objectarraylist = new ObjectArrayList<>(ContiguousSet.create(Range.closedOpen(0, 20), DiscreteDomain.integers())); Util.shuffle(objectarraylist, RandomSource.create(p_289793_)); return objectarraylist; })); this.exitPortalPattern = BlockPatternBuilder.start() .aisle(" ", " ", " ", " # ", " ", " ", " ") .aisle(" ", " ", " ", " # ", " ", " ", " ") .aisle(" ", " ", " ", " # ", " ", " ", " ") .aisle(" ### ", " # # ", "# #", "# # #", "# #", " # # ", " ### ") .aisle(" ", " ### ", " ##### ", " ##### ", " ##### ", " ### ", " ") .where('#', BlockInWorld.hasState(BlockPredicate.forBlock(Blocks.BEDROCK))) .build(); } @Deprecated @VisibleForTesting public void skipArenaLoadedCheck() { this.skipArenaLoadedCheck = true; } public EndDragonFight.Data saveData() { return new EndDragonFight.Data( this.needsStateScanning, this.dragonKilled, this.previouslyKilled, false, Optional.ofNullable(this.dragonUUID), Optional.ofNullable(this.portalLocation), Optional.of(this.gateways) ); } public void tick() { this.dragonEvent.setVisible(!this.dragonKilled); if (++this.ticksSinceLastPlayerScan >= 20) { this.updatePlayers(); this.ticksSinceLastPlayerScan = 0; } if (!this.dragonEvent.getPlayers().isEmpty()) { this.level.getChunkSource().addTicketWithRadius(TicketType.DRAGON, new ChunkPos(0, 0), 9); boolean flag = this.isArenaLoaded(); if (this.needsStateScanning && flag) { this.scanState(); this.needsStateScanning = false; } if (this.respawnStage != null) { if (this.respawnCrystals == null && flag) { this.respawnStage = null; this.tryRespawn(); } this.respawnStage.tick(this.level, this, this.respawnCrystals, this.respawnTime++, this.portalLocation); } if (!this.dragonKilled) { if ((this.dragonUUID == null || ++this.ticksSinceDragonSeen >= 1200) && flag) { this.findOrCreateDragon(); this.ticksSinceDragonSeen = 0; } if (++this.ticksSinceCrystalsScanned >= 100 && flag) { this.updateCrystalCount(); this.ticksSinceCrystalsScanned = 0; } } } else { this.level.getChunkSource().removeTicketWithRadius(TicketType.DRAGON, new ChunkPos(0, 0), 9); } } private void scanState() { LOGGER.info("Scanning for legacy world dragon fight..."); boolean flag = this.hasActiveExitPortal(); if (flag) { LOGGER.info("Found that the dragon has been killed in this world already."); this.previouslyKilled = true; } else { LOGGER.info("Found that the dragon has not yet been killed in this world."); this.previouslyKilled = false; if (this.findExitPortal() == null) { this.spawnExitPortal(false); } } List list = this.level.getDragons(); if (list.isEmpty()) { this.dragonKilled = true; } else { EnderDragon enderdragon = list.get(0); this.dragonUUID = enderdragon.getUUID(); LOGGER.info("Found that there's a dragon still alive ({})", enderdragon); this.dragonKilled = false; if (!flag) { LOGGER.info("But we didn't have a portal, let's remove it."); enderdragon.discard(); this.dragonUUID = null; } } if (!this.previouslyKilled && this.dragonKilled) { this.dragonKilled = false; } } private void findOrCreateDragon() { List list = this.level.getDragons(); if (list.isEmpty()) { LOGGER.debug("Haven't seen the dragon, respawning it"); this.createNewDragon(); } else { LOGGER.debug("Haven't seen our dragon, but found another one to use."); this.dragonUUID = list.get(0).getUUID(); } } protected void setRespawnStage(DragonRespawnAnimation p_64088_) { if (this.respawnStage == null) { throw new IllegalStateException("Dragon respawn isn't in progress, can't skip ahead in the animation."); } else { this.respawnTime = 0; if (p_64088_ == DragonRespawnAnimation.END) { this.respawnStage = null; this.dragonKilled = false; EnderDragon enderdragon = this.createNewDragon(); if (enderdragon != null) { for (ServerPlayer serverplayer : this.dragonEvent.getPlayers()) { CriteriaTriggers.SUMMONED_ENTITY.trigger(serverplayer, enderdragon); } } } else { this.respawnStage = p_64088_; } } } private boolean hasActiveExitPortal() { for (int i = -8; i <= 8; i++) { for (int j = -8; j <= 8; j++) { LevelChunk levelchunk = this.level.getChunk(i, j); for (BlockEntity blockentity : levelchunk.getBlockEntities().values()) { if (blockentity instanceof TheEndPortalBlockEntity) { return true; } } } } return false; } @Nullable private BlockPattern.BlockPatternMatch findExitPortal() { ChunkPos chunkpos = new ChunkPos(this.origin); for (int i = -8 + chunkpos.x; i <= 8 + chunkpos.x; i++) { for (int j = -8 + chunkpos.z; j <= 8 + chunkpos.z; j++) { LevelChunk levelchunk = this.level.getChunk(i, j); for (BlockEntity blockentity : levelchunk.getBlockEntities().values()) { if (blockentity instanceof TheEndPortalBlockEntity) { BlockPattern.BlockPatternMatch blockpattern$blockpatternmatch = this.exitPortalPattern.find(this.level, blockentity.getBlockPos()); if (blockpattern$blockpatternmatch != null) { BlockPos blockpos = blockpattern$blockpatternmatch.getBlock(3, 3, 3).getPos(); if (this.portalLocation == null) { this.portalLocation = blockpos; } return blockpattern$blockpatternmatch; } } } } } BlockPos blockpos1 = EndPodiumFeature.getLocation(this.origin); int k = this.level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, blockpos1).getY(); for (int l = k; l >= this.level.getMinY(); l--) { BlockPattern.BlockPatternMatch blockpattern$blockpatternmatch1 = this.exitPortalPattern .find(this.level, new BlockPos(blockpos1.getX(), l, blockpos1.getZ())); if (blockpattern$blockpatternmatch1 != null) { if (this.portalLocation == null) { this.portalLocation = blockpattern$blockpatternmatch1.getBlock(3, 3, 3).getPos(); } return blockpattern$blockpatternmatch1; } } return null; } private boolean isArenaLoaded() { if (this.skipArenaLoadedCheck) { return true; } else { ChunkPos chunkpos = new ChunkPos(this.origin); for (int i = -8 + chunkpos.x; i <= 8 + chunkpos.x; i++) { for (int j = 8 + chunkpos.z; j <= 8 + chunkpos.z; j++) { ChunkAccess chunkaccess = this.level.getChunk(i, j, ChunkStatus.FULL, false); if (!(chunkaccess instanceof LevelChunk)) { return false; } FullChunkStatus fullchunkstatus = ((LevelChunk)chunkaccess).getFullStatus(); if (!fullchunkstatus.isOrAfter(FullChunkStatus.BLOCK_TICKING)) { return false; } } } return true; } } private void updatePlayers() { Set set = Sets.newHashSet(); for (ServerPlayer serverplayer : this.level.getPlayers(this.validPlayer)) { this.dragonEvent.addPlayer(serverplayer); set.add(serverplayer); } Set set1 = Sets.newHashSet(this.dragonEvent.getPlayers()); set1.removeAll(set); for (ServerPlayer serverplayer1 : set1) { this.dragonEvent.removePlayer(serverplayer1); } } private void updateCrystalCount() { this.ticksSinceCrystalsScanned = 0; this.crystalsAlive = 0; for (SpikeFeature.EndSpike spikefeature$endspike : SpikeFeature.getSpikesForLevel(this.level)) { this.crystalsAlive = this.crystalsAlive + this.level.getEntitiesOfClass(EndCrystal.class, spikefeature$endspike.getTopBoundingBox()).size(); } LOGGER.debug("Found {} end crystals still alive", this.crystalsAlive); } public void setDragonKilled(EnderDragon p_64086_) { if (p_64086_.getUUID().equals(this.dragonUUID)) { this.dragonEvent.setProgress(0.0F); this.dragonEvent.setVisible(false); this.spawnExitPortal(true); this.spawnNewGateway(); if (!this.previouslyKilled) { this.level .setBlockAndUpdate(this.level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, EndPodiumFeature.getLocation(this.origin)), Blocks.DRAGON_EGG.defaultBlockState()); } this.previouslyKilled = true; this.dragonKilled = true; } } @Deprecated @VisibleForTesting public void removeAllGateways() { this.gateways.clear(); } private void spawnNewGateway() { if (!this.gateways.isEmpty()) { int i = this.gateways.remove(this.gateways.size() - 1); int j = Mth.floor(96.0 * Math.cos(2.0 * (-Math.PI + (Math.PI / 20) * i))); int k = Mth.floor(96.0 * Math.sin(2.0 * (-Math.PI + (Math.PI / 20) * i))); this.spawnNewGateway(new BlockPos(j, 75, k)); } } private void spawnNewGateway(BlockPos p_64090_) { this.level.levelEvent(3000, p_64090_, 0); this.level .registryAccess() .lookup(Registries.CONFIGURED_FEATURE) .flatMap(p_360583_ -> p_360583_.get(EndFeatures.END_GATEWAY_DELAYED)) .ifPresent(p_256486_ -> p_256486_.value().place(this.level, this.level.getChunkSource().getGenerator(), RandomSource.create(), p_64090_)); } private void spawnExitPortal(boolean p_64094_) { EndPodiumFeature endpodiumfeature = new EndPodiumFeature(p_64094_); if (this.portalLocation == null) { this.portalLocation = this.level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, EndPodiumFeature.getLocation(this.origin)).below(); while (this.level.getBlockState(this.portalLocation).is(Blocks.BEDROCK) && this.portalLocation.getY() > 63) { this.portalLocation = this.portalLocation.below(); } this.portalLocation = this.portalLocation.atY(Math.max(this.level.getMinY() + 1, this.portalLocation.getY())); } if (endpodiumfeature.place(FeatureConfiguration.NONE, this.level, this.level.getChunkSource().getGenerator(), RandomSource.create(), this.portalLocation) ) { int i = Mth.positiveCeilDiv(4, 16); this.level.getChunkSource().chunkMap.waitForLightBeforeSending(new ChunkPos(this.portalLocation), i); } } @Nullable private EnderDragon createNewDragon() { this.level.getChunkAt(new BlockPos(this.origin.getX(), 128 + this.origin.getY(), this.origin.getZ())); EnderDragon enderdragon = EntityType.ENDER_DRAGON.create(this.level, EntitySpawnReason.EVENT); if (enderdragon != null) { enderdragon.setDragonFight(this); enderdragon.setFightOrigin(this.origin); enderdragon.getPhaseManager().setPhase(EnderDragonPhase.HOLDING_PATTERN); enderdragon.snapTo( this.origin.getX(), 128 + this.origin.getY(), this.origin.getZ(), this.level.random.nextFloat() * 360.0F, 0.0F ); this.level.addFreshEntity(enderdragon); this.dragonUUID = enderdragon.getUUID(); } return enderdragon; } public void updateDragon(EnderDragon p_64097_) { if (p_64097_.getUUID().equals(this.dragonUUID)) { this.dragonEvent.setProgress(p_64097_.getHealth() / p_64097_.getMaxHealth()); this.ticksSinceDragonSeen = 0; if (p_64097_.hasCustomName()) { this.dragonEvent.setName(p_64097_.getDisplayName()); } } } public int getCrystalsAlive() { return this.crystalsAlive; } public void onCrystalDestroyed(EndCrystal p_64083_, DamageSource p_64084_) { if (this.respawnStage != null && this.respawnCrystals.contains(p_64083_)) { LOGGER.debug("Aborting respawn sequence"); this.respawnStage = null; this.respawnTime = 0; this.resetSpikeCrystals(); this.spawnExitPortal(true); } else { this.updateCrystalCount(); if (this.level.getEntity(this.dragonUUID) instanceof EnderDragon enderdragon) { enderdragon.onCrystalDestroyed(this.level, p_64083_, p_64083_.blockPosition(), p_64084_); } } } public boolean hasPreviouslyKilledDragon() { return this.previouslyKilled; } public void tryRespawn() { if (this.dragonKilled && this.respawnStage == null) { BlockPos blockpos = this.portalLocation; if (blockpos == null) { LOGGER.debug("Tried to respawn, but need to find the portal first."); BlockPattern.BlockPatternMatch blockpattern$blockpatternmatch = this.findExitPortal(); if (blockpattern$blockpatternmatch == null) { LOGGER.debug("Couldn't find a portal, so we made one."); this.spawnExitPortal(true); } else { LOGGER.debug("Found the exit portal & saved its location for next time."); } blockpos = this.portalLocation; } List list1 = Lists.newArrayList(); BlockPos blockpos1 = blockpos.above(1); for (Direction direction : Direction.Plane.HORIZONTAL) { List list = this.level.getEntitiesOfClass(EndCrystal.class, new AABB(blockpos1.relative(direction, 2))); if (list.isEmpty()) { return; } list1.addAll(list); } LOGGER.debug("Found all crystals, respawning dragon."); this.respawnDragon(list1); } } private void respawnDragon(List p_64092_) { if (this.dragonKilled && this.respawnStage == null) { for (BlockPattern.BlockPatternMatch blockpattern$blockpatternmatch = this.findExitPortal(); blockpattern$blockpatternmatch != null; blockpattern$blockpatternmatch = this.findExitPortal() ) { for (int i = 0; i < this.exitPortalPattern.getWidth(); i++) { for (int j = 0; j < this.exitPortalPattern.getHeight(); j++) { for (int k = 0; k < this.exitPortalPattern.getDepth(); k++) { BlockInWorld blockinworld = blockpattern$blockpatternmatch.getBlock(i, j, k); if (blockinworld.getState().is(Blocks.BEDROCK) || blockinworld.getState().is(Blocks.END_PORTAL)) { this.level.setBlockAndUpdate(blockinworld.getPos(), Blocks.END_STONE.defaultBlockState()); } } } } } this.respawnStage = DragonRespawnAnimation.START; this.respawnTime = 0; this.spawnExitPortal(false); this.respawnCrystals = p_64092_; } } public void resetSpikeCrystals() { for (SpikeFeature.EndSpike spikefeature$endspike : SpikeFeature.getSpikesForLevel(this.level)) { for (EndCrystal endcrystal : this.level.getEntitiesOfClass(EndCrystal.class, spikefeature$endspike.getTopBoundingBox())) { endcrystal.setInvulnerable(false); endcrystal.setBeamTarget(null); } } } @Nullable public UUID getDragonUUID() { return this.dragonUUID; } public record Data( boolean needsStateScanning, boolean dragonKilled, boolean previouslyKilled, boolean isRespawning, Optional dragonUUID, Optional exitPortalLocation, Optional> gateways ) { public static final Codec CODEC = RecordCodecBuilder.create( p_289803_ -> p_289803_.group( Codec.BOOL.fieldOf("NeedsStateScanning").orElse(true).forGetter(EndDragonFight.Data::needsStateScanning), Codec.BOOL.fieldOf("DragonKilled").orElse(false).forGetter(EndDragonFight.Data::dragonKilled), Codec.BOOL.fieldOf("PreviouslyKilled").orElse(false).forGetter(EndDragonFight.Data::previouslyKilled), Codec.BOOL.lenientOptionalFieldOf("IsRespawning", false).forGetter(EndDragonFight.Data::isRespawning), UUIDUtil.CODEC.lenientOptionalFieldOf("Dragon").forGetter(EndDragonFight.Data::dragonUUID), BlockPos.CODEC.lenientOptionalFieldOf("ExitPortalLocation").forGetter(EndDragonFight.Data::exitPortalLocation), Codec.list(Codec.INT).lenientOptionalFieldOf("Gateways").forGetter(EndDragonFight.Data::gateways) ) .apply(p_289803_, EndDragonFight.Data::new) ); public static final EndDragonFight.Data DEFAULT = new EndDragonFight.Data( true, false, false, false, Optional.empty(), Optional.empty(), Optional.empty() ); } }