mirror of https://github.com/Minestom/Minestom.git
678 lines
29 KiB
Java
678 lines
29 KiB
Java
package net.minestom.server.instance;
|
|
|
|
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
|
|
import net.minestom.server.MinecraftServer;
|
|
import net.minestom.server.coordinate.Point;
|
|
import net.minestom.server.coordinate.Vec;
|
|
import net.minestom.server.entity.Entity;
|
|
import net.minestom.server.entity.Player;
|
|
import net.minestom.server.event.EventDispatcher;
|
|
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.block.Block;
|
|
import net.minestom.server.instance.block.BlockFace;
|
|
import net.minestom.server.instance.block.BlockHandler;
|
|
import net.minestom.server.instance.block.rule.BlockPlacementRule;
|
|
import net.minestom.server.instance.generator.Generator;
|
|
import net.minestom.server.instance.palette.Palette;
|
|
import net.minestom.server.network.packet.server.play.BlockChangePacket;
|
|
import net.minestom.server.network.packet.server.play.BlockEntityDataPacket;
|
|
import net.minestom.server.network.packet.server.play.EffectPacket;
|
|
import net.minestom.server.network.packet.server.play.UnloadChunkPacket;
|
|
import net.minestom.server.thread.TickSchedulerThread;
|
|
import net.minestom.server.utils.NamespaceID;
|
|
import net.minestom.server.utils.PacketUtils;
|
|
import net.minestom.server.utils.async.AsyncUtils;
|
|
import net.minestom.server.utils.block.BlockUtils;
|
|
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.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 org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import space.vectrix.flare.fastutil.Long2ObjectSyncMap;
|
|
|
|
import java.util.*;
|
|
import java.util.concurrent.CompletableFuture;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import java.util.concurrent.CopyOnWriteArrayList;
|
|
import java.util.concurrent.ForkJoinPool;
|
|
import java.util.concurrent.locks.Lock;
|
|
import java.util.concurrent.locks.ReentrantLock;
|
|
import java.util.function.Consumer;
|
|
import java.util.function.Supplier;
|
|
|
|
import static net.minestom.server.utils.chunk.ChunkUtils.*;
|
|
|
|
/**
|
|
* InstanceContainer is an instance that contains chunks in contrary to SharedInstance.
|
|
*/
|
|
public class InstanceContainer extends Instance {
|
|
private static final Logger LOGGER = LoggerFactory.getLogger(InstanceContainer.class);
|
|
|
|
private static final AnvilLoader DEFAULT_LOADER = new AnvilLoader("world");
|
|
|
|
private static final BlockFace[] BLOCK_UPDATE_FACES = new BlockFace[]{
|
|
BlockFace.WEST, BlockFace.EAST, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.BOTTOM, BlockFace.TOP
|
|
};
|
|
|
|
// the shared instances assigned to this instance
|
|
private final List<SharedInstance> sharedInstances = new CopyOnWriteArrayList<>();
|
|
|
|
// the chunk generator used, can be null
|
|
private volatile Generator generator;
|
|
// (chunk index -> chunk) map, contains all the chunks in the instance
|
|
// used as a monitor when access is required
|
|
private final Long2ObjectSyncMap<Chunk> chunks = Long2ObjectSyncMap.hashmap();
|
|
private final Map<Long, CompletableFuture<Chunk>> loadingChunks = new ConcurrentHashMap<>();
|
|
|
|
private final Lock changingBlockLock = new ReentrantLock();
|
|
private final Map<Point, 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 = true;
|
|
|
|
// 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)
|
|
|
|
public InstanceContainer(@NotNull UUID uniqueId, @NotNull DimensionType dimensionType) {
|
|
this(uniqueId, dimensionType, null, dimensionType.getName());
|
|
}
|
|
|
|
public InstanceContainer(@NotNull UUID uniqueId, @NotNull DimensionType dimensionType, @NotNull NamespaceID dimensionName) {
|
|
this(uniqueId, dimensionType, null, dimensionName);
|
|
}
|
|
|
|
@ApiStatus.Experimental
|
|
public InstanceContainer(@NotNull UUID uniqueId, @NotNull DimensionType dimensionType, @Nullable IChunkLoader loader) {
|
|
this(uniqueId, dimensionType, loader, dimensionType.getName());
|
|
}
|
|
|
|
@ApiStatus.Experimental
|
|
public InstanceContainer(@NotNull UUID uniqueId, @NotNull DimensionType dimensionType, @Nullable IChunkLoader loader, @NotNull NamespaceID dimensionName) {
|
|
super(uniqueId, dimensionType, dimensionName);
|
|
setChunkSupplier(DynamicChunk::new);
|
|
setChunkLoader(Objects.requireNonNullElse(loader, DEFAULT_LOADER));
|
|
this.chunkLoader.loadInstance(this);
|
|
}
|
|
|
|
@Override
|
|
public void setBlock(int x, int y, int z, @NotNull Block block, boolean doBlockUpdates) {
|
|
Chunk chunk = getChunkAt(x, z);
|
|
if (chunk == null) {
|
|
Check.stateCondition(!hasEnabledAutoChunkLoad(),
|
|
"Tried to set a block to an unloaded chunk with auto chunk load disabled");
|
|
chunk = loadChunk(getChunkCoordinate(x), getChunkCoordinate(z)).join();
|
|
}
|
|
if (isLoaded(chunk)) UNSAFE_setBlock(chunk, x, y, z, block, null, null, doBlockUpdates, 0);
|
|
}
|
|
|
|
/**
|
|
* Sets a block at the specified 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 block the block to place
|
|
*/
|
|
private synchronized void UNSAFE_setBlock(@NotNull Chunk chunk, int x, int y, int z, @NotNull Block block,
|
|
@Nullable BlockHandler.Placement placement, @Nullable BlockHandler.Destroy destroy,
|
|
boolean doBlockUpdates, int updateDistance) {
|
|
if (chunk.isReadOnly()) return;
|
|
if(y >= getDimensionType().getMaxY() || y < getDimensionType().getMinY()) {
|
|
LOGGER.warn("tried to set a block outside the world bounds, should be within [{}, {}): {}", getDimensionType().getMinY(), getDimensionType().getMaxY(), y);
|
|
return;
|
|
}
|
|
|
|
synchronized (chunk) {
|
|
// Refresh the last block change time
|
|
this.lastBlockChangeTime = System.currentTimeMillis();
|
|
final Vec blockPosition = new Vec(x, y, z);
|
|
if (isAlreadyChanged(blockPosition, block)) { // 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;
|
|
}
|
|
this.currentlyChangingBlocks.put(blockPosition, block);
|
|
|
|
// Change id based on neighbors
|
|
final BlockPlacementRule blockPlacementRule = MinecraftServer.getBlockManager().getBlockPlacementRule(block);
|
|
if (placement != null && blockPlacementRule != null && doBlockUpdates) {
|
|
BlockPlacementRule.PlacementState rulePlacement;
|
|
if (placement instanceof BlockHandler.PlayerPlacement pp) {
|
|
rulePlacement = new BlockPlacementRule.PlacementState(
|
|
this, block, pp.getBlockFace(), blockPosition,
|
|
new Vec(pp.getCursorX(), pp.getCursorY(), pp.getCursorZ()),
|
|
pp.getPlayer().getPosition(),
|
|
pp.getPlayer().getItemInHand(pp.getHand()).meta(),
|
|
pp.getPlayer().isSneaking()
|
|
);
|
|
} else {
|
|
rulePlacement = new BlockPlacementRule.PlacementState(
|
|
this, block, null, blockPosition,
|
|
null, null, null,
|
|
false
|
|
);
|
|
}
|
|
|
|
block = blockPlacementRule.blockPlace(rulePlacement);
|
|
if (block == null) block = Block.AIR;
|
|
}
|
|
|
|
// Set the block
|
|
chunk.setBlock(x, y, z, block, placement, destroy);
|
|
|
|
// Refresh neighbors since a new block has been placed
|
|
if (doBlockUpdates) {
|
|
executeNeighboursBlockPlacementRule(blockPosition, updateDistance);
|
|
}
|
|
|
|
// Refresh player chunk block
|
|
{
|
|
chunk.sendPacketToViewers(new BlockChangePacket(blockPosition, block.stateId()));
|
|
var registry = block.registry();
|
|
if (registry.isBlockEntity()) {
|
|
final NBTCompound data = BlockUtils.extractClientNbt(block);
|
|
chunk.sendPacketToViewers(new BlockEntityDataPacket(blockPosition, registry.blockEntityId(), data));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean placeBlock(@NotNull BlockHandler.Placement placement, boolean doBlockUpdates) {
|
|
final Point blockPosition = placement.getBlockPosition();
|
|
final Chunk chunk = getChunkAt(blockPosition);
|
|
if (!isLoaded(chunk)) return false;
|
|
UNSAFE_setBlock(chunk, blockPosition.blockX(), blockPosition.blockY(), blockPosition.blockZ(),
|
|
placement.getBlock(), placement, null, doBlockUpdates, 0);
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean breakBlock(@NotNull Player player, @NotNull Point blockPosition, @NotNull BlockFace blockFace, boolean doBlockUpdates) {
|
|
final Chunk chunk = getChunkAt(blockPosition);
|
|
Check.notNull(chunk, "You cannot break blocks in a null chunk!");
|
|
if (chunk.isReadOnly()) return false;
|
|
if (!isLoaded(chunk)) return false;
|
|
|
|
final Block block = getBlock(blockPosition);
|
|
final int x = blockPosition.blockX();
|
|
final int y = blockPosition.blockY();
|
|
final int z = blockPosition.blockZ();
|
|
if (block.isAir()) {
|
|
// The player probably have a wrong version of this chunk section, send it
|
|
chunk.sendChunk(player);
|
|
return false;
|
|
}
|
|
PlayerBlockBreakEvent blockBreakEvent = new PlayerBlockBreakEvent(player, block, Block.AIR, blockPosition, blockFace);
|
|
EventDispatcher.call(blockBreakEvent);
|
|
final boolean allowed = !blockBreakEvent.isCancelled();
|
|
if (allowed) {
|
|
// Break or change the broken block based on event result
|
|
final Block resultBlock = blockBreakEvent.getResultBlock();
|
|
UNSAFE_setBlock(chunk, x, y, z, resultBlock, null,
|
|
new BlockHandler.PlayerDestroy(block, this, blockPosition, player), doBlockUpdates, 0);
|
|
// Send the block break effect packet
|
|
PacketUtils.sendGroupedPacket(chunk.getViewers(),
|
|
new EffectPacket(2001 /*Block break + block break sound*/, blockPosition, block.stateId(), false),
|
|
// Prevent the block breaker to play the particles and sound two times
|
|
(viewer) -> !viewer.equals(player));
|
|
}
|
|
return allowed;
|
|
}
|
|
|
|
@Override
|
|
public @NotNull CompletableFuture<Chunk> loadChunk(int chunkX, int chunkZ) {
|
|
return loadOrRetrieve(chunkX, chunkZ, () -> retrieveChunk(chunkX, chunkZ));
|
|
}
|
|
|
|
@Override
|
|
public @NotNull CompletableFuture<Chunk> loadOptionalChunk(int chunkX, int chunkZ) {
|
|
return loadOrRetrieve(chunkX, chunkZ, () -> hasEnabledAutoChunkLoad() ? retrieveChunk(chunkX, chunkZ) : AsyncUtils.empty());
|
|
}
|
|
|
|
@Override
|
|
public synchronized void unloadChunk(@NotNull Chunk chunk) {
|
|
if (!isLoaded(chunk)) return;
|
|
final int chunkX = chunk.getChunkX();
|
|
final int chunkZ = chunk.getChunkZ();
|
|
chunk.sendPacketToViewers(new UnloadChunkPacket(chunkX, chunkZ));
|
|
EventDispatcher.call(new InstanceChunkUnloadEvent(this, chunk));
|
|
// Remove all entities in chunk
|
|
getEntityTracker().chunkEntities(chunkX, chunkZ, EntityTracker.Target.ENTITIES).forEach(Entity::remove);
|
|
// Clear cache
|
|
this.chunks.remove(getChunkIndex(chunkX, chunkZ));
|
|
chunk.unload();
|
|
if (chunkLoader != null) {
|
|
chunkLoader.unloadChunk(chunk);
|
|
}
|
|
var dispatcher = MinecraftServer.process().dispatcher();
|
|
dispatcher.deletePartition(chunk);
|
|
}
|
|
|
|
@Override
|
|
public Chunk getChunk(int chunkX, int chunkZ) {
|
|
return chunks.get(getChunkIndex(chunkX, chunkZ));
|
|
}
|
|
|
|
@Override
|
|
public @NotNull CompletableFuture<Void> saveInstance() {
|
|
return chunkLoader.saveInstance(this);
|
|
}
|
|
|
|
@Override
|
|
public @NotNull CompletableFuture<Void> saveChunkToStorage(@NotNull Chunk chunk) {
|
|
return chunkLoader.saveChunk(chunk);
|
|
}
|
|
|
|
@Override
|
|
public @NotNull CompletableFuture<Void> saveChunksToStorage() {
|
|
return chunkLoader.saveChunks(getChunks());
|
|
}
|
|
|
|
protected @NotNull CompletableFuture<@NotNull Chunk> retrieveChunk(int chunkX, int chunkZ) {
|
|
CompletableFuture<Chunk> completableFuture = new CompletableFuture<>();
|
|
final long index = getChunkIndex(chunkX, chunkZ);
|
|
final CompletableFuture<Chunk> prev = loadingChunks.putIfAbsent(index, completableFuture);
|
|
if (prev != null) return prev;
|
|
final IChunkLoader loader = chunkLoader;
|
|
final Consumer<Chunk> postLoadCallback = chunk -> {
|
|
cacheChunk(chunk);
|
|
chunk.onLoad();
|
|
|
|
EventDispatcher.call(new InstanceChunkLoadEvent(this, chunk));
|
|
final CompletableFuture<Chunk> future = this.loadingChunks.remove(index);
|
|
assert future == completableFuture : "Invalid future: " + future;
|
|
completableFuture.complete(chunk);
|
|
};
|
|
final Runnable retriever = () -> loader.loadChunk(this, chunkX, chunkZ)
|
|
.thenCompose(chunk -> {
|
|
if (chunk != null) {
|
|
// Chunk has been loaded from storage
|
|
return CompletableFuture.completedFuture(chunk);
|
|
} else {
|
|
// Loader couldn't load the chunk, generate it
|
|
return createChunk(chunkX, chunkZ);
|
|
}
|
|
})
|
|
// cache the retrieved chunk
|
|
.thenAccept(chunk -> {
|
|
if (Thread.currentThread() instanceof TickSchedulerThread) {
|
|
postLoadCallback.accept(chunk); // Already running in instance tick thread
|
|
} else {
|
|
scheduleNextTick(ignored -> postLoadCallback.accept(chunk));
|
|
}
|
|
})
|
|
.exceptionally(throwable -> {
|
|
MinecraftServer.getExceptionManager().handleException(throwable);
|
|
return null;
|
|
});
|
|
if (loader.supportsParallelLoading()) {
|
|
CompletableFuture.runAsync(retriever);
|
|
} else {
|
|
retriever.run();
|
|
}
|
|
return completableFuture;
|
|
}
|
|
|
|
Map<Long, List<GeneratorImpl.SectionModifierImpl>> generationForks = new ConcurrentHashMap<>();
|
|
|
|
protected @NotNull CompletableFuture<@NotNull Chunk> createChunk(int chunkX, int chunkZ) {
|
|
final Chunk chunk = chunkSupplier.createChunk(this, chunkX, chunkZ);
|
|
Check.notNull(chunk, "Chunks supplied by a ChunkSupplier cannot be null.");
|
|
Generator generator = generator();
|
|
if (generator != null && chunk.shouldGenerate()) {
|
|
CompletableFuture<Chunk> resultFuture = new CompletableFuture<>();
|
|
// TODO: virtual thread once Loom is available
|
|
ForkJoinPool.commonPool().submit(() -> {
|
|
var chunkUnit = GeneratorImpl.chunk(chunk);
|
|
try {
|
|
// Generate block/biome palette
|
|
generator.generate(chunkUnit);
|
|
// Apply nbt/handler
|
|
if (chunkUnit.modifier() instanceof GeneratorImpl.AreaModifierImpl chunkModifier) {
|
|
for (var section : chunkModifier.sections()) {
|
|
if (section.modifier() instanceof GeneratorImpl.SectionModifierImpl sectionModifier) {
|
|
applyGenerationData(chunk, sectionModifier);
|
|
}
|
|
}
|
|
}
|
|
// Register forks or apply locally
|
|
for (var fork : chunkUnit.forks()) {
|
|
var sections = ((GeneratorImpl.AreaModifierImpl) fork.modifier()).sections();
|
|
for (var section : sections) {
|
|
if (section.modifier() instanceof GeneratorImpl.SectionModifierImpl sectionModifier) {
|
|
if (sectionModifier.blockPalette().count() == 0)
|
|
continue;
|
|
final Point start = section.absoluteStart();
|
|
final Chunk forkChunk = start.chunkX() == chunkX && start.chunkZ() == chunkZ ? chunk : getChunkAt(start);
|
|
if (forkChunk != null) {
|
|
applyFork(forkChunk, sectionModifier);
|
|
// Update players
|
|
forkChunk.invalidate();
|
|
forkChunk.sendChunk();
|
|
} else {
|
|
final long index = ChunkUtils.getChunkIndex(start);
|
|
this.generationForks.compute(index, (i, sectionModifiers) -> {
|
|
if (sectionModifiers == null) sectionModifiers = new ArrayList<>();
|
|
sectionModifiers.add(sectionModifier);
|
|
return sectionModifiers;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Apply awaiting forks
|
|
processFork(chunk);
|
|
} catch (Throwable e) {
|
|
MinecraftServer.getExceptionManager().handleException(e);
|
|
} finally {
|
|
// End generation
|
|
refreshLastBlockChangeTime();
|
|
resultFuture.complete(chunk);
|
|
}
|
|
});
|
|
return resultFuture;
|
|
} else {
|
|
// No chunk generator, execute the callback with the empty chunk
|
|
processFork(chunk);
|
|
return CompletableFuture.completedFuture(chunk);
|
|
}
|
|
}
|
|
|
|
private void processFork(Chunk chunk) {
|
|
this.generationForks.compute(ChunkUtils.getChunkIndex(chunk), (aLong, sectionModifiers) -> {
|
|
if (sectionModifiers != null) {
|
|
for (var sectionModifier : sectionModifiers) {
|
|
applyFork(chunk, sectionModifier);
|
|
}
|
|
}
|
|
return null;
|
|
});
|
|
}
|
|
|
|
private void applyFork(Chunk chunk, GeneratorImpl.SectionModifierImpl sectionModifier) {
|
|
synchronized (chunk) {
|
|
Section section = chunk.getSectionAt(sectionModifier.start().blockY());
|
|
Palette currentBlocks = section.blockPalette();
|
|
// -1 is necessary because forked units handle explicit changes by changing AIR 0 to 1
|
|
sectionModifier.blockPalette().getAllPresent((x, y, z, value) -> currentBlocks.set(x, y, z, value - 1));
|
|
applyGenerationData(chunk, sectionModifier);
|
|
}
|
|
}
|
|
|
|
private void applyGenerationData(Chunk chunk, GeneratorImpl.SectionModifierImpl section) {
|
|
var cache = section.cache();
|
|
if (cache.isEmpty()) return;
|
|
final int height = section.start().blockY();
|
|
synchronized (chunk) {
|
|
Int2ObjectMaps.fastForEach(cache, blockEntry -> {
|
|
final int index = blockEntry.getIntKey();
|
|
final Block block = blockEntry.getValue();
|
|
final int x = ChunkUtils.blockIndexToChunkPositionX(index);
|
|
final int y = ChunkUtils.blockIndexToChunkPositionY(index) + height;
|
|
final int z = ChunkUtils.blockIndexToChunkPositionZ(index);
|
|
chunk.setBlock(x, y, z, block);
|
|
});
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void enableAutoChunkLoad(boolean enable) {
|
|
this.autoChunkLoad = enable;
|
|
}
|
|
|
|
@Override
|
|
public boolean hasEnabledAutoChunkLoad() {
|
|
return autoChunkLoad;
|
|
}
|
|
|
|
@Override
|
|
public boolean isInVoid(@NotNull Point point) {
|
|
// TODO: more customizable
|
|
return point.y() < getDimensionType().getMinY() - 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)}
|
|
* 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
|
|
*/
|
|
@Override
|
|
public void setChunkSupplier(@NotNull ChunkSupplier chunkSupplier) {
|
|
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(Instance, int, int)},
|
|
* {@link UUID} is randomized and {@link DimensionType} is passed over.
|
|
*
|
|
* @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());
|
|
copiedInstance.srcInstance = this;
|
|
copiedInstance.tagHandler = this.tagHandler.copy();
|
|
copiedInstance.lastBlockChangeTime = this.lastBlockChangeTime;
|
|
for (Chunk chunk : chunks.values()) {
|
|
final int chunkX = chunk.getChunkX();
|
|
final int chunkZ = chunk.getChunkZ();
|
|
final Chunk copiedChunk = chunk.copy(copiedInstance, chunkX, chunkZ);
|
|
copiedInstance.cacheChunk(copiedChunk);
|
|
}
|
|
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
|
|
*/
|
|
public @Nullable 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();
|
|
}
|
|
|
|
@Override
|
|
public @Nullable Generator generator() {
|
|
return generator;
|
|
}
|
|
|
|
@Override
|
|
public void setGenerator(@Nullable Generator generator) {
|
|
this.generator = generator;
|
|
}
|
|
|
|
/**
|
|
* Gets all the instance chunks.
|
|
*
|
|
* @return the chunks of this instance
|
|
*/
|
|
@Override
|
|
public @NotNull Collection<@NotNull Chunk> getChunks() {
|
|
return chunks.values();
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
@Override
|
|
public void tick(long time) {
|
|
// Time/world border
|
|
super.tick(time);
|
|
// Clear block change map
|
|
Lock wrlock = this.changingBlockLock;
|
|
wrlock.lock();
|
|
this.currentlyChangingBlocks.clear();
|
|
wrlock.unlock();
|
|
}
|
|
|
|
/**
|
|
* 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 block the block
|
|
* @return true if the block changed since the last update
|
|
*/
|
|
private boolean isAlreadyChanged(@NotNull Point blockPosition, @NotNull Block block) {
|
|
final Block changedBlock = currentlyChangingBlocks.get(blockPosition);
|
|
return Objects.equals(changedBlock, block);
|
|
}
|
|
|
|
/**
|
|
* 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(@NotNull Point blockPosition, int updateDistance) {
|
|
ChunkCache cache = new ChunkCache(this, null, null);
|
|
for (var updateFace : BLOCK_UPDATE_FACES) {
|
|
var direction = updateFace.toDirection();
|
|
final int neighborX = blockPosition.blockX() + direction.normalX();
|
|
final int neighborY = blockPosition.blockY() + direction.normalY();
|
|
final int neighborZ = blockPosition.blockZ() + direction.normalZ();
|
|
if (neighborY < getDimensionType().getMinY() || neighborY > getDimensionType().getTotalHeight())
|
|
continue;
|
|
final Block neighborBlock = cache.getBlock(neighborX, neighborY, neighborZ, Condition.TYPE);
|
|
if (neighborBlock == null)
|
|
continue;
|
|
final BlockPlacementRule neighborBlockPlacementRule = MinecraftServer.getBlockManager().getBlockPlacementRule(neighborBlock);
|
|
if (neighborBlockPlacementRule == null || updateDistance >= neighborBlockPlacementRule.maxUpdateDistance()) continue;
|
|
|
|
final Vec neighborPosition = new Vec(neighborX, neighborY, neighborZ);
|
|
final Block newNeighborBlock = neighborBlockPlacementRule.blockUpdate(new BlockPlacementRule.UpdateState(
|
|
this,
|
|
neighborPosition,
|
|
neighborBlock,
|
|
updateFace.getOppositeFace()
|
|
));
|
|
if (neighborBlock != newNeighborBlock) {
|
|
final Chunk chunk = getChunkAt(neighborPosition);
|
|
if (!isLoaded(chunk)) continue;
|
|
UNSAFE_setBlock(chunk, neighborPosition.blockX(), neighborPosition.blockY(), neighborPosition.blockZ(), newNeighborBlock,
|
|
null, null, true, updateDistance + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
private CompletableFuture<Chunk> loadOrRetrieve(int chunkX, int chunkZ, Supplier<CompletableFuture<Chunk>> supplier) {
|
|
final Chunk chunk = getChunk(chunkX, chunkZ);
|
|
if (chunk != null) {
|
|
// Chunk already loaded
|
|
return CompletableFuture.completedFuture(chunk);
|
|
}
|
|
return supplier.get();
|
|
}
|
|
|
|
private void cacheChunk(@NotNull Chunk chunk) {
|
|
this.chunks.put(getChunkIndex(chunk), chunk);
|
|
var dispatcher = MinecraftServer.process().dispatcher();
|
|
dispatcher.createPartition(chunk);
|
|
}
|
|
}
|