Start 'v2' batches with relative chunkbatch and absolute blockbatch

This commit is contained in:
Matt Worzala 2021-01-08 00:42:58 -05:00
parent 27aec6b48e
commit ccaf96b434
5 changed files with 431 additions and 4 deletions

View File

@ -0,0 +1,110 @@
package net.minestom.server.instance.batch.v2;
import net.minestom.server.data.Data;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.InstanceContainer;
import net.minestom.server.utils.chunk.ChunkUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/**
* A Batch which can be used when changes are required across chunk borders,
* but the changes do not need any translation. If translation is required,
* use a {@link RelativeBlockBatch} instead.
* <p>
* Coordinates are relative to the world origin.
*
* @see Batch
* @see RelativeBlockBatch
*/
public class AbsoluteBlockBatch implements Batch<Runnable> {
// In the form of <Chunk Index, Batch>
private final Map<Long, ChunkBatch> data = new HashMap<>();
public AbsoluteBlockBatch() {}
@Override
public void setSeparateBlocks(int x, int y, int z, short blockStateId, short customBlockId, @Nullable Data data) {
int chunkX = ChunkUtils.getChunkCoordinate(x);
int chunkZ = ChunkUtils.getChunkCoordinate(z);
long chunkIndex = ChunkUtils.getChunkIndex(chunkX, chunkZ);
ChunkBatch chunkBatch = this.data.get(chunkIndex);
if (chunkBatch == null)
chunkBatch = new ChunkBatch();
int relativeX = x - (chunkX * Chunk.CHUNK_SIZE_X);
int relativeZ = z - (chunkZ * Chunk.CHUNK_SIZE_Z);
chunkBatch.setSeparateBlocks(relativeX, y, relativeZ, blockStateId, customBlockId, data);
this.data.put(chunkIndex, chunkBatch);
}
@Override
public void clear() {
synchronized (data) {
this.data.clear();
}
}
/**
* Apply this batch to the given instance.
*
* @param instance The instance in which the batch should be applied
* @param callback The callback to be executed when the batch is applied
*/
@Override
public void apply(@NotNull InstanceContainer instance, @Nullable Runnable callback) {
apply(instance, callback, true);
}
/**
* Apply this batch to the given instance, and execute the callback immediately when the
* blocks have been applied, in an unknown thread.
*
* @param instance The instance in which the batch should be applied
* @param callback The callback to be executed when the batch is applied
*/
public void unsafeApply(@NotNull InstanceContainer instance, @Nullable Runnable callback) {
apply(instance, callback, false);
}
/**
* Apply this batch to the given instance, and execute the callback depending on safeCallback.
*
* @param instance The instance in which the batch should be applied
* @param callback The callback to be executed when the batch is applied
* @param safeCallback If true, the callback will be executed in the next instance update. Otherwise it will be executed immediately upon completion
*
*/
protected void apply(@NotNull InstanceContainer instance, @Nullable Runnable callback, boolean safeCallback) {
synchronized (data) {
AtomicInteger counter = new AtomicInteger();
for (Map.Entry<Long, ChunkBatch> entry : data.entrySet()) {
long chunkIndex = entry.getKey();
int chunkX = ChunkUtils.getChunkCoordX(chunkIndex);
int chunkZ = ChunkUtils.getChunkCoordZ(chunkIndex);
ChunkBatch batch = entry.getValue();
batch.apply(instance, chunkX, chunkZ, c -> {
final boolean isLast = counter.incrementAndGet() == data.size();
// Execute the callback if this was the last chunk to process
if (isLast) {
instance.refreshLastBlockChangeTime();
if (callback != null) {
if (safeCallback)
instance.scheduleNextTick(inst -> callback.run());
else callback.run();
}
}
});
}
}
}
}

View File

