mirror of https://github.com/Minestom/Minestom.git
846 lines
33 KiB
Java
846 lines
33 KiB
Java
package net.minestom.server.instance;
|
|
|
|
import net.minestom.server.MinecraftServer;
|
|
import net.minestom.server.data.Data;
|
|
import net.minestom.server.data.SerializableData;
|
|
import net.minestom.server.entity.Player;
|
|
import net.minestom.server.event.instance.InstanceChunkLoadEvent;
|
|
import net.minestom.server.event.instance.InstanceChunkUnloadEvent;
|
|
import net.minestom.server.event.player.PlayerBlockBreakEvent;
|
|
import net.minestom.server.instance.batch.BlockBatch;
|
|
import net.minestom.server.instance.batch.ChunkBatch;
|
|
import net.minestom.server.instance.block.Block;
|
|
import net.minestom.server.instance.block.CustomBlock;
|
|
import net.minestom.server.instance.block.rule.BlockPlacementRule;
|
|
import net.minestom.server.network.packet.server.play.BlockChangePacket;
|
|
import net.minestom.server.network.packet.server.play.ParticlePacket;
|
|
import net.minestom.server.network.packet.server.play.UnloadChunkPacket;
|
|
import net.minestom.server.particle.Particle;
|
|
import net.minestom.server.particle.ParticleCreator;
|
|
import net.minestom.server.storage.StorageLocation;
|
|
import net.minestom.server.utils.BlockPosition;
|
|
import net.minestom.server.utils.Position;
|
|
import net.minestom.server.utils.block.CustomBlockUtils;
|
|
import net.minestom.server.utils.callback.OptionalCallback;
|
|
import net.minestom.server.utils.chunk.ChunkCallback;
|
|
import net.minestom.server.utils.chunk.ChunkSupplier;
|
|
import net.minestom.server.utils.chunk.ChunkUtils;
|
|
import net.minestom.server.utils.time.TimeUnit;
|
|
import net.minestom.server.utils.validate.Check;
|
|
import net.minestom.server.world.DimensionType;
|
|
import net.minestom.server.world.biomes.Biome;
|
|
import org.jetbrains.annotations.NotNull;
|
|
import org.jetbrains.annotations.Nullable;
|
|
|
|
import java.util.*;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import java.util.concurrent.CopyOnWriteArrayList;
|
|
import java.util.concurrent.locks.Lock;
|
|
import java.util.concurrent.locks.ReadWriteLock;
|
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
|
|
|
/**
|
|
* InstanceContainer is an instance that contains chunks in contrary to SharedInstance.
|
|
*/
|
|
public class InstanceContainer extends Instance {
|
|
|
|
private static final String UUID_KEY = "uuid";
|
|
private static final String DATA_KEY = "data";
|
|
|
|
// the storage location of this instance, can be null
|
|
private StorageLocation storageLocation;
|
|
|
|
// the shared instances assigned to this instance
|
|
private final List<SharedInstance> sharedInstances = new CopyOnWriteArrayList<>();
|
|
|
|
// the chunk generator used, can be null
|
|
private ChunkGenerator chunkGenerator;
|
|
// (chunk index -> chunk) map, contains all the chunks in the instance
|
|
private final ConcurrentHashMap<Long, Chunk> chunks = new ConcurrentHashMap<>();
|
|
// contains all the chunks to remove during the next instance tick
|
|
protected final Set<Chunk> scheduledChunksToRemove = new HashSet<>();
|
|
|
|
private final ReadWriteLock changingBlockLock = new ReentrantReadWriteLock();
|
|
private final Map<BlockPosition, Block> currentlyChangingBlocks = new HashMap<>();
|
|
|
|
// the chunk loader, used when trying to load/save a chunk from another source
|
|
private IChunkLoader chunkLoader;
|
|
|
|
// used to automatically enable the chunk loading or not
|
|
private boolean autoChunkLoad;
|
|
|
|
// used to supply a new chunk object at a position when requested
|
|
private ChunkSupplier chunkSupplier;
|
|
|
|
// Fields for instance copy
|
|
protected InstanceContainer srcInstance; // only present if this instance has been created using a copy
|
|
private long lastBlockChangeTime; // Time at which the last block change happened (#setBlock)
|
|
|
|
/**
|
|
* Creates an {@link InstanceContainer}.
|
|
*
|
|
* @param uniqueId the unique id of the instance
|
|
* @param dimensionType the dimension type of the instance
|
|
* @param storageLocation the {@link StorageLocation} of the instance,
|
|
* can be null if you do not wish to save the instance later on
|
|
*/
|
|
public InstanceContainer(@NotNull UUID uniqueId, @NotNull DimensionType dimensionType, @Nullable StorageLocation storageLocation) {
|
|
super(uniqueId, dimensionType);
|
|
|
|
this.storageLocation = storageLocation;
|
|
|
|
// Set the default chunk supplier using DynamicChunk
|
|
setChunkSupplier(DynamicChunk::new);
|
|
|
|
// Set the default chunk loader which use the instance's StorageLocation and ChunkSupplier to save and load chunks
|
|
setChunkLoader(new MinestomBasicChunkLoader(this));
|
|
|
|
// Get instance data from the saved data if a StorageLocation is defined
|
|
if (storageLocation != null) {
|
|
// Retrieve instance data
|
|
this.uniqueId = storageLocation.getOrDefault(UUID_KEY, UUID.class, uniqueId);
|
|
|
|
final Data data = storageLocation.getOrDefault(DATA_KEY, SerializableData.class, null);
|
|
setData(data);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void setBlockStateId(int x, int y, int z, short blockStateId, @Nullable Data data) {
|
|
setBlock(x, y, z, blockStateId, null, data);
|
|
}
|
|
|
|
@Override
|
|
public void setCustomBlock(int x, int y, int z, short customBlockId, @Nullable Data data) {
|
|
final CustomBlock customBlock = BLOCK_MANAGER.getCustomBlock(customBlockId);
|
|
Check.notNull(customBlock, "The custom block with the id " + customBlockId + " does not exist.");
|
|
setBlock(x, y, z, customBlock.getDefaultBlockStateId(), customBlock, data);
|
|
}
|
|
|
|
@Override
|
|
public void setSeparateBlocks(int x, int y, int z, short blockStateId, short customBlockId, @Nullable Data data) {
|
|
final CustomBlock customBlock = BLOCK_MANAGER.getCustomBlock(customBlockId);
|
|
setBlock(x, y, z, blockStateId, customBlock, data);
|
|
}
|
|
|
|
/**
|
|
* Set a block at the position
|
|
* <p>
|
|
* Verifies if the {@link Chunk} at the position is loaded, place the block if yes.
|
|
* Otherwise, check if {@link #hasEnabledAutoChunkLoad()} is true to load the chunk automatically and place the block.
|
|
*
|
|
* @param x the block X
|
|
* @param y the block Y
|
|
* @param z the block Z
|
|
* @param blockStateId the block state id
|
|
* @param customBlock the {@link CustomBlock}, null if none
|
|
* @param data the {@link Data}, null if none
|
|
*/
|
|
private synchronized void setBlock(int x, int y, int z, short blockStateId,
|
|
@Nullable CustomBlock customBlock, @Nullable Data data) {
|
|
final Chunk chunk = getChunkAt(x, z);
|
|
if (ChunkUtils.isLoaded(chunk)) {
|
|
UNSAFE_setBlock(chunk, x, y, z, blockStateId, customBlock, data);
|
|
} else {
|
|
Check.stateCondition(!hasEnabledAutoChunkLoad(),
|
|
"Tried to set a block to an unloaded chunk with auto chunk load disabled");
|
|
final int chunkX = ChunkUtils.getChunkCoordinate(x);
|
|
final int chunkZ = ChunkUtils.getChunkCoordinate(z);
|
|
loadChunk(chunkX, chunkZ, c -> UNSAFE_setBlock(c, x, y, z, blockStateId, customBlock, data));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set a block at the position
|
|
* <p>
|
|
* Unsafe because the method is not synchronized and it does not verify if the chunk is loaded or not
|
|
*
|
|
* @param chunk the {@link Chunk} which should be loaded
|
|
* @param x the block X
|
|
* @param y the block Y
|
|
* @param z the block Z
|
|
* @param blockStateId the block state id
|
|
* @param customBlock the {@link CustomBlock}, null if none
|
|
* @param data the {@link Data}, null if none
|
|
*/
|
|
private void UNSAFE_setBlock(@NotNull Chunk chunk, int x, int y, int z, short blockStateId,
|
|
@Nullable CustomBlock customBlock, @Nullable Data data) {
|
|
|
|
// Cannot place block in a read-only chunk
|
|
if (chunk.isReadOnly()) {
|
|
return;
|
|
}
|
|
|
|
synchronized (chunk) {
|
|
|
|
// Refresh the last block change time
|
|
this.lastBlockChangeTime = System.currentTimeMillis();
|
|
|
|
final boolean isCustomBlock = customBlock != null;
|
|
|
|
final BlockPosition blockPosition = new BlockPosition(x, y, z);
|
|
|
|
if (isAlreadyChanged(blockPosition, blockStateId)) { // do NOT change the block again.
|
|
// Avoids StackOverflowExceptions when onDestroy tries to destroy the block itself
|
|
// This can happen with nether portals which break the entire frame when a portal block is broken
|
|
return;
|
|
}
|
|
setAlreadyChanged(blockPosition, blockStateId);
|
|
final int index = ChunkUtils.getBlockIndex(x, y, z);
|
|
|
|
final CustomBlock previousBlock = chunk.getCustomBlock(index);
|
|
if (previousBlock != null) {
|
|
// Previous block was a custom block
|
|
|
|
// Call the destroy listener
|
|
callBlockDestroy(chunk, index, previousBlock, blockPosition);
|
|
|
|
// Remove digging information for the previous custom block
|
|
previousBlock.removeDiggingInformation(this, blockPosition);
|
|
}
|
|
|
|
// Change id based on neighbors
|
|
blockStateId = executeBlockPlacementRule(blockStateId, blockPosition);
|
|
|
|
// Retrieve custom block values
|
|
short customBlockId = 0;
|
|
boolean hasUpdate = false;
|
|
if (isCustomBlock) {
|
|
customBlockId = customBlock.getCustomBlockId();
|
|
data = customBlock.createData(this, blockPosition, data);
|
|
hasUpdate = CustomBlockUtils.hasUpdate(customBlock);
|
|
}
|
|
|
|
// Set the block
|
|
chunk.UNSAFE_setBlock(x, y, z, blockStateId, customBlockId, data, hasUpdate);
|
|
|
|
// Refresh neighbors since a new block has been placed
|
|
executeNeighboursBlockPlacementRule(blockPosition);
|
|
|
|
// Refresh player chunk block
|
|
sendBlockChange(chunk, blockPosition, blockStateId);
|
|
|
|
// Call the place listener for custom block
|
|
if (isCustomBlock)
|
|
callBlockPlace(chunk, index, blockPosition);
|
|
}
|
|
}
|
|
|
|
private void setAlreadyChanged(@NotNull BlockPosition blockPosition, short blockStateId) {
|
|
currentlyChangingBlocks.put(blockPosition, Block.fromStateId(blockStateId));
|
|
}
|
|
|
|
/**
|
|
* Has this block already changed since last update?
|
|
* Prevents StackOverflow with blocks trying to modify their position in onDestroy or onPlace.
|
|
*
|
|
* @param blockPosition the block position
|
|
* @param blockStateId the block state id
|
|
* @return true if the block changed since the last update
|
|
*/
|
|
private boolean isAlreadyChanged(@NotNull BlockPosition blockPosition, short blockStateId) {
|
|
final Block changedBlock = currentlyChangingBlocks.get(blockPosition);
|
|
if (changedBlock == null)
|
|
return false;
|
|
return changedBlock.getBlockId() == blockStateId;
|
|
}
|
|
|
|
@Override
|
|
public void refreshBlockStateId(@NotNull BlockPosition blockPosition, short blockStateId) {
|
|
final Chunk chunk = getChunkAt(blockPosition.getX(), blockPosition.getZ());
|
|
Check.notNull(chunk, "You cannot refresh a block in a null chunk!");
|
|
synchronized (chunk) {
|
|
chunk.refreshBlockStateId(blockPosition.getX(), blockPosition.getY(),
|
|
blockPosition.getZ(), blockStateId);
|
|
|
|
sendBlockChange(chunk, blockPosition, blockStateId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calls {@link CustomBlock#onDestroy(Instance, BlockPosition, Data)} for {@code previousBlock}.
|
|
* <p>
|
|
* WARNING {@code chunk} needs to be synchronized.
|
|
*
|
|
* @param chunk the chunk where the block is
|
|
* @param index the index of the block
|
|
* @param previousBlock the block which has been destroyed
|
|
* @param blockPosition the block position
|
|
*/
|
|
private void callBlockDestroy(@NotNull Chunk chunk, int index, @NotNull CustomBlock previousBlock, @NotNull BlockPosition blockPosition) {
|
|
final Data previousData = chunk.getBlockData(index);
|
|
previousBlock.onDestroy(this, blockPosition, previousData);
|
|
}
|
|
|
|
/**
|
|
* Calls {@link CustomBlock#onPlace(Instance, BlockPosition, Data)} for the current custom block at the position.
|
|
* <p>
|
|
* WARNING {@code chunk} needs to be synchronized.
|
|
*
|
|
* @param chunk the chunk where the block is
|
|
* @param index the block index
|
|
* @param blockPosition the block position
|
|
*/
|
|
private void callBlockPlace(@NotNull Chunk chunk, int index, @NotNull BlockPosition blockPosition) {
|
|
final CustomBlock actualBlock = chunk.getCustomBlock(index);
|
|
if (actualBlock == null)
|
|
return;
|
|
final Data previousData = chunk.getBlockData(index);
|
|
actualBlock.onPlace(this, blockPosition, previousData);
|
|
}
|
|
|
|
/**
|
|
* Calls the {@link BlockPlacementRule} for the specified block state id.
|
|
*
|
|
* @param blockStateId the block state id to modify
|
|
* @param blockPosition the block position
|
|
* @return the modified block state id
|
|
*/
|
|
private short executeBlockPlacementRule(short blockStateId, @NotNull BlockPosition blockPosition) {
|
|
final BlockPlacementRule blockPlacementRule = BLOCK_MANAGER.getBlockPlacementRule(blockStateId);
|
|
if (blockPlacementRule != null) {
|
|
return blockPlacementRule.blockRefresh(this, blockPosition, blockStateId);
|
|
}
|
|
return blockStateId;
|
|
}
|
|
|
|
/**
|
|
* Executed when a block is modified, this is used to modify the states of neighbours blocks.
|
|
* <p>
|
|
* For example, this can be used for redstone wires which need an understanding of its neighborhoods to take the right shape.
|
|
*
|
|
* @param blockPosition the position of the modified block
|
|
*/
|
|
private void executeNeighboursBlockPlacementRule(BlockPosition blockPosition) {
|
|
for (int offsetX = -1; offsetX < 2; offsetX++) {
|
|
for (int offsetY = -1; offsetY < 2; offsetY++) {
|
|
for (int offsetZ = -1; offsetZ < 2; offsetZ++) {
|
|
if (offsetX == 0 && offsetY == 0 && offsetZ == 0)
|
|
continue;
|
|
final int neighborX = blockPosition.getX() + offsetX;
|
|
final int neighborY = blockPosition.getY() + offsetY;
|
|
final int neighborZ = blockPosition.getZ() + offsetZ;
|
|
final Chunk chunk = getChunkAt(neighborX, neighborZ);
|
|
|
|
// Do not try to get neighbour in an unloaded chunk
|
|
if (chunk == null)
|
|
continue;
|
|
|
|
final short neighborStateId = chunk.getBlockStateId(neighborX, neighborY, neighborZ);
|
|
final BlockPlacementRule neighborBlockPlacementRule = BLOCK_MANAGER.getBlockPlacementRule(neighborStateId);
|
|
if (neighborBlockPlacementRule != null) {
|
|
final short newNeighborId = neighborBlockPlacementRule.blockRefresh(this,
|
|
new BlockPosition(neighborX, neighborY, neighborZ), neighborStateId);
|
|
if (neighborStateId != newNeighborId) {
|
|
refreshBlockStateId(neighborX, neighborY, neighborZ, neighborStateId);
|
|
}
|
|
}
|
|
|
|
// Update neighbors
|
|
final CustomBlock customBlock = getCustomBlock(neighborX, neighborY, neighborZ);
|
|
if (customBlock != null) {
|
|
boolean directNeighbor = false; // only if directly connected to neighbor (no diagonals)
|
|
if (offsetX != 0 ^ offsetZ != 0) {
|
|
directNeighbor = offsetY == 0;
|
|
} else if (offsetX == 0 && offsetZ == 0) {
|
|
directNeighbor = true;
|
|
}
|
|
customBlock.updateFromNeighbor(this, new BlockPosition(neighborX, neighborY, neighborZ), blockPosition, directNeighbor);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean breakBlock(@NotNull Player player, @NotNull BlockPosition blockPosition) {
|
|
player.resetTargetBlock();
|
|
|
|
final Chunk chunk = getChunkAt(blockPosition);
|
|
Check.notNull(chunk, "You cannot break blocks in a null chunk!");
|
|
|
|
// Cancel if the chunk is read-only
|
|
if (chunk.isReadOnly()) {
|
|
return false;
|
|
}
|
|
|
|
// Chunk unloaded, stop here
|
|
if (!ChunkUtils.isLoaded(chunk))
|
|
return false;
|
|
|
|
final int x = blockPosition.getX();
|
|
final int y = blockPosition.getY();
|
|
final int z = blockPosition.getZ();
|
|
|
|
final short blockStateId = getBlockStateId(x, y, z);
|
|
|
|
// The player probably have a wrong version of this chunk section, send it
|
|
if (blockStateId == 0) {
|
|
chunk.sendChunkSectionUpdate(ChunkUtils.getSectionAt(y), player);
|
|
return false;
|
|
}
|
|
|
|
final CustomBlock customBlock = getCustomBlock(x, y, z);
|
|
|
|
PlayerBlockBreakEvent blockBreakEvent = new PlayerBlockBreakEvent(player, blockPosition, blockStateId, customBlock, (short) 0, (short) 0);
|
|
player.callEvent(PlayerBlockBreakEvent.class, blockBreakEvent);
|
|
final boolean allowed = !blockBreakEvent.isCancelled();
|
|
if (allowed) {
|
|
// Break or change the broken block based on event result
|
|
setSeparateBlocks(x, y, z, blockBreakEvent.getResultBlockStateId(), blockBreakEvent.getResultCustomBlockId());
|
|
|
|
ParticlePacket particlePacket = ParticleCreator.createParticlePacket(Particle.BLOCK, false,
|
|
x + 0.5f, y, z + 0.5f,
|
|
0.4f, 0.5f, 0.4f,
|
|
0.3f, 125, writer -> writer.writeVarInt(blockStateId));
|
|
|
|
chunk.getViewers().forEach(p -> {
|
|
// The player who breaks the block already get particles client-side
|
|
if (customBlock != null || !(p.equals(player) && !player.isCreative())) {
|
|
p.getPlayerConnection().sendPacket(particlePacket);
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
return allowed;
|
|
}
|
|
|
|
@Override
|
|
public void loadChunk(int chunkX, int chunkZ, @Nullable ChunkCallback callback) {
|
|
final Chunk chunk = getChunk(chunkX, chunkZ);
|
|
if (chunk != null) {
|
|
// Chunk already loaded
|
|
OptionalCallback.execute(callback, chunk);
|
|
} else {
|
|
// Retrieve chunk from somewhere else (file or create a new one using the ChunkGenerator)
|
|
retrieveChunk(chunkX, chunkZ, callback);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void loadOptionalChunk(int chunkX, int chunkZ, @Nullable ChunkCallback callback) {
|
|
final Chunk chunk = getChunk(chunkX, chunkZ);
|
|
if (chunk != null) {
|
|
// Chunk already loaded
|
|
OptionalCallback.execute(callback, chunk);
|
|
} else {
|
|
if (hasEnabledAutoChunkLoad()) {
|
|
// Load chunk from StorageLocation or with ChunkGenerator
|
|
retrieveChunk(chunkX, chunkZ, callback);
|
|
} else {
|
|
// Chunk not loaded, return null
|
|
OptionalCallback.execute(callback, null);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void unloadChunk(@NotNull Chunk chunk) {
|
|
// Already unloaded chunk
|
|
if (!ChunkUtils.isLoaded(chunk)) {
|
|
return;
|
|
}
|
|
// Schedule the chunk removal
|
|
synchronized (this.scheduledChunksToRemove) {
|
|
this.scheduledChunksToRemove.add(chunk);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Chunk getChunk(int chunkX, int chunkZ) {
|
|
final long index = ChunkUtils.getChunkIndex(chunkX, chunkZ);
|
|
final Chunk chunk = chunks.get(index);
|
|
return ChunkUtils.isLoaded(chunk) ? chunk : null;
|
|
}
|
|
|
|
/**
|
|
* Saves the instance ({@link #getUniqueId()} {@link #getData()}) and call {@link #saveChunksToStorage(Runnable)}.
|
|
* <p>
|
|
* WARNING: {@link #getData()} needs to be a {@link SerializableData} in order to be saved.
|
|
*
|
|
* @param callback the optional callback once the saving is done
|
|
*/
|
|
public void saveInstance(@Nullable Runnable callback) {
|
|
Check.notNull(getStorageLocation(), "You cannot save the instance if no StorageLocation has been defined");
|
|
|
|
this.storageLocation.set(UUID_KEY, getUniqueId(), UUID.class);
|
|
final Data data = getData();
|
|
if (data != null) {
|
|
// Save the instance data
|
|
Check.stateCondition(!(data instanceof SerializableData),
|
|
"Instance#getData needs to be a SerializableData in order to be saved");
|
|
this.storageLocation.set(DATA_KEY, (SerializableData) getData(), SerializableData.class);
|
|
}
|
|
|
|
saveChunksToStorage(callback);
|
|
}
|
|
|
|
/**
|
|
* Save the instance without callback.
|
|
*
|
|
* @see #saveInstance(Runnable)
|
|
*/
|
|
public void saveInstance() {
|
|
saveInstance(null);
|
|
}
|
|
|
|
@Override
|
|
public void saveChunkToStorage(@NotNull Chunk chunk, Runnable callback) {
|
|
this.chunkLoader.saveChunk(chunk, callback);
|
|
}
|
|
|
|
@Override
|
|
public void saveChunksToStorage(@Nullable Runnable callback) {
|
|
this.chunkLoader.saveChunks(chunks.values(), callback);
|
|
}
|
|
|
|
@Override
|
|
public BlockBatch createBlockBatch() {
|
|
return new BlockBatch(this);
|
|
}
|
|
|
|
@Override
|
|
public ChunkBatch createChunkBatch(@NotNull Chunk chunk) {
|
|
Check.notNull(chunk, "The chunk of a ChunkBatch cannot be null");
|
|
return new ChunkBatch(this, chunk, false);
|
|
}
|
|
|
|
@Override
|
|
protected void retrieveChunk(int chunkX, int chunkZ, @Nullable ChunkCallback callback) {
|
|
final boolean loaded = chunkLoader.loadChunk(this, chunkX, chunkZ, chunk -> {
|
|
cacheChunk(chunk);
|
|
UPDATE_MANAGER.signalChunkLoad(this, chunkX, chunkZ);
|
|
// Execute callback and event in the instance thread
|
|
scheduleNextTick(instance -> {
|
|
callChunkLoadEvent(chunkX, chunkZ);
|
|
OptionalCallback.execute(callback, chunk);
|
|
});
|
|
});
|
|
|
|
if (!loaded) {
|
|
// Not found, create a new chunk
|
|
createChunk(chunkX, chunkZ, callback);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void createChunk(int chunkX, int chunkZ, @Nullable ChunkCallback callback) {
|
|
Biome[] biomes = new Biome[Chunk.BIOME_COUNT];
|
|
if (chunkGenerator == null) {
|
|
Arrays.fill(biomes, MinecraftServer.getBiomeManager().getById(0));
|
|
} else {
|
|
chunkGenerator.fillBiomes(biomes, chunkX, chunkZ);
|
|
}
|
|
|
|
final Chunk chunk = chunkSupplier.getChunk(biomes, chunkX, chunkZ);
|
|
Check.notNull(chunk, "Chunks supplied by a ChunkSupplier cannot be null.");
|
|
|
|
cacheChunk(chunk);
|
|
|
|
if (chunkGenerator != null && chunk.shouldGenerate()) {
|
|
// Execute the chunk generator to populate the chunk
|
|
final ChunkBatch chunkBatch = new ChunkBatch(this, chunk, true);
|
|
|
|
chunkBatch.flushChunkGenerator(chunkGenerator, callback);
|
|
} else {
|
|
// No chunk generator, execute the callback with the empty chunk
|
|
OptionalCallback.execute(callback, chunk);
|
|
}
|
|
|
|
UPDATE_MANAGER.signalChunkLoad(this, chunkX, chunkZ);
|
|
callChunkLoadEvent(chunkX, chunkZ);
|
|
}
|
|
|
|
@Override
|
|
public void enableAutoChunkLoad(boolean enable) {
|
|
this.autoChunkLoad = enable;
|
|
}
|
|
|
|
@Override
|
|
public boolean hasEnabledAutoChunkLoad() {
|
|
return autoChunkLoad;
|
|
}
|
|
|
|
@Override
|
|
public boolean isInVoid(@NotNull Position position) {
|
|
// TODO: customizable
|
|
return position.getY() < -64;
|
|
}
|
|
|
|
/**
|
|
* Changes which type of {@link Chunk} implementation to use once one needs to be loaded.
|
|
* <p>
|
|
* Uses {@link DynamicChunk} by default.
|
|
* <p>
|
|
* WARNING: if you need to save this instance's chunks later,
|
|
* the code needs to be predictable for {@link IChunkLoader#loadChunk(Instance, int, int, ChunkCallback)}
|
|
* to create the correct type of {@link Chunk}. tl;dr: Need chunk save = no random type.
|
|
*
|
|
* @param chunkSupplier the new {@link ChunkSupplier} of this instance, chunks need to be non-null
|
|
* @throws NullPointerException if {@code chunkSupplier} is null
|
|
*/
|
|
public void setChunkSupplier(@NotNull ChunkSupplier chunkSupplier) {
|
|
Check.notNull(chunkSupplier, "The chunk supplier cannot be null!");
|
|
this.chunkSupplier = chunkSupplier;
|
|
}
|
|
|
|
/**
|
|
* Gets the current {@link ChunkSupplier}.
|
|
* <p>
|
|
* You shouldn't use it to generate a new chunk, but as a way to view which one is currently in use.
|
|
*
|
|
* @return the current {@link ChunkSupplier}
|
|
*/
|
|
public ChunkSupplier getChunkSupplier() {
|
|
return chunkSupplier;
|
|
}
|
|
|
|
/**
|
|
* Gets all the {@link SharedInstance} linked to this container.
|
|
*
|
|
* @return an unmodifiable {@link List} containing all the {@link SharedInstance} linked to this container
|
|
*/
|
|
public List<SharedInstance> getSharedInstances() {
|
|
return Collections.unmodifiableList(sharedInstances);
|
|
}
|
|
|
|
/**
|
|
* Gets if this instance has {@link SharedInstance} linked to it.
|
|
*
|
|
* @return true if {@link #getSharedInstances()} is not empty
|
|
*/
|
|
public boolean hasSharedInstances() {
|
|
return !sharedInstances.isEmpty();
|
|
}
|
|
|
|
/**
|
|
* Assigns a {@link SharedInstance} to this container.
|
|
* <p>
|
|
* Only used by {@link InstanceManager}, mostly unsafe.
|
|
*
|
|
* @param sharedInstance the shared instance to assign to this container
|
|
*/
|
|
protected void addSharedInstance(SharedInstance sharedInstance) {
|
|
this.sharedInstances.add(sharedInstance);
|
|
}
|
|
|
|
/**
|
|
* Copies all the chunks of this instance and create a new instance container with all of them.
|
|
* <p>
|
|
* Chunks are copied with {@link Chunk#copy(int, int)},
|
|
* {@link UUID} is randomized, {@link DimensionType} is passed over and the {@link StorageLocation} is null.
|
|
*
|
|
* @return an {@link InstanceContainer} with the exact same chunks as 'this'
|
|
* @see #getSrcInstance() to retrieve the "creation source" of the copied instance
|
|
*/
|
|
public synchronized InstanceContainer copy() {
|
|
InstanceContainer copiedInstance = new InstanceContainer(UUID.randomUUID(), getDimensionType(), null);
|
|
copiedInstance.srcInstance = this;
|
|
copiedInstance.lastBlockChangeTime = lastBlockChangeTime;
|
|
|
|
for (Chunk chunk : chunks.values()) {
|
|
final int chunkX = chunk.getChunkX();
|
|
final int chunkZ = chunk.getChunkZ();
|
|
|
|
final Chunk copiedChunk = chunk.copy(chunkX, chunkZ);
|
|
|
|
copiedInstance.cacheChunk(copiedChunk);
|
|
UPDATE_MANAGER.signalChunkLoad(copiedInstance, chunkX, chunkZ);
|
|
}
|
|
|
|
return copiedInstance;
|
|
}
|
|
|
|
/**
|
|
* Gets the instance from which this one has been copied.
|
|
* <p>
|
|
* Only present if this instance has been created with {@link InstanceContainer#copy()}.
|
|
*
|
|
* @return the instance source, null if not created by a copy
|
|
* @see #copy() to create a copy of this instance with 'this' as the source
|
|
*/
|
|
@Nullable
|
|
public InstanceContainer getSrcInstance() {
|
|
return srcInstance;
|
|
}
|
|
|
|
/**
|
|
* Gets the last time at which a block changed.
|
|
*
|
|
* @return the time at which the last block changed in milliseconds, 0 if never
|
|
*/
|
|
public long getLastBlockChangeTime() {
|
|
return lastBlockChangeTime;
|
|
}
|
|
|
|
/**
|
|
* Signals the instance that a block changed.
|
|
* <p>
|
|
* Useful if you change blocks values directly using a {@link Chunk} object.
|
|
*/
|
|
public void refreshLastBlockChangeTime() {
|
|
this.lastBlockChangeTime = System.currentTimeMillis();
|
|
}
|
|
|
|
/**
|
|
* Adds a {@link Chunk} to the internal instance map.
|
|
* <p>
|
|
* WARNING: the chunk will not automatically be sent to players and
|
|
* {@link net.minestom.server.UpdateManager#signalChunkLoad(Instance, int, int)} must be called manually.
|
|
*
|
|
* @param chunk the chunk to cache
|
|
*/
|
|
public void cacheChunk(@NotNull Chunk chunk) {
|
|
final long index = ChunkUtils.getChunkIndex(chunk.getChunkX(), chunk.getChunkZ());
|
|
this.chunks.put(index, chunk);
|
|
}
|
|
|
|
@Override
|
|
public ChunkGenerator getChunkGenerator() {
|
|
return chunkGenerator;
|
|
}
|
|
|
|
@Override
|
|
public void setChunkGenerator(ChunkGenerator chunkGenerator) {
|
|
this.chunkGenerator = chunkGenerator;
|
|
}
|
|
|
|
/**
|
|
* Gets all the instance chunks.
|
|
*
|
|
* @return the chunks of this instance
|
|
*/
|
|
@NotNull
|
|
public Collection<Chunk> getChunks() {
|
|
return Collections.unmodifiableCollection(chunks.values());
|
|
}
|
|
|
|
@Override
|
|
public StorageLocation getStorageLocation() {
|
|
return storageLocation;
|
|
}
|
|
|
|
@Override
|
|
public void setStorageLocation(StorageLocation storageLocation) {
|
|
this.storageLocation = storageLocation;
|
|
}
|
|
|
|
/**
|
|
* Gets the {@link IChunkLoader} of this instance.
|
|
*
|
|
* @return the {@link IChunkLoader} of this instance
|
|
*/
|
|
public IChunkLoader getChunkLoader() {
|
|
return chunkLoader;
|
|
}
|
|
|
|
/**
|
|
* Changes the {@link IChunkLoader} of this instance (to change how chunks are retrieved when not already loaded).
|
|
*
|
|
* @param chunkLoader the new {@link IChunkLoader}
|
|
*/
|
|
public void setChunkLoader(IChunkLoader chunkLoader) {
|
|
this.chunkLoader = chunkLoader;
|
|
}
|
|
|
|
/**
|
|
* Sends a {@link BlockChangePacket} at the specified {@link BlockPosition} to set the block as {@code blockStateId}.
|
|
* <p>
|
|
* WARNING: this does not change the internal block data, this is strictly visual for the players.
|
|
*
|
|
* @param chunk the chunk where the block is
|
|
* @param blockPosition the block position
|
|
* @param blockStateId the new state of the block
|
|
*/
|
|
private void sendBlockChange(@NotNull Chunk chunk, @NotNull BlockPosition blockPosition, short blockStateId) {
|
|
BlockChangePacket blockChangePacket = new BlockChangePacket();
|
|
blockChangePacket.blockPosition = blockPosition;
|
|
blockChangePacket.blockStateId = blockStateId;
|
|
chunk.sendPacketToViewers(blockChangePacket);
|
|
}
|
|
|
|
@Override
|
|
public void scheduleUpdate(int time, @NotNull TimeUnit unit, @NotNull BlockPosition position) {
|
|
final CustomBlock toUpdate = getCustomBlock(position);
|
|
if (toUpdate == null) {
|
|
return;
|
|
}
|
|
|
|
MinecraftServer.getSchedulerManager().buildTask(() -> {
|
|
final CustomBlock currentBlock = getCustomBlock(position);
|
|
if (currentBlock == null)
|
|
return;
|
|
if (currentBlock.getCustomBlockId() != toUpdate.getCustomBlockId()) { // block changed
|
|
return;
|
|
}
|
|
currentBlock.scheduledUpdate(this, position, getBlockData(position));
|
|
}).delay(time, unit).schedule();
|
|
}
|
|
|
|
@Override
|
|
public void tick(long time) {
|
|
// Unload all waiting chunks
|
|
UNSAFE_unloadChunks();
|
|
|
|
// Time/world border
|
|
super.tick(time);
|
|
|
|
Lock wrlock = changingBlockLock.writeLock();
|
|
wrlock.lock();
|
|
currentlyChangingBlocks.clear();
|
|
wrlock.unlock();
|
|
}
|
|
|
|
/**
|
|
* Unloads all waiting chunks.
|
|
* <p>
|
|
* Unsafe because it has to be done on the same thread as the instance/chunks tick update.
|
|
*/
|
|
protected void UNSAFE_unloadChunks() {
|
|
synchronized (this.scheduledChunksToRemove) {
|
|
for (Chunk chunk : scheduledChunksToRemove) {
|
|
final int chunkX = chunk.getChunkX();
|
|
final int chunkZ = chunk.getChunkZ();
|
|
|
|
final long index = ChunkUtils.getChunkIndex(chunkX, chunkZ);
|
|
|
|
UnloadChunkPacket unloadChunkPacket = new UnloadChunkPacket();
|
|
unloadChunkPacket.chunkX = chunkX;
|
|
unloadChunkPacket.chunkZ = chunkZ;
|
|
chunk.sendPacketToViewers(unloadChunkPacket);
|
|
|
|
for (Player viewer : chunk.getViewers()) {
|
|
chunk.removeViewer(viewer);
|
|
}
|
|
|
|
callChunkUnloadEvent(chunkX, chunkZ);
|
|
|
|
// Remove all entities in chunk
|
|
getChunkEntities(chunk).forEach(entity -> {
|
|
if (!(entity instanceof Player))
|
|
entity.remove();
|
|
});
|
|
|
|
// Clear cache
|
|
this.chunks.remove(index);
|
|
this.chunkEntities.remove(index);
|
|
|
|
chunk.unload();
|
|
|
|
UPDATE_MANAGER.signalChunkUnload(this, chunkX, chunkZ);
|
|
}
|
|
this.scheduledChunksToRemove.clear();
|
|
}
|
|
}
|
|
|
|
private void callChunkLoadEvent(int chunkX, int chunkZ) {
|
|
InstanceChunkLoadEvent chunkLoadEvent = new InstanceChunkLoadEvent(this, chunkX, chunkZ);
|
|
callEvent(InstanceChunkLoadEvent.class, chunkLoadEvent);
|
|
}
|
|
|
|
private void callChunkUnloadEvent(int chunkX, int chunkZ) {
|
|
InstanceChunkUnloadEvent chunkUnloadEvent = new InstanceChunkUnloadEvent(this, chunkX, chunkZ);
|
|
callEvent(InstanceChunkUnloadEvent.class, chunkUnloadEvent);
|
|
}
|
|
} |