package net.minestom.server.instance; import it.unimi.dsi.fastutil.objects.ObjectArraySet; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.pointer.Pointers; import net.minestom.server.MinecraftServer; import net.minestom.server.ServerProcess; import net.minestom.server.Tickable; import net.minestom.server.adventure.audience.PacketGroupingAudience; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Entity; import net.minestom.server.entity.EntityCreature; import net.minestom.server.entity.ExperienceOrb; import net.minestom.server.entity.Player; import net.minestom.server.entity.pathfinding.PFInstanceSpace; import net.minestom.server.event.EventDispatcher; import net.minestom.server.event.EventFilter; import net.minestom.server.event.EventHandler; import net.minestom.server.event.EventNode; import net.minestom.server.event.instance.InstanceTickEvent; import net.minestom.server.event.trait.InstanceEvent; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.BlockFace; import net.minestom.server.instance.block.BlockHandler; import net.minestom.server.instance.generator.Generator; import net.minestom.server.instance.light.Light; import net.minestom.server.network.packet.server.play.BlockActionPacket; import net.minestom.server.network.packet.server.play.TimeUpdatePacket; import net.minestom.server.snapshot.*; import net.minestom.server.tag.TagHandler; import net.minestom.server.tag.Taggable; import net.minestom.server.thread.ThreadDispatcher; import net.minestom.server.timer.Schedulable; import net.minestom.server.timer.Scheduler; import net.minestom.server.utils.ArrayUtils; import net.minestom.server.utils.NamespaceID; import net.minestom.server.utils.PacketUtils; import net.minestom.server.utils.chunk.ChunkCache; import net.minestom.server.utils.chunk.ChunkSupplier; import net.minestom.server.utils.chunk.ChunkUtils; import net.minestom.server.utils.time.Cooldown; import net.minestom.server.utils.time.TimeUnit; import net.minestom.server.utils.validate.Check; import net.minestom.server.world.DimensionType; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jglrxavpok.hephaistos.nbt.NBTCompound; import java.time.Duration; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.stream.Collectors; /** * Instances are what are called "worlds" in Minecraft, you can add an entity in it using {@link Entity#setInstance(Instance)}. *
* An instance has entities and chunks, each instance contains its own entity list but the * chunk implementation has to be defined, see {@link InstanceContainer}. *
* WARNING: when making your own implementation registering the instance manually is required
* with {@link InstanceManager#registerInstance(Instance)}, and
* you need to be sure to signal the {@link ThreadDispatcher} of every partition/element changes.
*/
public abstract class Instance implements Block.Getter, Block.Setter,
Tickable, Schedulable, Snapshotable, EventHandler
* WARNING: during unloading, all entities other than {@link Player} will be removed.
*
* @param chunk the chunk to unload
*/
public abstract void unloadChunk(@NotNull Chunk chunk);
/**
* Unloads the chunk at the given position.
*
* @param chunkX the chunk X
* @param chunkZ the chunk Z
*/
public void unloadChunk(int chunkX, int chunkZ) {
final Chunk chunk = getChunk(chunkX, chunkZ);
Check.notNull(chunk, "The chunk at {0}:{1} is already unloaded", chunkX, chunkZ);
unloadChunk(chunk);
}
/**
* Gets the loaded {@link Chunk} at a position.
*
* WARNING: this should only return already-loaded chunk, use {@link #loadChunk(int, int)} or similar to load one instead.
*
* @param chunkX the chunk X
* @param chunkZ the chunk Z
* @return the chunk at the specified position, null if not loaded
*/
public abstract @Nullable Chunk getChunk(int chunkX, int chunkZ);
/**
* @param chunkX the chunk X
* @param chunkZ this chunk Z
* @return true if the chunk is loaded
*/
public boolean isChunkLoaded(int chunkX, int chunkZ) {
return getChunk(chunkX, chunkZ) != null;
}
/**
* @param point coordinate of a block or other
* @return true if the chunk is loaded
*/
public boolean isChunkLoaded(Point point) {
return isChunkLoaded(point.chunkX(), point.chunkZ());
}
/**
* Saves the current instance tags.
*
* Warning: only the global instance data will be saved, not chunks.
* You would need to call {@link #saveChunksToStorage()} too.
*
* @return the future called once the instance data has been saved
*/
@ApiStatus.Experimental
public abstract @NotNull CompletableFuture
* WARNING: should only be used by {@link InstanceManager}.
*
* @param registered true to mark the instance as registered
*/
protected void setRegistered(boolean registered) {
this.registered = registered;
}
/**
* Gets the instance {@link DimensionType}.
*
* @return the dimension of the instance
*/
public DimensionType getDimensionType() {
return dimensionType;
}
/**
* Gets the instance dimension name.
* @return the dimension name of the instance
*/
public @NotNull String getDimensionName() {
return dimensionName;
}
/**
* Gets the age of this instance in tick.
*
* @return the age of this instance in tick
*/
public long getWorldAge() {
return worldAge;
}
/**
* Gets the current time in the instance (sun/moon).
*
* @return the time in the instance
*/
public long getTime() {
return time;
}
/**
* Changes the current time in the instance, from 0 to 24000.
*
* If the time is negative, the vanilla client will not move the sun.
*
* 0 = sunrise
* 6000 = noon
* 12000 = sunset
* 18000 = midnight
*
* This method is unaffected by {@link #getTimeRate()}
*
* It does send the new time to all players in the instance, unaffected by {@link #getTimeUpdate()}
*
* @param time the new time of the instance
*/
public void setTime(long time) {
this.time = time;
PacketUtils.sendGroupedPacket(getPlayers(), createTimePacket());
}
/**
* Gets the rate of the time passing, it is 1 by default
*
* @return the time rate of the instance
*/
public int getTimeRate() {
return timeRate;
}
/**
* Changes the time rate of the instance
*
* 1 is the default value and can be set to 0 to be completely disabled (constant time)
*
* @param timeRate the new time rate of the instance
* @throws IllegalStateException if {@code timeRate} is lower than 0
*/
public void setTimeRate(int timeRate) {
Check.stateCondition(timeRate < 0, "The time rate cannot be lower than 0");
this.timeRate = timeRate;
}
/**
* Gets the rate at which the client is updated with the current instance time
*
* @return the client update rate for time related packet
*/
public @Nullable Duration getTimeUpdate() {
return timeUpdate;
}
/**
* Changes the rate at which the client is updated about the time
*
* Setting it to null means that the client will never know about time change
* (but will still change server-side)
*
* @param timeUpdate the new update rate concerning time
*/
public void setTimeUpdate(@Nullable Duration timeUpdate) {
this.timeUpdate = timeUpdate;
}
/**
* Creates a {@link TimeUpdatePacket} with the current age and time of this instance
*
* @return the {@link TimeUpdatePacket} with this instance data
*/
@ApiStatus.Internal
public @NotNull TimeUpdatePacket createTimePacket() {
long time = this.time;
if (timeRate == 0) {
//Negative values stop the sun and moon from moving
//0 as a long cannot be negative
time = time == 0 ? -24000L : -Math.abs(time);
}
return new TimeUpdatePacket(worldAge, time);
}
/**
* Gets the instance {@link WorldBorder};
*
* @return the {@link WorldBorder} linked to the instance
*/
public @NotNull WorldBorder getWorldBorder() {
return worldBorder;
}
/**
* Gets the entities in the instance;
*
* @return an unmodifiable {@link Set} containing all the entities in the instance
*/
public @NotNull Set<@NotNull Entity> getEntities() {
return entityTracker.entities();
}
/**
* Gets the players in the instance;
*
* @return an unmodifiable {@link Set} containing all the players in the instance
*/
@Override
public @NotNull Set<@NotNull Player> getPlayers() {
return entityTracker.entities(EntityTracker.Target.PLAYERS);
}
/**
* Gets the creatures in the instance;
*
* @return an unmodifiable {@link Set} containing all the creatures in the instance
*/
@Deprecated
public @NotNull Set<@NotNull EntityCreature> getCreatures() {
return entityTracker.entities().stream()
.filter(EntityCreature.class::isInstance)
.map(entity -> (EntityCreature) entity)
.collect(Collectors.toUnmodifiableSet());
}
/**
* Gets the experience orbs in the instance.
*
* @return an unmodifiable {@link Set} containing all the experience orbs in the instance
*/
@Deprecated
public @NotNull Set<@NotNull ExperienceOrb> getExperienceOrbs() {
return entityTracker.entities().stream()
.filter(ExperienceOrb.class::isInstance)
.map(entity -> (ExperienceOrb) entity)
.collect(Collectors.toUnmodifiableSet());
}
/**
* Gets the entities located in the chunk.
*
* @param chunk the chunk to get the entities from
* @return an unmodifiable {@link Set} containing all the entities in a chunk,
* if {@code chunk} is unloaded, return an empty {@link HashSet}
*/
public @NotNull Set<@NotNull Entity> getChunkEntities(Chunk chunk) {
var chunkEntities = entityTracker.chunkEntities(chunk.toPosition(), EntityTracker.Target.ENTITIES);
return ObjectArraySet.ofUnchecked(chunkEntities.toArray(Entity[]::new));
}
/**
* Gets nearby entities to the given position.
*
* @param point position to look at
* @param range max range from the given point to collect entities at
* @return entities that are not further than the specified distance from the transmitted position.
*/
public @NotNull Collection
* Warning: this does not update chunks and entities.
*
* @param time the tick time in milliseconds
*/
@Override
public void tick(long time) {
// Scheduled tasks
this.scheduler.processTick();
// Time
{
this.worldAge++;
this.time += timeRate;
// time needs to be sent to players
if (timeUpdate != null && !Cooldown.hasCooldown(time, lastTimeUpdate, timeUpdate)) {
PacketUtils.sendGroupedPacket(getPlayers(), createTimePacket());
this.lastTimeUpdate = time;
}
}
// Weather
if (remainingRainTransitionTicks > 0 || remainingThunderTransitionTicks > 0) {
Weather previousWeather = transitioningWeather;
transitioningWeather = transitionWeather(remainingRainTransitionTicks, remainingThunderTransitionTicks);
sendWeatherPackets(previousWeather);
remainingRainTransitionTicks = Math.max(0, remainingRainTransitionTicks - 1);
remainingThunderTransitionTicks = Math.max(0, remainingThunderTransitionTicks - 1);
}
// Tick event
{
// Process tick events
EventDispatcher.call(new InstanceTickEvent(this, time, lastTickAge));
// Set last tick age
this.lastTickAge = time;
}
this.worldBorder.update();
}
/**
* Gets the weather of this instance
*
* @return the instance weather
*/
public @NotNull Weather getWeather() {
return weather;
}
/**
* Sets the weather on this instance, transitions over time
*
* @param weather the new weather
* @param transitionTicks the ticks to transition to new weather
*/
public void setWeather(@NotNull Weather weather, int transitionTicks) {
Check.stateCondition(transitionTicks < 1, "Transition ticks cannot be lower than 0");
this.weather = weather;
remainingRainTransitionTicks = transitionTicks;
remainingThunderTransitionTicks = transitionTicks;
}
/**
* Sets the weather of this instance with a fixed transition
*
* @param weather the new weather
*/
public void setWeather(@NotNull Weather weather) {
this.weather = weather;
remainingRainTransitionTicks = (int) Math.max(1, Math.abs((this.weather.rainLevel() - transitioningWeather.rainLevel()) / 0.01));
remainingThunderTransitionTicks = (int) Math.max(1, Math.abs((this.weather.thunderLevel() - transitioningWeather.thunderLevel()) / 0.01));
}
private void sendWeatherPackets(@NotNull Weather previousWeather) {
boolean toggledRain = (transitioningWeather.isRaining() != previousWeather.isRaining());
if (toggledRain) sendGroupedPacket(transitioningWeather.createIsRainingPacket());
if (transitioningWeather.rainLevel() != previousWeather.rainLevel()) sendGroupedPacket(transitioningWeather.createRainLevelPacket());
if (transitioningWeather.thunderLevel() != previousWeather.thunderLevel()) sendGroupedPacket(transitioningWeather.createThunderLevelPacket());
}
private @NotNull Weather transitionWeather(int remainingRainTransitionTicks, int remainingThunderTransitionTicks) {
Weather target = weather;
Weather current = transitioningWeather;
float rainLevel = current.rainLevel() + (target.rainLevel() - current.rainLevel()) * (1 / (float)Math.max(1, remainingRainTransitionTicks));
float thunderLevel = current.thunderLevel() + (target.thunderLevel() - current.thunderLevel()) * (1 / (float)Math.max(1, remainingThunderTransitionTicks));
return new Weather(rainLevel, thunderLevel);
}
@Override
public @NotNull TagHandler tagHandler() {
return tagHandler;
}
@Override
public @NotNull Scheduler scheduler() {
return scheduler;
}
@Override
@ApiStatus.Experimental
public @NotNull EventNode
* Used by the pathfinder for entities.
*
* @return the instance space
*/
@ApiStatus.Internal
public @NotNull PFInstanceSpace getInstanceSpace() {
return instanceSpace;
}
@Override
public @NotNull Pointers pointers() {
return this.pointers;
}
public int getBlockLight(int blockX, int blockY, int blockZ) {
var chunk = getChunkAt(blockX, blockZ);
if (chunk == null) return 0;
Section section = chunk.getSectionAt(blockY);
Light light = section.blockLight();
int sectionCoordinate = ChunkUtils.getChunkCoordinate(blockY);
int coordX = ChunkUtils.toSectionRelativeCoordinate(blockX);
int coordY = ChunkUtils.toSectionRelativeCoordinate(blockY);
int coordZ = ChunkUtils.toSectionRelativeCoordinate(blockZ);
if (light.requiresUpdate()) LightingChunk.relightSection(chunk.getInstance(), chunk.chunkX, sectionCoordinate, chunk.chunkZ);
return light.getLevel(coordX, coordY, coordZ);
}
public int getSkyLight(int blockX, int blockY, int blockZ) {
var chunk = getChunkAt(blockX, blockZ);
if (chunk == null) return 0;
Section section = chunk.getSectionAt(blockY);
Light light = section.skyLight();
int sectionCoordinate = ChunkUtils.getChunkCoordinate(blockY);
int coordX = ChunkUtils.toSectionRelativeCoordinate(blockX);
int coordY = ChunkUtils.toSectionRelativeCoordinate(blockY);
int coordZ = ChunkUtils.toSectionRelativeCoordinate(blockZ);
if (light.requiresUpdate()) LightingChunk.relightSection(chunk.getInstance(), chunk.chunkX, sectionCoordinate, chunk.chunkZ);
return light.getLevel(coordX, coordY, coordZ);
}
}