SPIGOT-5880, SPIGOT-5567: New ChunkGenerator API

## **Current API**
The current world generation API is very old and limited when you want to make more complex world generation. Resulting in some hard to fix bugs such as that you cannot modify blocks outside the chunk in the BlockPopulator (which should and was per the docs possible), or strange behavior such as SPIGOT-5880.

## **New API**
With the new API, the generation is more separate in multiple methods and is more in line with Vanilla chunk generation. The new API is designed to as future proof as possible. If for example a new generation step is added it can easily also be added as a step in API by simply creating the method for it. On the other side if a generation step gets removed, the method can easily be called after another, which is the case with surface and bedrock. The new API and changes are also fully backwards compatible with old chunk generators.

### **Changes in the new api**
**Extra generation steps:**
Noise, surface, bedrock and caves are added as steps. With those generation steps three extra methods for Vanilla generation are also added. Those new methods provide the ChunkData instead of returning one. The reason for this is, that the ChunkData is now backed by a ChunkAccess. With this, each step has the information of the step before and the Vanilla information (if chosen by setting a 'should' method to true). The old method is deprecated.

**New class BiomeProvider**
The BiomeProvider acts as Biome source and wrapper for the NMS class WorldChunkManager. With this the underlying Vanilla ChunkGeneration knows which Biome to use for the structure and decoration generation. (Fixes: SPIGOT-5880). Although the List of Biomes which is required in BiomeProvider, is currently not much in use in Vanilla, I decided to add it to future proof the API when it may be required in later versions of Minecraft.
The BiomeProvider is also separated from the ChunkGenerator for plugins which only want to change the biome map, such as single Biome worlds or if some biomes should be more present than others.

**Deprecated isParallelCapable**
Mojang has and is pushing to a more multi threaded chunk generation. This should also be the case for custom chunk generators. This is why the new API only supports multi threaded generation. This does not affect the old API, which is still checking this.

**Base height method added**
This method was added to also bring the Minecraft generator and Bukkit generator more in line. With this it is possible to return the max height of a location (before decorations). This is useful to let most structures know were to place them. This fixes SPIGOT-5567. (This fixes not all structures placement, desert pyramids for example are still way up at y-level 64, This however is more a vanilla bug and should be fixed at Mojangs end).

**WorldInfo Class**
The World object was swapped for a WorldInfo object. This is because many methods of the World object won't work during world generation and would mostly likely result in a deadlock. It contains any information a plugin should need to identify the world.

**BlockPopulator Changes**
Instead of directly manipulating a chunk, changes are now made to a new class LimitedRegion, this class provides methods to populated the chunk and its surrounding area. The wrapping is done so that the population can be moved into the place where Minecraft generates decorations. Where there is no chunk to access yet. By moving it into this place the generation is now async and the surrounding area of the chunk can also be used.

For common methods between the World and LimitedRegion a RegionAccessor was added.

By: DerFrZocker <derrieple@gmail.com>
This commit is contained in:
CraftBukkit/Spigot
2021-08-15 08:08:16 +10:00
parent fdefaeeccd
commit c2e4e91b1b
41 changed files with 2034 additions and 949 deletions

View File