@ -0,0 +1,75 @@
package net.minestom.server.instance.batch.v2;
import net.minestom.server.MinecraftServer;
import net.minestom.server.data.Data;
import net.minestom.server.instance.BlockModifier;
import net.minestom.server.instance.InstanceContainer;
import net.minestom.server.instance.block.CustomBlock;
import net.minestom.server.utils.thread.MinestomThread;
import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.concurrent.ExecutorService;
/**
* A Batch is a tool used to cache a list of block changes, and apply the changes whenever you want.
* <p>
* Batches offer a performance benefit because clients are not notified of any change until all of
* the blocks have been placed.
* <p>
* All batches may be rotated using {link}, however rotate operations do not mutate the batch, so the
* result should be cached if used multiple times.
* <p>
* If reversal is a desired behavior, batches may be applied in "reversal mode" using {link}. This
* operation will return a new batch with the blocks set to whatever they were before the batch was
* applied.
*
* @see ChunkBatch
* @see AbsoluteBlockBatch
* @see RelativeBlockBatch
*/
public interface Batch<Callback> extends BlockModifier {
ExecutorService BLOCK_BATCH_POOL = new MinestomThread(MinecraftServer.THREAD_COUNT_BLOCK_BATCH, MinecraftServer.THREAD_NAME_BLOCK_BATCH);
@Override
default void setBlockStateId(int x, int y, int z, short blockStateId, @Nullable Data data) {
setSeparateBlocks(x, y, z, blockStateId, (short) 0, data);
}
@Override
default 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!");
setSeparateBlocks((byte) x, y, (byte) z, customBlock.getDefaultBlockStateId(), customBlockId, data);
}
@Override
void setSeparateBlocks(int x, int y, int z, short blockStateId, short customBlockId, @Nullable Data data);
/**
* Removes all block data from this batch.
*/
void clear();
// Batch rotate(? );
/**
* Called to apply the batch to the given instance.
* <p>
* The implementation for all current batches executes the block updates in a dedicated pool,
* and runs the callback on the next instance update after block placement is complete. This
* means that the callback can be called up to 50ms after the blocks have been placed, however,
* it will be called in a determinable thread. If immediate execution of the callback is needed,
* see the unsafeApply method in each implementation.
* <p>
* See the specific batch classes for alternative application methods.
*
* @param instance The instance in which the batch should be applied
* @param callback The callback to be executed when the batch is applied
*/
void apply(@NotNull InstanceContainer instance, @Nullable Callback callback);
// @NotNull
// Batch<Callback> reversableApply(@NotNull InstanceContainer instance, @Nullable Callback callback);
}

View File

