bring back BatchOption, batch inversion start

This commit is contained in:
Matt Worzala 2021-01-08 18:59:06 -05:00
parent d5a53641ba
commit 5a9e393ae2
7 changed files with 249 additions and 65 deletions

View File

@ -10,6 +10,7 @@ import net.minestom.server.utils.chunk.ChunkUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
@ -27,7 +28,20 @@ public class AbsoluteBlockBatch implements Batch<Runnable> {
// In the form of <Chunk Index, Batch>
private final Long2ObjectMap<ChunkBatch> chunkBatchesMap = new Long2ObjectOpenHashMap<>();
private final CountDownLatch readyLatch;
private final BatchOption options;
public AbsoluteBlockBatch() {
this(new BatchOption());
}
public AbsoluteBlockBatch(BatchOption options) {
this(options, true);
}
private AbsoluteBlockBatch(BatchOption options, boolean ready) {
this.readyLatch = new CountDownLatch(ready ? 0 : 1);
this.options = options;
}
@Override
@ -38,7 +52,7 @@ public class AbsoluteBlockBatch implements Batch<Runnable> {
final ChunkBatch chunkBatch;
synchronized (chunkBatchesMap) {
chunkBatch = chunkBatchesMap.computeIfAbsent(chunkIndex, i -> new ChunkBatch());
chunkBatch = chunkBatchesMap.computeIfAbsent(chunkIndex, i -> new ChunkBatch(this.options));
}
final int relativeX = x - (chunkX * Chunk.CHUNK_SIZE_X);
@ -53,6 +67,20 @@ public class AbsoluteBlockBatch implements Batch<Runnable> {
}
}
@Override
public boolean isReady() {
return this.readyLatch.getCount() == 0;
}
@Override
public void awaitReady() {
try {
this.readyLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException("#awaitReady interrupted!", e);
}
}
/**
* Applies this batch to the given instance.
*
@ -60,8 +88,8 @@ public class AbsoluteBlockBatch implements Batch<Runnable> {
* @param callback The callback to be executed when the batch is applied
*/
@Override
public void apply(@NotNull Instance instance, @Nullable Runnable callback) {
apply(instance, callback, true);
public AbsoluteBlockBatch apply(@NotNull Instance instance, @Nullable Runnable callback) {
return apply(instance, callback, true);
}
/**
@ -71,8 +99,8 @@ public class AbsoluteBlockBatch implements Batch<Runnable> {
* @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 Instance instance, @Nullable Runnable callback) {
apply(instance, callback, false);
public AbsoluteBlockBatch unsafeApply(@NotNull Instance instance, @Nullable Runnable callback) {
return apply(instance, callback, false);
}
/**
@ -83,7 +111,10 @@ public class AbsoluteBlockBatch implements Batch<Runnable> {
* @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 Instance instance, @Nullable Runnable callback, boolean safeCallback) {
protected AbsoluteBlockBatch apply(@NotNull Instance instance, @Nullable Runnable callback, boolean safeCallback) {
if (!this.options.isUnsafeApply()) this.awaitReady();
final AbsoluteBlockBatch inverse = this.options.shouldCalculateInverse() ? new AbsoluteBlockBatch() : null;
synchronized (chunkBatchesMap) {
AtomicInteger counter = new AtomicInteger();
for (Long2ObjectMap.Entry<ChunkBatch> entry : chunkBatchesMap.long2ObjectEntrySet()) {
@ -92,11 +123,12 @@ public class AbsoluteBlockBatch implements Batch<Runnable> {
final int chunkZ = ChunkUtils.getChunkCoordZ(chunkIndex);
final ChunkBatch batch = entry.getValue();
batch.apply(instance, chunkX, chunkZ, c -> {
ChunkBatch chunkInverse = batch.apply(instance, chunkX, chunkZ, c -> {
final boolean isLast = counter.incrementAndGet() == chunkBatchesMap.size();
// Execute the callback if this was the last chunk to process
if (isLast) {
if (inverse != null) inverse.readyLatch.countDown();
if (instance instanceof InstanceContainer) {
// FIXME: put method in Instance instead
@ -112,7 +144,12 @@ public class AbsoluteBlockBatch implements Batch<Runnable> {
}
}
});
if (inverse != null)
inverse.chunkBatchesMap.put(chunkIndex, chunkInverse);
}
}
return inverse;
}
}

View File

@ -48,6 +48,22 @@ public interface Batch<Callback> extends BlockModifier {
@Override
void setSeparateBlocks(int x, int y, int z, short blockStateId, short customBlockId, @Nullable Data data);
/**
* Gets if the batch is ready to be applied to an instance.
* <p>
* This is true by default, and will only be false while a reversal is being generated.
*
* @return true if the batch is ready to apply
*/
default boolean isReady() { return true; }
/**
* Blocks the current thread until the batch is ready to be applied.
*
* @see #isReady() for a non-blocking way to determine if the batch is ready
*/
default void awaitReady() {}
/**
* Removes all block data from this batch.
*/
@ -68,9 +84,8 @@ public interface Batch<Callback> extends BlockModifier {
*
* @param instance The instance in which the batch should be applied
* @param callback The callback to be executed when the batch is applied
* @return The inverse of this batch, if inverse is enabled in the {@link BatchOption}
*/
void apply(@NotNull Instance instance, @Nullable Callback callback);
// @NotNull
// Batch<Callback> reversableApply(@NotNull InstanceContainer instance, @Nullable Callback callback);
@Nullable
Batch<Callback> apply(@NotNull Instance instance, @Nullable Callback callback);
}

View File

@ -3,8 +3,9 @@ package net.minestom.server.instance.batch;
import org.jetbrains.annotations.NotNull;
public class BatchOption {
private boolean fullChunk = false;
private boolean calculateInverse = false;
private boolean unsafeApply = false;
public BatchOption() {
}
@ -20,6 +21,14 @@ public class BatchOption {
return fullChunk;
}
public boolean shouldCalculateInverse() {
return calculateInverse;
}
public boolean isUnsafeApply() {
return this.unsafeApply;
}
/**
* @param fullChunk true to make this batch composes the whole chunk
* @return 'this' for chaining
@ -30,4 +39,16 @@ public class BatchOption {
this.fullChunk = fullChunk;
return this;
}
@NotNull
public BatchOption setCalculateInverse(boolean calculateInverse) {
this.calculateInverse = calculateInverse;
return this;
}
@NotNull
public BatchOption setUnsafeApply(boolean unsafeApply) {
this.unsafeApply = unsafeApply;
return this;
}
}

View File

@ -17,6 +17,8 @@ import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.CountDownLatch;
/**
* 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.
@ -40,13 +42,27 @@ public class ChunkBatch implements Batch<ChunkCallback> {
// block index - data
private final Int2ObjectMap<Data> blockDataMap;
private final CountDownLatch readyLatch;
private final BatchOption options;
public ChunkBatch() {
this(new LongArrayList(), new Int2ObjectOpenHashMap<>());
this(new BatchOption());
}
protected ChunkBatch(LongList blocks, Int2ObjectMap<Data> blockDataMap) {
public ChunkBatch(BatchOption options) {
this(new LongArrayList(), new Int2ObjectOpenHashMap<>(), options);
}
protected ChunkBatch(LongList blocks, Int2ObjectMap<Data> blockDataMap, BatchOption options) {
this(blocks, blockDataMap, options, true);
}
private ChunkBatch(LongList blocks, Int2ObjectMap<Data> blockDataMap, BatchOption options, boolean ready) {
this.blocks = blocks;
this.blockDataMap = blockDataMap;
this.readyLatch = new CountDownLatch(ready ? 0 : 1);
this.options = options;
}
@Override
@ -75,6 +91,20 @@ public class ChunkBatch implements Batch<ChunkCallback> {
}
}
@Override
public boolean isReady() {
return this.readyLatch.getCount() == 0;
}
@Override
public void awaitReady() {
try {
this.readyLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException("#awaitReady interrupted!", e);
}
}
/**
* Apply this batch to chunk (0, 0).
*
@ -82,8 +112,8 @@ public class ChunkBatch implements Batch<ChunkCallback> {
* @param callback The callback to be executed when the batch is applied
*/
@Override
public void apply(@NotNull Instance instance, @Nullable ChunkCallback callback) {
apply(instance, 0, 0, callback);
public ChunkBatch apply(@NotNull Instance instance, @Nullable ChunkCallback callback) {
return apply(instance, 0, 0, callback);
}
/**
@ -94,14 +124,14 @@ public class ChunkBatch implements Batch<ChunkCallback> {
* @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 Instance instance, int chunkX, int chunkZ, @Nullable ChunkCallback callback) {
public ChunkBatch apply(@NotNull Instance 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;
return null;
}
apply(instance, chunk, callback);
return apply(instance, chunk, callback);
}
/**
@ -111,8 +141,8 @@ public class ChunkBatch implements Batch<ChunkCallback> {
* @param chunk The target chunk
* @param callback The callback to be executed when the batch is applied
*/
public void apply(@NotNull Instance instance, @NotNull Chunk chunk, @Nullable ChunkCallback callback) {
apply(instance, chunk, callback, true);
public ChunkBatch apply(@NotNull Instance instance, @NotNull Chunk chunk, @Nullable ChunkCallback callback) {
return apply(instance, chunk, callback, true);
}
/**
@ -123,8 +153,8 @@ public class ChunkBatch implements Batch<ChunkCallback> {
* @param chunk The target chunk
* @param callback The callback to be executed when the batch is applied
*/
public void unsafeApply(@NotNull Instance instance, @NotNull Chunk chunk, @Nullable ChunkCallback callback) {
apply(instance, chunk, callback, false);
public ChunkBatch unsafeApply(@NotNull Instance instance, @NotNull Chunk chunk, @Nullable ChunkCallback callback) {
return apply(instance, chunk, callback, false);
}
/**
@ -136,36 +166,44 @@ public class ChunkBatch implements Batch<ChunkCallback> {
* @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 Instance instance,
protected ChunkBatch apply(@NotNull Instance instance,
@NotNull Chunk chunk, @Nullable ChunkCallback callback,
boolean safeCallback) {
BLOCK_BATCH_POOL.execute(() -> singleThreadFlush(instance, chunk, callback, safeCallback));
if (!this.options.isUnsafeApply()) this.awaitReady();
final ChunkBatch inverse = this.options.shouldCalculateInverse() ? new ChunkBatch(new LongArrayList(), new Int2ObjectOpenHashMap<>(), options, false) : null;
BLOCK_BATCH_POOL.execute(() -> singleThreadFlush(instance, chunk, inverse, callback, safeCallback));
return inverse;
}
/**
* Applies this batch in the current thread, executing the callback upon completion.
*/
private void singleThreadFlush(Instance 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);
private void singleThreadFlush(Instance instance, Chunk chunk, @Nullable ChunkBatch inverse,
@Nullable ChunkCallback callback, boolean safeCallback) {
try {
if (blocks.isEmpty()) {
OptionalCallback.execute(callback, chunk);
return;
}
}
updateChunk(instance, chunk, callback, safeCallback);
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, inverse);
}
}
if (inverse != null) inverse.readyLatch.countDown();
updateChunk(instance, chunk, callback, safeCallback);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
@ -174,7 +212,7 @@ public class ChunkBatch implements Batch<ChunkCallback> {
* @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) {
private void apply(@NotNull Chunk chunk, long value, @Nullable ChunkBatch inverse) {
final short customBlockId = (short) (value & 0xFFFF);
final short blockId = (short) ((value >> 16) & 0xFFFF);
final int index = (int) ((value >> 32) & 0xFFFFFFFFL);
@ -186,10 +224,14 @@ public class ChunkBatch implements Batch<ChunkCallback> {
}
}
chunk.UNSAFE_setBlock(ChunkUtils.blockIndexToChunkPositionX(index),
ChunkUtils.blockIndexToChunkPositionY(index),
ChunkUtils.blockIndexToChunkPositionZ(index),
blockId, customBlockId, data, CustomBlockUtils.hasUpdate(customBlockId));
final int x = ChunkUtils.blockIndexToChunkPositionX(index);
final int y = ChunkUtils.blockIndexToChunkPositionY(index);
final int z = ChunkUtils.blockIndexToChunkPositionZ(index);
if (inverse != null)
inverse.setSeparateBlocks(x, y, z, chunk.getBlockStateId(x, y, z), chunk.getCustomBlockId(x, y, z), chunk.getBlockData(index));
chunk.UNSAFE_setBlock(x, y, z, blockId, customBlockId, data, CustomBlockUtils.hasUpdate(customBlockId));
}
/**

View File

@ -14,7 +14,7 @@ public class ChunkGenerationBatch extends ChunkBatch {
private final Chunk chunk;
public ChunkGenerationBatch(InstanceContainer instance, Chunk chunk) {
super(null, null);
super(null, null, new BatchOption());
this.instance = instance;
this.chunk = chunk;
@ -54,9 +54,7 @@ public class ChunkGenerationBatch extends ChunkBatch {
}
@Override
protected void apply(@NotNull Instance instance,
@NotNull Chunk chunk, @Nullable ChunkCallback callback,
boolean safeCallback) {
protected ChunkBatch apply(@NotNull Instance instance, @NotNull Chunk chunk, @Nullable ChunkCallback callback, boolean safeCallback) {
throw new IllegalStateException("#apply is not supported for chunk generation batch.");
}
}

View File

@ -22,9 +22,19 @@ public class RelativeBlockBatch implements Batch<Runnable> {
// relative pos - data
private final Long2ObjectMap<Data> blockDataMap = new Long2ObjectOpenHashMap<>();
private final BatchOption options;
private volatile boolean firstEntry = true;
private int offsetX, offsetY, offsetZ;
public RelativeBlockBatch() {
this(new BatchOption());
}
public RelativeBlockBatch(BatchOption options) {
this.options = options;
}
@Override
public void setSeparateBlocks(int x, int y, int z, short blockStateId, short customBlockId, @Nullable Data data) {
@ -72,25 +82,34 @@ public class RelativeBlockBatch implements Batch<Runnable> {
}
@Override
public void apply(@NotNull Instance instance, @Nullable Runnable callback) {
apply(instance, 0, 0, 0, callback);
public AbsoluteBlockBatch apply(@NotNull Instance instance, @Nullable Runnable callback) {
return apply(instance, 0, 0, 0, callback);
}
public void apply(@NotNull Instance instance, @NotNull BlockPosition position, @Nullable Runnable callback) {
apply(instance, position.getX(), position.getY(), position.getZ(), callback);
public AbsoluteBlockBatch apply(@NotNull Instance instance, @NotNull BlockPosition position, @Nullable Runnable callback) {
return apply(instance, position.getX(), position.getY(), position.getZ(), callback);
}
public void apply(@NotNull Instance instance, int x, int y, int z, @Nullable Runnable callback) {
apply(instance, x, y, z, callback, true);
public AbsoluteBlockBatch apply(@NotNull Instance instance, int x, int y, int z, @Nullable Runnable callback) {
return apply(instance, x, y, z, callback, true);
}
public void applyUnsafe(@NotNull Instance instance, int x, int y, int z, @Nullable Runnable callback) {
apply(instance, x, y, z, callback, false);
public AbsoluteBlockBatch applyUnsafe(@NotNull Instance instance, int x, int y, int z, @Nullable Runnable callback) {
return apply(instance, x, y, z, callback, false);
}
protected void apply(@NotNull Instance instance, int x, int y, int z, @Nullable Runnable callback, boolean safeCallback) {
AbsoluteBlockBatch batch = new AbsoluteBlockBatch();
protected AbsoluteBlockBatch apply(@NotNull Instance instance, int x, int y, int z, @Nullable Runnable callback, boolean safeCallback) {
return this.toAbsoluteBatch(x, y, z).apply(instance, callback, safeCallback);
}
@NotNull
public AbsoluteBlockBatch toAbsoluteBatch() {
return toAbsoluteBatch(0, 0, 0);
}
@NotNull
public AbsoluteBlockBatch toAbsoluteBatch(int x, int y, int z) {
final AbsoluteBlockBatch batch = new AbsoluteBlockBatch(this.options);
synchronized (blockIdMap) {
for (Long2IntMap.Entry entry : blockIdMap.long2IntEntrySet()) {
final long pos = entry.getLongKey();
@ -116,7 +135,6 @@ public class RelativeBlockBatch implements Batch<Runnable> {
batch.setSeparateBlocks(finalX, finalY, finalZ, blockStateId, customBlockId, data);
}
}
batch.apply(instance, callback, safeCallback);
return batch;
}
}

View File

@ -6,22 +6,31 @@ import net.minestom.server.chat.ColoredText;
import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.Arguments;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.arguments.Argument;
import net.minestom.server.command.builder.arguments.ArgumentType;
import net.minestom.server.command.builder.arguments.ArgumentWord;
import net.minestom.server.entity.Player;
import net.minestom.server.instance.InstanceContainer;
import net.minestom.server.instance.batch.AbsoluteBlockBatch;
import net.minestom.server.instance.batch.BatchOption;
import net.minestom.server.instance.batch.ChunkBatch;
import net.minestom.server.instance.batch.RelativeBlockBatch;
import net.minestom.server.instance.block.Block;
import net.minestom.server.utils.time.TimeUnit;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicReference;
public class CubeBatchCommand extends Command {
public CubeBatchCommand() {
super("cube");
Argument<String> subcommand = ArgumentType.Word("sub").from("create", "undo");
setDefaultExecutor(this::execute);
addSyntax(this::handleSubcommand, subcommand);
}
private void execute(CommandSender sender, Arguments args) {
@ -59,6 +68,50 @@ public class CubeBatchCommand extends Command {
}
private volatile ChunkBatch inverse = null;
private volatile AbsoluteBlockBatch absInverse = null;
private void handleSubcommand(CommandSender sender, Arguments args) {
if (!sender.isPlayer()) {
sender.sendMessage("This command may only be run by players.");
return;
}
Player player = sender.asPlayer();
InstanceContainer instance = (InstanceContainer) player.getInstance();
String sub = args.getWord("sub");
if (sub.equalsIgnoreCase("create")) {
final ChunkBatch batch = new ChunkBatch(new BatchOption().setCalculateInverse(true));
for (int x = 0; x < 16; x += 2) {
for (int y = 0; y < 50; y += 2) {
for (int z = 0; z < 16; z += 2) {
batch.setBlockStateId(x, y + 50, z, Block.STONE.getBlockId());
}
}
}
inverse = batch.apply(instance, 1, 1, c -> sender.sendMessage("Applied batch"));
final AbsoluteBlockBatch absBatch = new AbsoluteBlockBatch(new BatchOption().setCalculateInverse(true));
for (int x = 0; x < 50; x += 2) {
for (int y = 0; y < 50; y += 2) {
for (int z = 0; z < 50; z += 2) {
absBatch.setBlockStateId(x - 100, y + 50, z + 50, Block.STONE.getBlockId());
}
}
}
absInverse = absBatch.apply(instance, () -> sender.sendMessage("Applied batch 2"));
} else if (sub.equalsIgnoreCase("undo")) {
final ChunkBatch inv = inverse;
if (inv == null) {
sender.sendMessage("No inverse set.");
return;
}
inv.apply(instance, 1, 1, c -> sender.sendMessage("Applied inverse"));
absInverse.apply(instance, () -> sender.sendMessage("Applied inverse 2"));
} else sender.sendMessage("Unknown subcommand '" + sub + "'");
}
private void applyBlockShape(InstanceContainer instance) {
for (int i = 0; i < 5; i++) {
AbsoluteBlockBatch batch = new AbsoluteBlockBatch();