diff --git a/paper-api/src/main/java/io/papermc/paper/event/packet/PlayerBiomesLoadEvent.java b/paper-api/src/main/java/io/papermc/paper/event/packet/PlayerBiomesLoadEvent.java new file mode 100644 index 0000000000..96408581b2 --- /dev/null +++ b/paper-api/src/main/java/io/papermc/paper/event/packet/PlayerBiomesLoadEvent.java @@ -0,0 +1,78 @@ +package io.papermc.paper.event.packet; + +import org.bukkit.BiomesSnapshot; +import org.bukkit.Chunk; +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; +import org.bukkit.event.world.ChunkEvent; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NullMarked; + +/** + * Is called when a {@link Player} receives {@link org.bukkit.block.Biome}s + *

+ * Can for example be used for replacing Biomes when the player receives the Biome list. + *

+ * Should only be used for packet/clientside related stuff. + * Not intended for modifying server side state. + */ +@NullMarked +public class PlayerBiomesLoadEvent extends ChunkEvent { + + private static final HandlerList HANDLER_LIST = new HandlerList(); + + private final Player player; + private @Nullable BiomesSnapshot biomesSnapshot; + + @ApiStatus.Internal + public PlayerBiomesLoadEvent(final Player player, Chunk chunk) { + super(chunk); + this.player = player; + } + + @Override + public HandlerList getHandlers() { + return HANDLER_LIST; + } + + public static HandlerList getHandlerList() { + return HANDLER_LIST; + } + + /** + * Returns the player that is receiving the biomes + * + * @return the player + */ + public Player getPlayer() { + return player; + } + + /** + * Returns a biomes snapshot for the given chunk, by default it won't be set. + * + * @return biome snapshot if one is set + */ + public @Nullable BiomesSnapshot getBiomeSnapshot() { + return biomesSnapshot; + } + + /** + * Sets the biome snapshot for the given chunk that will be sent as an override to the player + * + * @param biomesSnapshot the biome override + */ + public void setBiomeSnapshot(@Nullable final BiomesSnapshot biomesSnapshot) { + this.biomesSnapshot = biomesSnapshot; + } + + /** + * Returns if chunk biomes were overridden + * + * @return true if override was made, else false + */ + public boolean hasOverrides() { + return biomesSnapshot != null; + } +} diff --git a/paper-api/src/main/java/io/papermc/paper/event/packet/PlayerChunkLoadEvent.java b/paper-api/src/main/java/io/papermc/paper/event/packet/PlayerChunkLoadEvent.java index 2815c5802e..26a361de3e 100644 --- a/paper-api/src/main/java/io/papermc/paper/event/packet/PlayerChunkLoadEvent.java +++ b/paper-api/src/main/java/io/papermc/paper/event/packet/PlayerChunkLoadEvent.java @@ -16,7 +16,7 @@ import org.jspecify.annotations.NullMarked; * Not intended for modifying server side state. */ @NullMarked -public class PlayerChunkLoadEvent extends ChunkEvent { +public class PlayerChunkLoadEvent extends PlayerBiomesLoadEvent { private static final HandlerList HANDLER_LIST = new HandlerList(); @@ -24,7 +24,7 @@ public class PlayerChunkLoadEvent extends ChunkEvent { @ApiStatus.Internal public PlayerChunkLoadEvent(final Chunk chunk, final Player player) { - super(chunk); + super(player, chunk); this.player = player; } diff --git a/paper-api/src/main/java/org/bukkit/BiomesSnapshot.java b/paper-api/src/main/java/org/bukkit/BiomesSnapshot.java new file mode 100644 index 0000000000..8fff8be090 --- /dev/null +++ b/paper-api/src/main/java/org/bukkit/BiomesSnapshot.java @@ -0,0 +1,73 @@ +package org.bukkit; + +import org.bukkit.block.Biome; +import org.jspecify.annotations.NullMarked; + +/** + * Represents a static, thread-safe snapshot of chunk of biomes. + *

+ * Purpose is to allow clean, efficient copy of a biome data to be made, and + * then handed off for processing in another thread + */ +@NullMarked +public interface BiomesSnapshot { + + /** + * Gets the X-coordinate of this chunk + * + * @return X-coordinate + */ + int getX(); + + /** + * Gets the Z-coordinate of this chunk + * + * @return Z-coordinate + */ + int getZ(); + + /** + * Gets name of the world containing this chunk + * + * @return Parent World Name + */ + String getWorldName(); + + /** + * Get biome at given coordinates + * + * @param x X-coordinate (0-15) + * @param y Y-coordinate (world minHeight (inclusive) - world maxHeight (exclusive)) + * @param z Z-coordinate (0-15) + * @return Biome at given coordinate + */ + Biome getBiome(int x, int y, int z); + + /** + * Get biome at given coordinates + * + * @param x X-coordinate (0-15) + * @param y Y-coordinate (world minHeight (inclusive) - world maxHeight (exclusive)) + * @param z Z-coordinate (0-15) + * @param biome the biome to set at the give coordinate + */ + void setBiome(int x, int y, int z, Biome biome); + + /** + * Get raw biome temperature at given coordinates + * + * @param x X-coordinate (0-15) + * @param y Y-coordinate (world minHeight (inclusive) - world maxHeight (exclusive)) + * @param z Z-coordinate (0-15) + * @return temperature at given coordinate + */ + double getRawBiomeTemperature(int x, int y, int z); + + /** + * Tests if this chunk contains the specified biome. + * + * @param biome biome to test + * @return if the biome is contained within + */ + boolean contains(Biome biome); +} diff --git a/paper-api/src/main/java/org/bukkit/ChunkSnapshot.java b/paper-api/src/main/java/org/bukkit/ChunkSnapshot.java index 725d7944ce..edcdb21219 100644 --- a/paper-api/src/main/java/org/bukkit/ChunkSnapshot.java +++ b/paper-api/src/main/java/org/bukkit/ChunkSnapshot.java @@ -10,29 +10,7 @@ import org.jetbrains.annotations.NotNull; * Purpose is to allow clean, efficient copy of a chunk data to be made, and * then handed off for processing in another thread (e.g. map rendering) */ -public interface ChunkSnapshot { - - /** - * Gets the X-coordinate of this chunk - * - * @return X-coordinate - */ - int getX(); - - /** - * Gets the Z-coordinate of this chunk - * - * @return Z-coordinate - */ - int getZ(); - - /** - * Gets name of the world containing this chunk - * - * @return Parent World Name - */ - @NotNull - String getWorldName(); +public interface ChunkSnapshot extends BiomesSnapshot { /** * Get block type for block at corresponding coordinate in the chunk @@ -110,17 +88,6 @@ public interface ChunkSnapshot { @Deprecated(since = "1.15") Biome getBiome(int x, int z); - /** - * Get biome at given coordinates - * - * @param x X-coordinate (0-15) - * @param y Y-coordinate (world minHeight (inclusive) - world maxHeight (exclusive)) - * @param z Z-coordinate (0-15) - * @return Biome at given coordinate - */ - @NotNull - Biome getBiome(int x, int y, int z); - /** * Get raw biome temperature at given coordinates * @@ -132,16 +99,6 @@ public interface ChunkSnapshot { @Deprecated(since = "1.15") double getRawBiomeTemperature(int x, int z); - /** - * Get raw biome temperature at given coordinates - * - * @param x X-coordinate (0-15) - * @param y Y-coordinate (world minHeight (inclusive) - world maxHeight (exclusive)) - * @param z Z-coordinate (0-15) - * @return temperature at given coordinate - */ - double getRawBiomeTemperature(int x, int y, int z); - /** * Get world full time when chunk snapshot was captured * @@ -164,12 +121,4 @@ public interface ChunkSnapshot { * @return if the block is contained within */ boolean contains(@NotNull BlockData block); - - /** - * Tests if this chunk contains the specified biome. - * - * @param biome biome to test - * @return if the biome is contained within - */ - boolean contains(@NotNull Biome biome); } diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftBiomesSnapshot.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftBiomesSnapshot.java new file mode 100644 index 0000000000..b33c682472 --- /dev/null +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftBiomesSnapshot.java @@ -0,0 +1,105 @@ +package org.bukkit.craftbukkit; + +import com.google.common.base.Preconditions; +import com.google.common.base.Predicates; +import java.util.Objects; +import java.util.function.Predicate; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.world.level.chunk.PalettedContainer; +import net.minecraft.world.level.chunk.PalettedContainerRO; +import org.bukkit.BiomesSnapshot; +import org.bukkit.block.Biome; +import org.bukkit.craftbukkit.block.CraftBiome; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public class CraftBiomesSnapshot implements BiomesSnapshot { + + private final int x, z; + private final String worldname; + private final int minHeight, maxHeight, seaLevel; + private final Registry biomeRegistry; + private final PalettedContainer>[] biome; + + public CraftBiomesSnapshot(final int x, final int z, final String worldname, final int minHeight, final int maxHeight, int seaLevel, final Registry registry, final PalettedContainer>[] biome) { + this.x = x; + this.z = z; + this.worldname = worldname; + this.minHeight = minHeight; + this.maxHeight = maxHeight; + this.biomeRegistry = registry; + this.biome = biome; + this.seaLevel = seaLevel; + } + + @Override + public int getX() { + return this.x; + } + + @Override + public int getZ() { + return this.z; + } + + @Override + public String getWorldName() { + return this.worldname; + } + + @Override + public void setBiome(final int x, final int y, final int z, final Biome biome) { + Preconditions.checkState(this.biome != null, "ChunkSnapshot created without biome. Please call getSnapshot with includeBiome=true"); + Objects.requireNonNull(biome, "biome cannot be null"); + this.validateChunkCoordinates(x, y, z); + PalettedContainer> biomeLocal = this.biome[this.getSectionIndex(y)]; + biomeLocal.set(x >> 2, (y & 0xF) >> 2, z >> 2, CraftBiome.bukkitToMinecraftHolder(biome)); + } + + @Override + public final double getRawBiomeTemperature(int x, int y, int z) { + Preconditions.checkState(this.biome != null, "ChunkSnapshot created without biome. Please call getSnapshot with includeBiome=true"); + this.validateChunkCoordinates(x, y, z); + + PalettedContainerRO> biome = this.biome[this.getSectionIndex(y)]; // SPIGOT-7188: Don't need to convert y to biome coordinate scale since it is bound to the block chunk section + return biome.get(x >> 2, (y & 0xF) >> 2, z >> 2).value().getTemperature(new BlockPos((this.x << 4) | x, y, (this.z << 4) | z), seaLevel); + } + + @Override + public final Biome getBiome(int x, int y, int z) { + Preconditions.checkState(this.biome != null, "ChunkSnapshot created without biome. Please call getSnapshot with includeBiome=true"); + this.validateChunkCoordinates(x, y, z); + + PalettedContainerRO> biome = this.biome[this.getSectionIndex(y)]; // SPIGOT-7188: Don't need to convert y to biome coordinate scale since it is bound to the block chunk section + return CraftBiome.minecraftHolderToBukkit(biome.get(x >> 2, (y & 0xF) >> 2, z >> 2)); + } + + + @Override + public boolean contains(Biome biome) { + Preconditions.checkArgument(biome != null, "Biome cannot be null"); + + Predicate> nms = Predicates.equalTo(CraftBiome.bukkitToMinecraftHolder(biome)); + for (PalettedContainerRO> palette : this.biome) { + if (palette.maybeHas(nms)) { + return true; + } + } + + return false; + } + + public PalettedContainer>[] getBiome(){ + return biome; + } + + protected void validateChunkCoordinates(int x, int y, int z) { + CraftChunk.validateChunkCoordinates(this.minHeight, this.maxHeight, x, y, z); + } + + protected int getSectionIndex(int y) { + return (y - this.minHeight) >> 4; + } +} diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftChunk.java index de8b9048c8..43239c713b 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftChunk.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftChunk.java @@ -303,7 +303,7 @@ public class CraftChunk implements Chunk { byte[][] sectionEmitLights = includeLightData ? new byte[cs.length][] : null; // Paper end - Add getChunkSnapshot includeLightData parameter boolean[] sectionEmpty = new boolean[cs.length]; - PalettedContainerRO>[] biome = (includeBiome || includeBiomeTempRain) ? new PalettedContainer[cs.length] : null; + PalettedContainer>[] biome = (includeBiome || includeBiomeTempRain) ? new PalettedContainer[cs.length] : null; Registry iregistry = this.worldServer.registryAccess().lookupOrThrow(Registries.BIOME); diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftChunkSnapshot.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftChunkSnapshot.java index ddcbdbcbfe..e85bcec5e8 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftChunkSnapshot.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftChunkSnapshot.java @@ -3,7 +3,6 @@ package org.bukkit.craftbukkit; import com.google.common.base.Preconditions; import com.google.common.base.Predicates; import java.util.function.Predicate; -import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; import net.minecraft.core.Registry; import net.minecraft.world.level.block.state.BlockState; @@ -14,8 +13,6 @@ import org.bukkit.ChunkSnapshot; import org.bukkit.Material; import org.bukkit.block.Biome; import org.bukkit.block.data.BlockData; -import org.bukkit.craftbukkit.block.CraftBiome; -import org.bukkit.craftbukkit.block.CraftBlockType; import org.bukkit.craftbukkit.block.data.CraftBlockData; import org.bukkit.craftbukkit.util.CraftMagicNumbers; @@ -23,49 +20,22 @@ import org.bukkit.craftbukkit.util.CraftMagicNumbers; * Represents a static, thread-safe snapshot of chunk of blocks * Purpose is to allow clean, efficient copy of a chunk data to be made, and then handed off for processing in another thread (e.g. map rendering) */ -public class CraftChunkSnapshot implements ChunkSnapshot { - private final int x, z; - private final int minHeight, maxHeight, seaLevel; - private final String worldname; +public class CraftChunkSnapshot extends CraftBiomesSnapshot implements ChunkSnapshot { private final PalettedContainer[] blockids; private final byte[][] skylight; private final byte[][] emitlight; private final boolean[] empty; private final Heightmap hmap; // Height map private final long captureFulltime; - private final Registry biomeRegistry; - private final PalettedContainerRO>[] biome; - CraftChunkSnapshot(int x, int z, int minHeight, int maxHeight, int seaLevel, String wname, long wtime, PalettedContainer[] sectionBlockIDs, byte[][] sectionSkyLights, byte[][] sectionEmitLights, boolean[] sectionEmpty, Heightmap hmap, Registry biomeRegistry, PalettedContainerRO>[] biome) { - this.x = x; - this.z = z; - this.minHeight = minHeight; - this.maxHeight = maxHeight; - this.seaLevel = seaLevel; - this.worldname = wname; + CraftChunkSnapshot(int x, int z, int minHeight, int maxHeight, int seaLevel, String wname, long wtime, PalettedContainer[] sectionBlockIDs, byte[][] sectionSkyLights, byte[][] sectionEmitLights, boolean[] sectionEmpty, Heightmap hmap, Registry biomeRegistry, PalettedContainer>[] biome) { + super(x, z, wname, minHeight, maxHeight, seaLevel, biomeRegistry, biome); this.captureFulltime = wtime; this.blockids = sectionBlockIDs; this.skylight = sectionSkyLights; this.emitlight = sectionEmitLights; this.empty = sectionEmpty; this.hmap = hmap; - this.biomeRegistry = biomeRegistry; - this.biome = biome; - } - - @Override - public int getX() { - return this.x; - } - - @Override - public int getZ() { - return this.z; - } - - @Override - public String getWorldName() { - return this.worldname; } @Override @@ -82,20 +52,6 @@ public class CraftChunkSnapshot implements ChunkSnapshot { return false; } - @Override - public boolean contains(Biome biome) { - Preconditions.checkArgument(biome != null, "Biome cannot be null"); - - Predicate> nms = Predicates.equalTo(CraftBiome.bukkitToMinecraftHolder(biome)); - for (PalettedContainerRO> palette : this.biome) { - if (palette.maybeHas(nms)) { - return true; - } - } - - return false; - } - @Override public Material getBlockType(int x, int y, int z) { this.validateChunkCoordinates(x, y, z); @@ -148,29 +104,11 @@ public class CraftChunkSnapshot implements ChunkSnapshot { return this.getBiome(x, 0, z); } - @Override - public final Biome getBiome(int x, int y, int z) { - Preconditions.checkState(this.biome != null, "ChunkSnapshot created without biome. Please call getSnapshot with includeBiome=true"); - this.validateChunkCoordinates(x, y, z); - - PalettedContainerRO> biome = this.biome[this.getSectionIndex(y)]; // SPIGOT-7188: Don't need to convert y to biome coordinate scale since it is bound to the block chunk section - return CraftBiome.minecraftHolderToBukkit(biome.get(x >> 2, (y & 0xF) >> 2, z >> 2)); - } - @Override public final double getRawBiomeTemperature(int x, int z) { return this.getRawBiomeTemperature(x, 0, z); } - @Override - public final double getRawBiomeTemperature(int x, int y, int z) { - Preconditions.checkState(this.biome != null, "ChunkSnapshot created without biome. Please call getSnapshot with includeBiome=true"); - this.validateChunkCoordinates(x, y, z); - - PalettedContainerRO> biome = this.biome[this.getSectionIndex(y)]; // SPIGOT-7188: Don't need to convert y to biome coordinate scale since it is bound to the block chunk section - return biome.get(x >> 2, (y & 0xF) >> 2, z >> 2).value().getTemperature(new BlockPos((this.x << 4) | x, y, (this.z << 4) | z), this.seaLevel); - } - @Override public final long getCaptureFullTime() { return this.captureFulltime; @@ -180,12 +118,4 @@ public class CraftChunkSnapshot implements ChunkSnapshot { public final boolean isSectionEmpty(int sy) { return this.empty[sy]; } - - private int getSectionIndex(int y) { - return (y - this.minHeight) >> 4; - } - - private void validateChunkCoordinates(int x, int y, int z) { - CraftChunk.validateChunkCoordinates(this.minHeight, this.maxHeight, x, y, z); - } }