@ -0,0 +1,207 @@
package net.minestom.server.instance.batch.v2;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongList;
import net.minestom.server.data.Data;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.InstanceContainer;
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.ChunkUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A Batch used when all of the block changed are contained inside a single chunk.
* If more than one chunk is needed, use an {@link AbsoluteBlockBatch} instead.
* <p>
* The batch can be placed in any chunk in any instance, however it will always remain
* aligned to a chunk border. If completely translatable block changes are needed, use a
* {@link RelativeBlockBatch} instead.
* <p>
* Coordinates are relative to the chunk (0-15) instead of world coordinates.
*
* @see Batch
*/
public class ChunkBatch implements Batch<ChunkCallback> {
private static final Logger LOGGER = LoggerFactory.getLogger(ChunkBatch.class);
// Need to be synchronized manually
// Format: blockIndex/blockStateId/customBlockId (32/16/16 bits)
private final LongList blocks;
// Need to be synchronized manually
// block index - data
private final Int2ObjectMap<Data> blockDataMap;
public ChunkBatch() {
this(new LongArrayList(), new Int2ObjectOpenHashMap<>());
}
protected ChunkBatch(LongList blocks, Int2ObjectMap<Data> blockDataMap) {
this.blocks = blocks;
this.blockDataMap = blockDataMap;
}
@Override
public void setSeparateBlocks(int x, int y, int z, short blockStateId, short customBlockId, @Nullable Data data) {
// Cache the entry to be placed later during flush
final int index = ChunkUtils.getBlockIndex(x, y, z);
long value = index;
value = (value << 16) | blockStateId;
value = (value << 16) | customBlockId;
synchronized (blocks) {
this.blocks.add(value);
}
if (data != null) {
synchronized (blockDataMap) {
this.blockDataMap.put(index, data);
}
}
}
@Override
public void clear() {
synchronized (blocks) {
this.blocks.clear();
}
}
/**
* Apply this batch to chunk (0, 0).
*
* @param instance The instance in which the batch should be applied
* @param callback The callback to be executed when the batch is applied
*/
@Override
public void apply(@NotNull InstanceContainer instance, @Nullable ChunkCallback callback) {
apply(instance, 0, 0, callback);
}
/**
* Apply this batch to the given chunk.
*
* @param instance The instance in which the batch should be applied
* @param chunkX The x chunk coordinate of the target chunk
* @param chunkZ The z chunk coordinate of the target chunk
* @param callback The callback to be executed when the batch is applied.
*/
public void apply(@NotNull InstanceContainer instance, int chunkX, int chunkZ, @Nullable ChunkCallback callback) {
final Chunk chunk = instance.getChunk(chunkX, chunkZ);
if (chunk == null) {
LOGGER.warn("Unable to apply ChunkBatch to unloaded chunk ({}, {}) in {}.", chunkX, chunkZ, instance.getUniqueId());
return;
}
apply(instance, chunk, callback);
}
/**
* Apply this batch to the given chunk.
*
* @param instance The instance in which the batch should be applied
* @param chunk The target chunk
* @param callback The callback to be executed when the batch is applied
*/
public void apply(@NotNull InstanceContainer instance, @NotNull Chunk chunk, @Nullable ChunkCallback callback) {
apply(instance, chunk, callback, true);
}
/**
* Apply this batch to the given chunk, and execute the callback
* immediately when the blocks have been applied, in an unknown thread.
*
* @param instance The instance in which the batch should be applied
* @param chunk The target chunk
* @param callback The callback to be executed when the batch is applied
*/
public void unsafeApply(@NotNull InstanceContainer instance, @NotNull Chunk chunk, @Nullable ChunkCallback callback) {
apply(instance, chunk, callback, false);
}
/**
* Apply this batch to the given chunk, and execute the callback depending on safeCallback.
*
* @param instance The instance in which the batch should be applied
* @param chunk The target chunk
* @param callback The callback to be executed when the batch is applied
* @param safeCallback If true, the callback will be executed in the next instance update. Otherwise it will be executed immediately upon completion
*/
protected void apply(@NotNull InstanceContainer instance, @NotNull Chunk chunk, @Nullable ChunkCallback callback, boolean safeCallback) {
BLOCK_BATCH_POOL.execute(() -> singleThreadFlush(instance, chunk, callback, safeCallback));
}
/**
* Applies this batch in the current thread, executing the callback upon completion.
*/
private void singleThreadFlush(InstanceContainer instance, Chunk chunk, @Nullable ChunkCallback callback, boolean safeCallback) {
if (blocks.isEmpty()) {
OptionalCallback.execute(callback, chunk);
return;
}
if (!chunk.isLoaded()) {
LOGGER.warn("Unable to apply ChunkBatch to unloaded chunk ({}, {}) in {}.", chunk.getChunkX(), chunk.getChunkZ(), instance.getUniqueId());
return;
}
synchronized (blocks) {
for (long block : blocks) {
apply(chunk, block);
}
}
updateChunk(instance, chunk, callback, safeCallback);
}
/**
* Applies a single block change given a chunk and a value in the described format.
*
* @param chunk The chunk to apply the change
* @param value block index|state id|custom block id (32|16|16 bits)
*/
private void apply(@NotNull Chunk chunk, long value) {
final short customBlockId = (short) (value & 0xFFFF);
final short blockId = (short) ((value >> 16) & 0xFFFF);
final int index = (int) ((value >> 32) & 0xFFFFFFFFL);
Data data = null;
if (!blockDataMap.isEmpty()) {
synchronized (blockDataMap) {
data = blockDataMap.get(index);
}
}
chunk.UNSAFE_setBlock(ChunkUtils.blockIndexToChunkPositionX(index),
ChunkUtils.blockIndexToChunkPositionY(index),
ChunkUtils.blockIndexToChunkPositionZ(index),
blockId, customBlockId, data, CustomBlockUtils.hasUpdate(customBlockId));
}
/**
* Updates the given chunk for all of its viewers, and executes the callback.
*/
private void updateChunk(InstanceContainer instance, Chunk chunk, @Nullable ChunkCallback callback, boolean safeCallback) {
// Refresh chunk for viewers
// Formerly this had an option to do a Chunk#sendChunkUpdate
// however Chunk#sendChunk does the same including a light update
chunk.sendChunk();
instance.refreshLastBlockChangeTime();
if (callback != null) {
if (safeCallback) {
instance.scheduleNextTick(inst -> callback.accept(chunk));
} else {
callback.accept(chunk);
}
}
}
}

