+ * This has a defined order based on the order of the bits in the internal encoding. It is not guaranteed to be stable + * between Minecraft versions, but it is stable within a single one. + *
+ */ +public final class ChunkSectionPosSet { + private static int index(int x, int y, int z) { + // Each value is 0-15, so 4 bits + // NOTE: This encoding specifically matches the encoding used by SectionPos in Minecraft, do not change unless they do + return (x << 8) | (z << 4) | y; + } + + @FunctionalInterface + public interface PosConsumer { + void apply(int x, int y, int z); + } + + private final BitSet mask = new BitSet(4096); + + public void set(int x, int y, int z) { + mask.set(index(x, y, z)); + } + + public void forEach(PosConsumer consumer) { + for (int i = mask.nextSetBit(0); i >= 0; i = mask.nextSetBit(i + 1)) { + consumer.apply((i >> 8) & 0xF, i & 0xF, (i >> 4) & 0xF); + } + } + + /** + * {@return a view of this set as a short collection} These shorts match those used by {@code SectionPos}. + */ + public ShortCollection asSectionPosEncodedShorts() { + return new AbstractShortCollection() { + @Override + public ShortIterator iterator() { + return new ShortIterator() { + private int next = mask.nextSetBit(0); + + @Override + public short nextShort() { + if (!hasNext()) { + throw new IllegalStateException(); + } + // Uses the fact that we share the encoding with SectionPos to efficiently map + short value = (short) next; + next = mask.nextSetBit(next + 1); + return value; + } + + @Override + public boolean hasNext() { + return next >= 0; + } + }; + } + + @Override + public int size() { + return mask.cardinality(); + } + }; + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/wna/NativeAdapter.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/wna/NativeAdapter.java new file mode 100644 index 0000000000..20c17dd920 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/wna/NativeAdapter.java @@ -0,0 +1,34 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q+ * Mapped onto the platform representation of a chunk section. However, unlike the platform representation, this + * interface is not thread-safe, as it is intended to be used in a single-threaded context. + * Internally this uses the platform representation's unsafe methods. + *
+ */ +public interface NativeChunkSection { + /** + * Set a block in the section. + * + * @param i the x-coordinate, 0-15 + * @param j the y-coordinate, 0-15 + * @param k the z-coordinate, 0-15 + * @param blockState the block state + * @return the old block state + */ + NativeBlockState getThenSetBlock(int i, int j, int k, NativeBlockState blockState); + + /** + * Get a block in the section. + * + * @param i the x-coordinate, 0-15 + * @param j the y-coordinate, 0-15 + * @param k the z-coordinate, 0-15 + * @return the block state + */ + NativeBlockState getBlock(int i, int j, int k); + + /** + * Get if this section is made of only air (specifically, {@link BlockMaterial#isAir()}). + * + * @return true if the section is only air + */ + boolean isOnlyAir(); + + /** + * Copy the section. + * + * @return the copy + */ + NativeChunkSection copy(); +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/wna/NativePosition.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/wna/NativePosition.java new file mode 100644 index 0000000000..02ae2ebcb7 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/wna/NativePosition.java @@ -0,0 +1,31 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q+ * See NeoForge's Level.markAndNotifyBlock + *
+ */ + public static void markAndNotifyBlock( + NativeWorld nativeWorld, NativePosition pos, NativeChunk chunk, NativeBlockState oldState, NativeBlockState newState, + SideEffectSet sideEffectSet + ) { + // Removed redundant branches + + if (chunk.isTicking()) { + if (sideEffectSet.shouldApply(SideEffect.ENTITY_AI)) { + nativeWorld.notifyBlockUpdate(pos, oldState, newState); + } else if (sideEffectSet.shouldApply(SideEffect.NETWORK)) { + // If we want to skip entity AI, just mark the block for sending + nativeWorld.markBlockChanged(pos); + } + } + + if (sideEffectSet.shouldApply(SideEffect.NEIGHBORS)) { + nativeWorld.notifyNeighbors(pos, oldState, newState, sideEffectSet.shouldApply(SideEffect.EVENTS)); + } + + // Make connection updates optional + if (sideEffectSet.shouldApply(SideEffect.NEIGHBORS)) { + nativeWorld.updateNeighbors(pos, oldState, newState, 512, sideEffectSet.shouldApply(SideEffect.EVENTS)); + } + + // Seems used only for PoI updates + if (sideEffectSet.shouldApply(SideEffect.POI_UPDATE)) { + nativeWorld.onBlockStateChange(pos, oldState, newState); + } + } + + /** + * After a chunk section replacement, this function can be called to update the heightmaps, block entities, etc. + * to keep consistency with {@link NativeChunk#setBlockState(NativePosition, NativeBlockState, boolean)}. Doing this allows + * skipping redundant updates caused by multiple set calls, and filtering out unwanted side effects. + * + * @param chunk the chunk + * @param index the replaced section index + * @param oldSection the old section + * @param newSection the new section + * @param modifiedBlocks the set of modified blocks + */ + public static void postChunkSectionReplacement( + NativeChunk chunk, int index, NativeChunkSection oldSection, NativeChunkSection newSection, + ChunkSectionPosSet modifiedBlocks + ) { + modifiedBlocks.forEach((secX, secY, secZ) -> { + NativeBlockState oldState = oldSection.getBlock(secX, secY, secZ); + NativeBlockState newState = newSection.getBlock(secX, secY, secZ); + int chunkY = chunk.getWorld().getYForSectionIndex(index) + secY; + // We skip heightmaps, they're optimized at a higher level to a single call. + + // We skip onRemove here, will call in UPDATE side effect if necessary. + + if (!oldState.isSameBlockType(newState) && oldState.hasBlockEntity()) { + chunk.removeSectionBlockEntity(secX, chunkY, secZ); + } + + // We skip onPlace here, will call in UPDATE side effect if necessary. + + if (newState.hasBlockEntity()) { + chunk.initializeBlockEntity(secX, chunkY, secZ, newState); + } + }); + + boolean wasOnlyAir = oldSection.isOnlyAir(); + boolean onlyAir = newSection.isOnlyAir(); + if (wasOnlyAir != onlyAir) { + chunk.updateLightingForSectionAirChange(index, onlyAir); + } + } + + private WNASharedImpl() { + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/wna/WorldNativeAccess.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/wna/WorldNativeAccess.java deleted file mode 100644 index 52d64cb130..0000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/wna/WorldNativeAccess.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q- * This allows the implementation to branch on the side-effects internally. - *
- * - * @param sideEffectSet the set of side-effects - */ - default void setCurrentSideEffectSet(SideEffectSet sideEffectSet) { - } - - // access functions - - NC getChunk(int x, int z); - - NBS toNative(BlockState state); - - NBS getBlockState(NC chunk, NP position); - - @Nullable - NBS setBlockState(NC chunk, NP position, NBS state); - - NBS getValidBlockForPosition(NBS block, NP position); - - NP getPosition(int x, int y, int z); - - void updateLightingForBlock(NP position); - - boolean updateTileEntity(NP position, LinCompoundTag tag); - - void notifyBlockUpdate(NC chunk, NP position, NBS oldState, NBS newState); - - boolean isChunkTicking(NC chunk); - - void markBlockChanged(NC chunk, NP position); - - void notifyNeighbors(NP pos, NBS oldState, NBS newState); - - default void updateBlock(NP pos, NBS oldState, NBS newState) { - } - - void updateNeighbors(NP pos, NBS oldState, NBS newState, int recursionLimit); - - void onBlockStateChange(NP pos, NBS oldState, NBS newState); - - /** - * This is a heavily modified function stripped from MC to apply WorldEdit-modifications. - * - *- * See Forge's World.markAndNotifyBlock - *
- */ - default void markAndNotifyBlock(NP pos, NC chunk, NBS oldState, NBS newState, SideEffectSet sideEffectSet) { - NBS blockState1 = getBlockState(chunk, pos); - if (blockState1 != newState) { - return; - } - - // Remove redundant branches - if (isChunkTicking(chunk)) { - if (sideEffectSet.shouldApply(SideEffect.ENTITY_AI)) { - notifyBlockUpdate(chunk, pos, oldState, newState); - } else if (sideEffectSet.shouldApply(SideEffect.NETWORK)) { - // If we want to skip entity AI, just mark the block for sending - markBlockChanged(chunk, pos); - } - } - - if (sideEffectSet.shouldApply(SideEffect.NEIGHBORS)) { - notifyNeighbors(pos, oldState, newState); - } - - // Make connection updates optional - if (sideEffectSet.shouldApply(SideEffect.NEIGHBORS)) { - updateNeighbors(pos, oldState, newState, 512); - } - - // Seems used only for PoI updates - if (sideEffectSet.shouldApply(SideEffect.POI_UPDATE)) { - onBlockStateChange(pos, oldState, blockState1); - } - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/wna/package-info.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/wna/package-info.java index 182a404487..c7ea6d325c 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/wna/package-info.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/wna/package-info.java @@ -21,7 +21,8 @@ * "WNA", or WorldEdit Native Access. * *- * Contains internal helper functions for sharing code between platforms. + * Contains internal helper functions for sharing code between platforms. "Native*" interfaces are wrapped around or + * mixed in to the native structures. *
*/ package com.sk89q.worldedit.internal.wna; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/math/BlockVector3.java b/worldedit-core/src/main/java/com/sk89q/worldedit/math/BlockVector3.java index a051594a2c..2332e4bfb9 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/math/BlockVector3.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/math/BlockVector3.java @@ -19,6 +19,7 @@ package com.sk89q.worldedit.math; +import com.sk89q.worldedit.internal.wna.NativePosition; import com.sk89q.worldedit.math.transform.AffineTransform; import java.util.Comparator; @@ -32,7 +33,8 @@ /** * An immutable 3-dimensional vector. */ -public record BlockVector3(int x, int y, int z) { +// This implements the internal `NativePosition` interface so that Bukkit can use this as its native position type +public record BlockVector3(int x, int y, int z) implements NativePosition { public static final BlockVector3 ZERO = new BlockVector3(0, 0, 0); public static final BlockVector3 UNIT_X = new BlockVector3(1, 0, 0); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/PropertiesConfiguration.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/PropertiesConfiguration.java index 27ecf063a1..1c23e2b1d5 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/PropertiesConfiguration.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/PropertiesConfiguration.java @@ -134,6 +134,7 @@ public void load() { extendedYLimit = getBool("extended-y-limit", extendedYLimit); setDefaultLocaleName(getString("default-locale", defaultLocaleName)); commandBlockSupport = getBool("command-block-support", commandBlockSupport); + chunkSectionEditing = getBool("chunk-section-editing", chunkSectionEditing); LocalSession.MAX_HISTORY_SIZE = Math.max(15, getInt("history-size", 15)); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/YAMLConfiguration.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/YAMLConfiguration.java index f2bea3c107..e1e6768b5b 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/YAMLConfiguration.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/YAMLConfiguration.java @@ -131,6 +131,8 @@ public void load() { setDefaultLocaleName(config.getString("default-locale", defaultLocaleName)); commandBlockSupport = config.getBoolean("command-block-support", false); + + chunkSectionEditing = config.getBoolean("chunk-section-editing", chunkSectionEditing); } public void unload() { diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/AbstractWorld.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/AbstractWorld.java index 14c607fa4b..07cb56177d 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/AbstractWorld.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/AbstractWorld.java @@ -26,6 +26,7 @@ import com.sk89q.worldedit.function.mask.BlockTypeMask; import com.sk89q.worldedit.function.mask.Mask; import com.sk89q.worldedit.function.operation.Operation; +import com.sk89q.worldedit.internal.wna.NativeWorld; import com.sk89q.worldedit.math.BlockVector2; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.math.Vector3; @@ -49,6 +50,13 @@ public abstract class AbstractWorld implements World { private final PriorityQueue