From 4aabc1362bee41b2fd6b20a30fa01e938aa20ac7 Mon Sep 17 00:00:00 2001 From: twme-ai <299940106+twme-ai@users.noreply.github.com> Date: Mon, 2 Nov 2026 09:59:56 +0000 Subject: [PATCH 1/2] Cache block state conversions in the FAWE batch processor For every changed block, FastAsyncWorldEditLogger converted the old and new block state to Bukkit Material and BlockData and serialized both with getAsString(). These results depend only on the block state, and an edit usually repeats a few states many times. BlockStateConversion holds the Material, a BlockData template and the data string of one block state, all in final fields. The processor indexes conversions by FAWE ordinal in a plain array, which is safe across FAWE's threads because the fields are final; two threads filling the same slot store equivalent conversions. BlockData is mutable and postProcess may modify it (checkWaterlogged), so callers get a clone. WorldEditLogger gains a postProcess overload that takes the converted values; the existing overloads delegate to it unchanged. Co-Authored-By: Claude Opus 5.5 --- .../worldedit/BlockStateConversion.java | 71 +++++++++++++++++++ .../worldedit/FastAsyncWorldEditLogger.java | 36 ++++++---- .../worldedit/WorldEditLogger.java | 13 ++-- 3 files changed, 104 insertions(+), 16 deletions(-) create mode 100644 src/main/java/net/coreprotect/worldedit/BlockStateConversion.java diff --git a/src/main/java/net/coreprotect/worldedit/BlockStateConversion.java b/src/main/java/net/coreprotect/worldedit/BlockStateConversion.java new file mode 100644 index 000000000..052194139 --- /dev/null +++ b/src/main/java/net/coreprotect/worldedit/BlockStateConversion.java @@ -0,0 +1,71 @@ +package net.coreprotect.worldedit; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.bukkit.Material; +import org.bukkit.block.data.BlockData; + +import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.world.block.BlockState; + +/** + * The Bukkit conversions of one WorldEdit block state: its Material, a BlockData template and the BlockData string. + * Converting a state builds a new BlockData each time and serializing it builds a new string, while an edit usually + * repeats a few states many times, so the loggers convert each state once. + * + * All fields are final, so an instance is fully visible to any thread that reads a reference to it, however that + * reference was published. The block state registry is fixed once the server is running, so the cache is bounded by + * the number of block states. + */ +final class BlockStateConversion { + + private static final Map CACHE = new ConcurrentHashMap<>(); + + private final Material material; + private final BlockData template; + private final String dataString; + + private BlockStateConversion(BlockState state) { + this.material = BukkitAdapter.adapt(state.getBlockType()); + this.template = BukkitAdapter.adapt(state); + this.dataString = template.getAsString(); + } + + /** + * WorldEdit keeps one instance per block state, so lookups normally match by identity. + */ + static BlockStateConversion of(BlockState state) { + BlockStateConversion conversion = CACHE.get(state); + if (conversion == null) { + conversion = new BlockStateConversion(state); + BlockStateConversion existing = CACHE.putIfAbsent(state, conversion); + if (existing != null) { + conversion = existing; + } + } + return conversion; + } + + Material getMaterial() { + return material; + } + + /** + * The shared template, for reading only. + */ + BlockData getTemplate() { + return template; + } + + /** + * A copy of the template that the caller owns, since logging may modify it. + */ + BlockData createBlockData() { + return template.clone(); + } + + String getDataString() { + return dataString; + } +} diff --git a/src/main/java/net/coreprotect/worldedit/FastAsyncWorldEditLogger.java b/src/main/java/net/coreprotect/worldedit/FastAsyncWorldEditLogger.java index 407dab9d2..de84eae86 100644 --- a/src/main/java/net/coreprotect/worldedit/FastAsyncWorldEditLogger.java +++ b/src/main/java/net/coreprotect/worldedit/FastAsyncWorldEditLogger.java @@ -12,7 +12,6 @@ import com.sk89q.worldedit.bukkit.BukkitAdapter; import com.sk89q.worldedit.extension.platform.Actor; import com.sk89q.worldedit.extent.Extent; -import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.world.World; import com.sk89q.worldedit.world.block.BaseBlock; import com.sk89q.worldedit.world.block.BlockState; @@ -25,13 +24,11 @@ final class FastAsyncWorldEditLogger implements IBatchProcessor { private static final ProcessorScope SCOPE = resolveScope(); private final Actor actor; private final World eventWorld; - private final Extent extent; private final org.bukkit.World world; FastAsyncWorldEditLogger(Actor actor, World world, Extent extent) { this.actor = actor; this.eventWorld = world; - this.extent = extent; this.world = BukkitAdapter.adapt(world); } @@ -72,15 +69,14 @@ public IChunkSet processSet(IChunk chunk, IChunkGet get, IChunkSet set) { int x = index & 15; int y = (layer << 4) + (index >> 8); int z = (index >> 4) & 15; - BlockState oldBlock = BlockState.getFromOrdinal(oldOrdinal); - BlockState newBlock = BlockState.getFromOrdinal(newOrdinal); - Material oldType = BukkitAdapter.adapt(oldBlock.getBlockType()); - BlockData oldBlockData = BukkitAdapter.adapt(oldBlock); - BlockVector3 position = BlockVector3.at(chunkX + x, y, chunkZ + z); + BlockStateConversion oldConversion = conversion(oldOrdinal); + BlockStateConversion newConversion = conversion(newOrdinal); + Material oldType = oldConversion.getMaterial(); + BlockData oldBlockData = oldConversion.createBlockData(); Location location = new Location(world, chunkX + x, y, chunkZ + z); BaseBlock baseBlock = WorldEditLogger.needsBaseBlock(oldType, config) ? get.getFullBlock(x, y, z) : null; org.bukkit.block.BlockState lowerHalf = WorldEditLogger.isLoggedTopHalf(oldType, oldBlockData) ? getLowerHalf(get, set, x, y, z, chunkX + x, chunkZ + z, oldBlockData) : null; - WorldEditLogger.postProcess(extent, actor, position, location, newBlock, baseBlock, oldType, oldBlockData, null, false, lowerHalf); + WorldEditLogger.postProcess(actor, location, baseBlock, oldType, oldBlockData, oldConversion.getDataString(), newConversion.getMaterial(), newConversion.createBlockData(), newConversion.getDataString(), null, false, lowerHalf); } } return set; @@ -108,12 +104,28 @@ private org.bukkit.block.BlockState getLowerHalf(IChunkGet get, IChunkSet set, i return null; } - BlockData blockData = BukkitAdapter.adapt(BlockState.getFromOrdinal(ordinal)); - if (!WorldEditLogger.isOtherHalf(topHalf, blockData)) { + BlockStateConversion conversion = conversion(ordinal); + if (!WorldEditLogger.isOtherHalf(topHalf, conversion.getTemplate())) { return null; } - return new WorldEditBlockState(new Location(world, worldX, lowerY, worldZ), blockData.getMaterial(), blockData); + return new WorldEditBlockState(new Location(world, worldX, lowerY, worldZ), conversion.getMaterial(), conversion.createBlockData()); + } + + /* + * BlockStateConversion indexed by FAWE block state ordinal, which avoids hashing the state for every changed block. + * FAWE runs this processor on several threads. A plain array is safe because every field of BlockStateConversion + * is final, and concurrent writers store equivalent conversions, so a lost race only repeats the work. + */ + private static final BlockStateConversion[] CONVERSIONS = new BlockStateConversion[BlockTypesCache.states.length]; + + private static BlockStateConversion conversion(int ordinal) { + BlockStateConversion conversion = CONVERSIONS[ordinal]; + if (conversion == null) { + conversion = BlockStateConversion.of(BlockState.getFromOrdinal(ordinal)); + CONVERSIONS[ordinal] = conversion; + } + return conversion; } @Override diff --git a/src/main/java/net/coreprotect/worldedit/WorldEditLogger.java b/src/main/java/net/coreprotect/worldedit/WorldEditLogger.java index 4c4fafb76..317297717 100644 --- a/src/main/java/net/coreprotect/worldedit/WorldEditLogger.java +++ b/src/main/java/net/coreprotect/worldedit/WorldEditLogger.java @@ -98,11 +98,16 @@ protected static void postProcess(Extent extent, Actor actor, BlockVector3 posit */ protected static void postProcess(Extent extent, Actor actor, BlockVector3 position, Location location, BlockStateHolder blockStateHolder, BaseBlock baseBlock, Material oldType, BlockData oldBlockData, ItemStack[] containerContents, boolean logBlockPhysics, BlockState otherHalf) { BlockData newBlockData = BukkitAdapter.adapt(blockStateHolder.toImmutableState()); - Material newType = newBlockData.getMaterial(); - - String oldBlockDataString = oldBlockData.getAsString(); - String newBlockDataString = newBlockData.getAsString(); + postProcess(actor, location, baseBlock, oldType, oldBlockData, oldBlockData.getAsString(), newBlockData.getMaterial(), newBlockData, newBlockData.getAsString(), containerContents, logBlockPhysics, otherHalf); + } + /** + * Same as above, with the Bukkit data already converted through BlockStateConversion. + * + * @param oldBlockData A fresh instance owned by this call, since it may be modified + * @param newBlockData A fresh instance owned by this call, since it may be modified + */ + protected static void postProcess(Actor actor, Location location, BaseBlock baseBlock, Material oldType, BlockData oldBlockData, String oldBlockDataString, Material newType, BlockData newBlockData, String newBlockDataString, ItemStack[] containerContents, boolean logBlockPhysics, BlockState otherHalf) { if (!oldType.equals(newType) || !oldBlockDataString.equals(newBlockDataString)) { BlockState oldBlock = new WorldEditBlockState(location, oldType, oldBlockData); BlockState newBlock = new WorldEditBlockState(location, newType, newBlockData); From fa9d9059d4ee845053cf9ff71852812cfa5afd3a Mon Sep 17 00:00:00 2001 From: twme-ai <299940106+twme-ai@users.noreply.github.com> Date: Mon, 2 Nov 2026 09:59:56 +0000 Subject: [PATCH 2/2] Cache block state conversions in the WorldEdit logger CoreProtectLogger, used with WorldEdit and with FAWE when fast-placement is off, did the same per-block conversions, and getOtherHalf converted the neighbour of every bisected block. WorldEdit's BukkitAdapter builds BlockData by serializing the state and parsing the string, without caching. Both now use BlockStateConversion, looked up in a ConcurrentHashMap keyed by the WorldEdit block state. WorldEdit keeps a single instance per state with a cached hash code, so lookups match by identity. The postProcess overloads that converted the data are now unused and removed. Co-Authored-By: Claude Opus 5.5 --- .../worldedit/CoreProtectLogger.java | 8 ++++--- .../worldedit/WorldEditLogger.java | 23 ++++--------------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/src/main/java/net/coreprotect/worldedit/CoreProtectLogger.java b/src/main/java/net/coreprotect/worldedit/CoreProtectLogger.java index 81c8fba0b..7ce3060b2 100755 --- a/src/main/java/net/coreprotect/worldedit/CoreProtectLogger.java +++ b/src/main/java/net/coreprotect/worldedit/CoreProtectLogger.java @@ -56,8 +56,9 @@ public > boolean setBlock(BlockVector3 position, T return eventExtent.setBlock(position, block); } - Material oldType = BukkitAdapter.adapt(oldBlock.getBlockType()); - BlockData oldBlockData = BukkitAdapter.adapt(oldBlock); + BlockStateConversion oldConversion = BlockStateConversion.of(oldBlock); + Material oldType = oldConversion.getMaterial(); + BlockData oldBlockData = oldConversion.createBlockData(); Location location = new Location(world, position.getBlockX(), position.getBlockY(), position.getBlockZ()); BaseBlock baseBlock = WorldEditLogger.needsBaseBlock(oldType, config) ? eventExtent.getFullBlock(position) : null; // Reads below this stage reach the live world, so capture the other half before setBlock or its physics can change it @@ -69,7 +70,8 @@ public > boolean setBlock(BlockVector3 position, T ItemStack[] containerData = !CoreProtectEditSessionEvent.isFAWE() && config.ITEM_TRANSACTIONS && BlockGroup.CONTAINERS.contains(oldType) ? ItemUtils.getContainerContents(oldType, null, location) : null; if (eventExtent.setBlock(position, block)) { - WorldEditLogger.postProcess(eventExtent, eventActor, position, location, block, baseBlock, oldType, oldBlockData, containerData, otherHalf); + BlockStateConversion newConversion = BlockStateConversion.of(block.toImmutableState()); + WorldEditLogger.postProcess(eventActor, location, baseBlock, oldType, oldBlockData, oldConversion.getDataString(), newConversion.getMaterial(), newConversion.createBlockData(), newConversion.getDataString(), containerData, true, otherHalf); return true; } diff --git a/src/main/java/net/coreprotect/worldedit/WorldEditLogger.java b/src/main/java/net/coreprotect/worldedit/WorldEditLogger.java index 317297717..e2d065adc 100644 --- a/src/main/java/net/coreprotect/worldedit/WorldEditLogger.java +++ b/src/main/java/net/coreprotect/worldedit/WorldEditLogger.java @@ -21,13 +21,11 @@ import com.sk89q.jnbt.CompoundTag; import com.sk89q.jnbt.NBTUtils; -import com.sk89q.worldedit.bukkit.BukkitAdapter; import com.sk89q.worldedit.bukkit.WorldEditPlugin; import com.sk89q.worldedit.extension.platform.Actor; import com.sk89q.worldedit.extent.Extent; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.world.block.BaseBlock; -import com.sk89q.worldedit.world.block.BlockStateHolder; import net.coreprotect.config.Config; import net.coreprotect.consumer.Queue; @@ -79,33 +77,22 @@ protected static BlockState getOtherHalf(Extent extent, BlockVector3 position, L return null; } - BlockData otherData = BukkitAdapter.adapt(extent.getBlock(position.add(0, offset, 0))); - if (!isOtherHalf(blockData, otherData)) { + BlockStateConversion other = BlockStateConversion.of(extent.getBlock(position.add(0, offset, 0))); + if (!isOtherHalf(blockData, other.getTemplate())) { return null; } Location otherLocation = location.clone(); otherLocation.setY(otherY); - return new WorldEditBlockState(otherLocation, otherData.getMaterial(), otherData); - } - - protected static void postProcess(Extent extent, Actor actor, BlockVector3 position, Location location, BlockStateHolder blockStateHolder, BaseBlock baseBlock, Material oldType, BlockData oldBlockData, ItemStack[] containerContents, BlockState otherHalf) { - postProcess(extent, actor, position, location, blockStateHolder, baseBlock, oldType, oldBlockData, containerContents, true, otherHalf); - } - - /** - * @param otherHalf The matching other half of a double block as it was before this edit changed it, or null. Callers that do not log block physics only supply the lower half of a top half. - */ - protected static void postProcess(Extent extent, Actor actor, BlockVector3 position, Location location, BlockStateHolder blockStateHolder, BaseBlock baseBlock, Material oldType, BlockData oldBlockData, ItemStack[] containerContents, boolean logBlockPhysics, BlockState otherHalf) { - BlockData newBlockData = BukkitAdapter.adapt(blockStateHolder.toImmutableState()); - postProcess(actor, location, baseBlock, oldType, oldBlockData, oldBlockData.getAsString(), newBlockData.getMaterial(), newBlockData, newBlockData.getAsString(), containerContents, logBlockPhysics, otherHalf); + return new WorldEditBlockState(otherLocation, other.getMaterial(), other.createBlockData()); } /** - * Same as above, with the Bukkit data already converted through BlockStateConversion. + * The loggers pass Bukkit data already converted through BlockStateConversion. * * @param oldBlockData A fresh instance owned by this call, since it may be modified * @param newBlockData A fresh instance owned by this call, since it may be modified + * @param otherHalf The matching other half of a double block as it was before this edit changed it, or null. Callers that do not log block physics only supply the lower half of a top half. */ protected static void postProcess(Actor actor, Location location, BaseBlock baseBlock, Material oldType, BlockData oldBlockData, String oldBlockDataString, Material newType, BlockData newBlockData, String newBlockDataString, ItemStack[] containerContents, boolean logBlockPhysics, BlockState otherHalf) { if (!oldType.equals(newType) || !oldBlockDataString.equals(newBlockDataString)) {