View File

@ -0,0 +1,4 @@
package net.minestom.server.instance.batch.v2;
public class RelativeBlockBatch {
}

View File

@ -1,5 +1,6 @@
package demo.commands;
import net.minestom.server.MinecraftServer;
import net.minestom.server.chat.ChatColor;
import net.minestom.server.chat.ColoredText;
import net.minestom.server.command.CommandSender;
@ -7,7 +8,10 @@ import net.minestom.server.command.builder.Arguments;
import net.minestom.server.command.builder.Command;
import net.minestom.server.entity.Player;
import net.minestom.server.instance.InstanceContainer;
import net.minestom.server.instance.batch.BlockBatch;
import net.minestom.server.instance.batch.v2.AbsoluteBlockBatch;
import net.minestom.server.instance.batch.v2.ChunkBatch;
import net.minestom.server.instance.block.Block;
import net.minestom.server.utils.time.TimeUnit;
import java.util.concurrent.ThreadLocalRandom;
@ -25,18 +29,45 @@ public class CubeBatchCommand extends Command {
return;
}
Player player = sender.asPlayer();
InstanceContainer instance = (InstanceContainer) player.getInstance();
BlockBatch batch = new BlockBatch((InstanceContainer) player.getInstance());
// applyChunkShape(instance);
AbsoluteBlockBatch batch = new AbsoluteBlockBatch();
int offset = 50;
for (int x = 0; x < 50; x += 2) {
for (int y = 0; y < 50; y += 2) {
for (int z = 0; z < 50; z += 2) {
batch.setBlockStateId(x + offset, y + offset, z + offset, (short) ThreadLocalRandom.current().nextInt(500));
batch.setBlockStateId(x + offset, y + offset, z + offset, Block.STONE.getBlockId());
}
}
}
batch.flush(() -> sender.sendMessage(ColoredText.of(ChatColor.BRIGHT_GREEN, "Created cube.")));
batch.apply(instance, () -> sender.sendMessage(ColoredText.of(ChatColor.BRIGHT_GREEN, "Created cube.")));
}
private void applyChunkShape(InstanceContainer instance) {
for (int i = 0; i < 20; i++) {
final ChunkBatch relBatch = new ChunkBatch();
for (int x = 0; x < 16; x += 2) {
for (int y = 0; y < 50; y += 2) {
for (int z = 0; z < 16; z += 2) {
relBatch.setBlockStateId(x, y + 50, z, (short) i);
}
}
}
MinecraftServer.getSchedulerManager().buildTask(() -> {
relBatch.apply(instance,
ThreadLocalRandom.current().nextInt(10) - 5,
ThreadLocalRandom.current().nextInt(10) - 5,
null);
}).delay(10, TimeUnit.TICK).repeat(1, TimeUnit.TICK).schedule();
}
}
}