@@ -20,26 +20,20 @@ public class CraftCreeper extends CraftMonster implements Creeper {
@Override
public void setPowered(boolean powered) {
CraftServer server = this.server;
Creeper entity = (Creeper) this.getHandle().getBukkitEntity();
CreeperPowerEvent.PowerCause cause = powered ? CreeperPowerEvent.PowerCause.SET_ON : CreeperPowerEvent.PowerCause.SET_OFF;
if (powered) {
CreeperPowerEvent event = new CreeperPowerEvent(entity, CreeperPowerEvent.PowerCause.SET_ON);
server.getPluginManager().callEvent(event);
if (!event.isCancelled()) {
getHandle().setPowered(true);
}
} else {
CreeperPowerEvent event = new CreeperPowerEvent(entity, CreeperPowerEvent.PowerCause.SET_OFF);
server.getPluginManager().callEvent(event);
if (!event.isCancelled()) {
getHandle().setPowered(false);
}
// only call event when we are not in world generation
if (getHandle().generation || !callPowerEvent(cause)) {
getHandle().setPowered(powered);
}
}
private boolean callPowerEvent(CreeperPowerEvent.PowerCause cause) {
CreeperPowerEvent event = new CreeperPowerEvent((Creeper) getHandle().getBukkitEntity(), cause);
server.getPluginManager().callEvent(event);
return event.isCancelled();
}
@Override
public void setMaxFuseTicks(int ticks) {
Preconditions.checkArgument(ticks >= 0, "ticks < 0");

View File

@@ -506,6 +506,8 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
// Let the server handle cross world teleports
if (!location.getWorld().equals(getWorld())) {
// Prevent teleportation to an other world during world generation
Preconditions.checkState(!entity.generation, "Cannot teleport entity to an other world during world generation");
entity.teleportTo(((CraftWorld) location.getWorld()).getHandle(), new BlockPosition(location.getX(), location.getY(), location.getZ()));
return true;
}
@@ -530,6 +532,8 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
@Override
public List<org.bukkit.entity.Entity> getNearbyEntities(double x, double y, double z) {
Preconditions.checkState(!entity.generation, "Cannot get nearby entities during world generation");
List<Entity> notchEntityList = entity.level.getEntities(entity, entity.getBoundingBox().grow(x, y, z), Predicates.alwaysTrue());
List<org.bukkit.entity.Entity> bukkitEntityList = new java.util.ArrayList<org.bukkit.entity.Entity>(notchEntityList.size());
@@ -730,6 +734,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
@Override
public void playEffect(EntityEffect type) {
Preconditions.checkArgument(type != null, "type");
Preconditions.checkState(!entity.generation, "Cannot play effect during world generation");
if (type.getApplicable().isInstance(this)) {
this.getHandle().level.broadcastEntityEffect(getHandle(), type.getData());

View File

@@ -42,7 +42,7 @@ public class CraftHanging extends CraftEntity implements Hanging {
getHandle().setDirection(EnumDirection.EAST);
break;
}
if (!force && !hanging.survives()) {
if (!force && !getHandle().generation && !hanging.survives()) {
// Revert since it doesn't fit
hanging.setDirection(dir);
return false;

View File

@@ -27,7 +27,7 @@ public class CraftItemFrame extends CraftHanging implements ItemFrame {
EnumDirection newDir = CraftBlock.blockFaceToNotch(face);
getHandle().setDirection(newDir);
if (!force && !hanging.survives()) {
if (!force && !getHandle().generation && !hanging.survives()) {
hanging.setDirection(oldDir);
return false;
}
@@ -47,7 +47,9 @@ public class CraftItemFrame extends CraftHanging implements ItemFrame {
}
// update redstone
getHandle().getWorld().updateAdjacentComparators(getHandle().pos, Blocks.AIR);
if (!getHandle().generation) {
getHandle().getWorld().updateAdjacentComparators(getHandle().pos, Blocks.AIR);
}
}
@Override
@@ -57,7 +59,8 @@ public class CraftItemFrame extends CraftHanging implements ItemFrame {
@Override
public void setItem(org.bukkit.inventory.ItemStack item, boolean playSound) {
getHandle().setItem(CraftItemStack.asNMSCopy(item), true, playSound);
// only updated redstone and play sound when it is not in generation
getHandle().setItem(CraftItemStack.asNMSCopy(item), !getHandle().generation, !getHandle().generation && playSound);
}
@Override

View File

@@ -114,6 +114,12 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity {
throw new IllegalArgumentException("Health must be between 0 and " + getMaxHealth() + "(" + health + ")");
}
// during world generation, we don't want to run logic for dropping items and xp
if (getHandle().generation && health == 0) {
getHandle().die();
return;
}
getHandle().setHealth((float) health);
if (health == 0) {
@@ -165,6 +171,8 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity {
}
private List<Block> getLineOfSight(Set<Material> transparent, int maxDistance, int maxLength) {
Preconditions.checkState(!getHandle().generation, "Cannot get line of sight during world generation");
if (transparent == null) {
transparent = Sets.newHashSet(Material.AIR, Material.CAVE_AIR, Material.VOID_AIR);
}
@@ -221,6 +229,8 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity {
@Override
public RayTraceResult rayTraceBlocks(double maxDistance, FluidCollisionMode fluidCollisionMode) {
Preconditions.checkState(!getHandle().generation, "Cannot ray tray blocks during world generation");
Location eyeLocation = this.getEyeLocation();
Vector direction = eyeLocation.getDirection();
return this.getWorld().rayTraceBlocks(eyeLocation, direction, maxDistance, fluidCollisionMode, false);
@@ -274,6 +284,8 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity {
@Override
public void damage(double amount, org.bukkit.entity.Entity source) {
Preconditions.checkState(!getHandle().generation, "Cannot damage entity during world generation");
DamageSource reason = DamageSource.GENERIC;
if (source instanceof HumanEntity) {
@@ -394,6 +406,8 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity {
@Override
@SuppressWarnings("unchecked")
public <T extends Projectile> T launchProjectile(Class<? extends T> projectile, Vector velocity) {
Preconditions.checkState(!getHandle().generation, "Cannot launch projectile during world generation");
net.minecraft.world.level.World world = ((CraftWorld) getWorld()).getHandle();
net.minecraft.world.entity.Entity launch = null;
@@ -486,6 +500,8 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity {
@Override
public boolean hasLineOfSight(Entity other) {
Preconditions.checkState(!getHandle().generation, "Cannot check line of sight during world generation");
return getHandle().hasLineOfSight(((CraftEntity) other).getHandle());
}
@@ -559,7 +575,7 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity {
@Override
public boolean setLeashHolder(Entity holder) {
if ((getHandle() instanceof EntityWither) || !(getHandle() instanceof EntityInsentient)) {
if (getHandle().generation || (getHandle() instanceof EntityWither) || !(getHandle() instanceof EntityInsentient)) {
return false;
}
@@ -608,6 +624,8 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity {
@Override
public boolean isClimbing() {
Preconditions.checkState(!getHandle().generation, "Cannot check if climbing during world generation");
return getHandle().isClimbing();
}
@@ -631,17 +649,22 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity {
@Override
public void attack(Entity target) {
Preconditions.checkArgument(target != null, "target == null");
Preconditions.checkState(!getHandle().generation, "Cannot attack during world generation");
getHandle().attackEntity(((CraftEntity) target).getHandle());
}
@Override
public void swingMainHand() {
Preconditions.checkState(!getHandle().generation, "Cannot swing hand during world generation");
getHandle().swingHand(EnumHand.MAIN_HAND, true);
}
@Override
public void swingOffHand() {
Preconditions.checkState(!getHandle().generation, "Cannot swing hand during world generation");
getHandle().swingHand(EnumHand.OFF_HAND, true);
}

View File

@@ -1,5 +1,6 @@
package org.bukkit.craftbukkit.entity;
import com.google.common.base.Preconditions;
import net.minecraft.world.entity.EntityInsentient;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
@@ -16,6 +17,8 @@ public abstract class CraftMob extends CraftLivingEntity implements Mob {
@Override
public void setTarget(LivingEntity target) {
Preconditions.checkState(!getHandle().generation, "Cannot set target during world generation");
EntityInsentient entity = getHandle();
if (target == null) {
entity.setGoalTarget(null, null, false);

View File

@@ -32,7 +32,7 @@ public class CraftPainting extends CraftHanging implements Painting {
Paintings oldArt = painting.motive;
painting.motive = CraftArt.BukkitToNotch(art);
painting.setDirection(painting.getDirection());
if (!force && !painting.survives()) {
if (!force && !getHandle().generation && !painting.survives()) {
// Revert painting since it doesn't fit
painting.motive = oldArt;
painting.setDirection(painting.getDirection());

View File

@@ -1,5 +1,6 @@
package org.bukkit.craftbukkit.entity;
import com.google.common.base.Preconditions;
import net.minecraft.world.entity.projectile.EntityShulkerBullet;
import org.bukkit.craftbukkit.CraftServer;
import org.bukkit.entity.Entity;
@@ -35,6 +36,8 @@ public class CraftShulkerBullet extends AbstractProjectile implements ShulkerBul
@Override
public void setTarget(org.bukkit.entity.Entity target) {
Preconditions.checkState(!getHandle().generation, "Cannot set target during world generation");
getHandle().setTarget(target == null ? null : ((CraftEntity) target).getHandle());
}

View File

@@ -89,6 +89,7 @@ public class CraftVillager extends CraftAbstractVillager implements Villager {
Preconditions.checkArgument(location != null, "Location cannot be null");
Preconditions.checkArgument(location.getWorld() != null, "Location needs to be in a world");
Preconditions.checkArgument(location.getWorld().equals(getWorld()), "Cannot sleep across worlds");
Preconditions.checkState(!getHandle().generation, "Cannot sleep during world generation");
BlockPosition position = new BlockPosition(location.getBlockX(), location.getBlockY(), location.getBlockZ());
IBlockData iblockdata = getHandle().level.getType(position);
@@ -103,6 +104,7 @@ public class CraftVillager extends CraftAbstractVillager implements Villager {
@Override
public void wakeup() {
Preconditions.checkState(isSleeping(), "Cannot wakeup if not sleeping");
Preconditions.checkState(!getHandle().generation, "Cannot wakeup during world generation");
getHandle().entityWakeup();
}