diff --git a/.github/workflows/announce-release-on-discord.yml b/.github/workflows/announce-release-on-discord.yml index 8103665409..77fa897323 100644 --- a/.github/workflows/announce-release-on-discord.yml +++ b/.github/workflows/announce-release-on-discord.yml @@ -13,7 +13,7 @@ jobs: DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} DISCORD_USERNAME: FastAsyncWorldEdit Release DISCORD_AVATAR: https://raw.githubusercontent.com/IntellectualSites/Assets/main/plugins/FastAsyncWorldEdit/FastAsyncWorldEdit.png - uses: Ilshidur/action-discord@0c4b27844ba47cb1c7bee539c8eead5284ce9fa9 # ratchet:Ilshidur/action-discord@0.3.2 + uses: Ilshidur/action-discord@0.3.2 with: args: | "<@&525015715300900875> <@&706463154804097105> <@&671372968462516240>" diff --git a/.gitignore b/.gitignore index 14ba6416a6..0a9d6020d9 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ worldedit-core/src/main/resources/lang/* /worldedit-core/.factorypath .DS_Store +### Run server ignore +run-* diff --git a/Jenkinsfile b/Jenkinsfile index ec28137831..8c6ece0eb3 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -4,14 +4,13 @@ pipeline { disableConcurrentBuilds() } stages { - stage('Set JDK 17') { - steps { - tool name: 'Temurin-17.0.6+10', type: 'jdk' - } - } stage('Build') { steps { - sh './gradlew clean build' + withEnv([ + "PATH+JAVA=${tool 'Temurin-17.0.6+10'}/bin" + ]) { + sh './gradlew clean build' + } } } stage('Archive artifacts') { diff --git a/build.gradle.kts b/build.gradle.kts index 5bbde35da5..2879f61072 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ import xyz.jpenilla.runpaper.task.RunServer plugins { id("io.github.gradle-nexus.publish-plugin") version "1.3.0" - id("xyz.jpenilla.run-paper") version "2.0.1" + id("xyz.jpenilla.run-paper") version "2.1.0" } if (!File("$rootDir/.git").exists()) { @@ -34,7 +34,7 @@ logger.lifecycle(""" ******************************************* """) -var rootVersion by extra("2.6.0") +var rootVersion by extra("2.6.2") var snapshot by extra("SNAPSHOT") var revision: String by extra("") var buildNumber by extra("") diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 15237a86ed..461e9deaa4 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -22,9 +22,9 @@ val properties = Properties().also { props -> dependencies { implementation(gradleApi()) - implementation("org.ajoberstar.grgit:grgit-gradle:5.0.0") + implementation("org.ajoberstar.grgit:grgit-gradle:5.2.0") implementation("gradle.plugin.com.github.johnrengelman:shadow:7.1.2") - implementation("io.papermc.paperweight.userdev:io.papermc.paperweight.userdev.gradle.plugin:1.5.3") + implementation("io.papermc.paperweight.userdev:io.papermc.paperweight.userdev.gradle.plugin:1.5.5") } kotlin { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d450d7ad9b..388f45d9b0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,12 +6,12 @@ log4j = "2.19.0" # Plugins dummypermscompat = "1.10" -worldguard-bukkit = "7.0.7" +worldguard-bukkit = "7.0.8" mapmanager = "1.8.0-SNAPSHOT" griefprevention = "16.18.1" griefdefender = "2.1.0-SNAPSHOT" residence = "4.5._13.1" -towny = "0.98.4.18" +towny = "0.99.0.5" # Third party bstats = "3.0.2" @@ -36,7 +36,7 @@ text = "3.0.4" piston = "0.5.7" # Tests -mockito = "5.2.0" +mockito = "5.3.1" # Gradle plugins pluginyml = "0.5.3" diff --git a/worldedit-bukkit/adapters/adapter-1_17_1/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_17_R1_2/PaperweightPlatformAdapter.java b/worldedit-bukkit/adapters/adapter-1_17_1/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_17_R1_2/PaperweightPlatformAdapter.java index c034c94eae..008fe7b04a 100644 --- a/worldedit-bukkit/adapters/adapter-1_17_1/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_17_R1_2/PaperweightPlatformAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_17_1/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_17_R1_2/PaperweightPlatformAdapter.java @@ -30,7 +30,9 @@ import net.minecraft.server.level.ChunkMap; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.TicketType; import net.minecraft.util.BitStorage; +import net.minecraft.util.Unit; import net.minecraft.world.entity.Entity; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.LevelAccessor; @@ -210,10 +212,12 @@ public static LevelChunk ensureLoaded(ServerLevel serverLevel, int chunkX, int c } else { LevelChunk nmsChunk = serverLevel.getChunkSource().getChunkAtIfCachedImmediately(chunkX, chunkZ); if (nmsChunk != null) { + addTicket(serverLevel, chunkX, chunkZ); return nmsChunk; } nmsChunk = serverLevel.getChunkSource().getChunkAtIfLoadedImmediately(chunkX, chunkZ); if (nmsChunk != null) { + addTicket(serverLevel, chunkX, chunkZ); return nmsChunk; } // Avoid "async" methods from the main thread. @@ -231,6 +235,13 @@ public static LevelChunk ensureLoaded(ServerLevel serverLevel, int chunkX, int c return TaskManager.taskManager().sync(() -> serverLevel.getChunk(chunkX, chunkZ)); } + private static void addTicket(ServerLevel serverLevel, int chunkX, int chunkZ) { + // Ensure chunk is definitely loaded before applying a ticket + net.minecraft.server.MCUtil.MAIN_EXECUTOR.execute(() -> serverLevel + .getChunkSource() + .addRegionTicket(TicketType.PLUGIN, new ChunkPos(chunkX, chunkZ), 0, Unit.INSTANCE)); + } + public static ChunkHolder getPlayerChunk(ServerLevel nmsWorld, final int chunkX, final int chunkZ) { ChunkMap chunkMap = nmsWorld.getChunkSource().chunkMap; try { diff --git a/worldedit-bukkit/adapters/adapter-1_18_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_18_R2/PaperweightPlatformAdapter.java b/worldedit-bukkit/adapters/adapter-1_18_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_18_R2/PaperweightPlatformAdapter.java index 78222e492d..28e28739a2 100644 --- a/worldedit-bukkit/adapters/adapter-1_18_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_18_R2/PaperweightPlatformAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_18_2/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_18_R2/PaperweightPlatformAdapter.java @@ -28,9 +28,11 @@ import net.minecraft.server.level.ChunkMap; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.TicketType; import net.minecraft.util.BitStorage; import net.minecraft.util.SimpleBitStorage; import net.minecraft.util.ThreadingDetector; +import net.minecraft.util.Unit; import net.minecraft.util.ZeroBitStorage; import net.minecraft.world.entity.Entity; import net.minecraft.world.level.ChunkPos; @@ -233,10 +235,12 @@ public static LevelChunk ensureLoaded(ServerLevel serverLevel, int chunkX, int c } else { LevelChunk nmsChunk = serverLevel.getChunkSource().getChunkAtIfCachedImmediately(chunkX, chunkZ); if (nmsChunk != null) { + addTicket(serverLevel, chunkX, chunkZ); return nmsChunk; } nmsChunk = serverLevel.getChunkSource().getChunkAtIfLoadedImmediately(chunkX, chunkZ); if (nmsChunk != null) { + addTicket(serverLevel, chunkX, chunkZ); return nmsChunk; } // Avoid "async" methods from the main thread. @@ -254,6 +258,13 @@ public static LevelChunk ensureLoaded(ServerLevel serverLevel, int chunkX, int c return TaskManager.taskManager().sync(() -> serverLevel.getChunk(chunkX, chunkZ)); } + private static void addTicket(ServerLevel serverLevel, int chunkX, int chunkZ) { + // Ensure chunk is definitely loaded before applying a ticket + net.minecraft.server.MCUtil.MAIN_EXECUTOR.execute(() -> serverLevel + .getChunkSource() + .addRegionTicket(TicketType.PLUGIN, new ChunkPos(chunkX, chunkZ), 0, Unit.INSTANCE)); + } + public static ChunkHolder getPlayerChunk(ServerLevel nmsWorld, final int chunkX, final int chunkZ) { ChunkMap chunkMap = nmsWorld.getChunkSource().chunkMap; try { diff --git a/worldedit-bukkit/adapters/adapter-1_19/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_19_R1/PaperweightPlatformAdapter.java b/worldedit-bukkit/adapters/adapter-1_19/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_19_R1/PaperweightPlatformAdapter.java index bf2aa7cb8b..c09946b799 100644 --- a/worldedit-bukkit/adapters/adapter-1_19/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_19_R1/PaperweightPlatformAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_19/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_19_R1/PaperweightPlatformAdapter.java @@ -30,10 +30,12 @@ import net.minecraft.server.level.ChunkMap; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.TicketType; import net.minecraft.util.BitStorage; import net.minecraft.util.ExceptionCollector; import net.minecraft.util.SimpleBitStorage; import net.minecraft.util.ThreadingDetector; +import net.minecraft.util.Unit; import net.minecraft.util.ZeroBitStorage; import net.minecraft.world.entity.Entity; import net.minecraft.world.level.ChunkPos; @@ -266,10 +268,12 @@ public static LevelChunk ensureLoaded(ServerLevel serverLevel, int chunkX, int c } else { LevelChunk nmsChunk = serverLevel.getChunkSource().getChunkAtIfCachedImmediately(chunkX, chunkZ); if (nmsChunk != null) { + addTicket(serverLevel, chunkX, chunkZ); return nmsChunk; } nmsChunk = serverLevel.getChunkSource().getChunkAtIfLoadedImmediately(chunkX, chunkZ); if (nmsChunk != null) { + addTicket(serverLevel, chunkX, chunkZ); return nmsChunk; } // Avoid "async" methods from the main thread. @@ -287,6 +291,13 @@ public static LevelChunk ensureLoaded(ServerLevel serverLevel, int chunkX, int c return TaskManager.taskManager().sync(() -> serverLevel.getChunk(chunkX, chunkZ)); } + private static void addTicket(ServerLevel serverLevel, int chunkX, int chunkZ) { + // Ensure chunk is definitely loaded before applying a ticket + io.papermc.paper.util.MCUtil.MAIN_EXECUTOR.execute(() -> serverLevel + .getChunkSource() + .addRegionTicket(TicketType.PLUGIN, new ChunkPos(chunkX, chunkZ), 0, Unit.INSTANCE)); + } + public static ChunkHolder getPlayerChunk(ServerLevel nmsWorld, final int chunkX, final int chunkZ) { ChunkMap chunkMap = nmsWorld.getChunkSource().chunkMap; try { diff --git a/worldedit-bukkit/adapters/adapter-1_19_3/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_19_R2/PaperweightPlatformAdapter.java b/worldedit-bukkit/adapters/adapter-1_19_3/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_19_R2/PaperweightPlatformAdapter.java index 73b4e81d4c..5c98c20c08 100644 --- a/worldedit-bukkit/adapters/adapter-1_19_3/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_19_R2/PaperweightPlatformAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_19_3/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_19_R2/PaperweightPlatformAdapter.java @@ -30,10 +30,12 @@ import net.minecraft.server.level.ChunkMap; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.TicketType; import net.minecraft.util.BitStorage; import net.minecraft.util.ExceptionCollector; import net.minecraft.util.SimpleBitStorage; import net.minecraft.util.ThreadingDetector; +import net.minecraft.util.Unit; import net.minecraft.util.ZeroBitStorage; import net.minecraft.world.entity.Entity; import net.minecraft.world.level.ChunkPos; @@ -268,10 +270,12 @@ public static LevelChunk ensureLoaded(ServerLevel serverLevel, int chunkX, int c } else { LevelChunk nmsChunk = serverLevel.getChunkSource().getChunkAtIfCachedImmediately(chunkX, chunkZ); if (nmsChunk != null) { + addTicket(serverLevel, chunkX, chunkZ); return nmsChunk; } nmsChunk = serverLevel.getChunkSource().getChunkAtIfLoadedImmediately(chunkX, chunkZ); if (nmsChunk != null) { + addTicket(serverLevel, chunkX, chunkZ); return nmsChunk; } // Avoid "async" methods from the main thread. @@ -289,6 +293,13 @@ public static LevelChunk ensureLoaded(ServerLevel serverLevel, int chunkX, int c return TaskManager.taskManager().sync(() -> serverLevel.getChunk(chunkX, chunkZ)); } + private static void addTicket(ServerLevel serverLevel, int chunkX, int chunkZ) { + // Ensure chunk is definitely loaded before applying a ticket + io.papermc.paper.util.MCUtil.MAIN_EXECUTOR.execute(() -> serverLevel + .getChunkSource() + .addRegionTicket(TicketType.PLUGIN, new ChunkPos(chunkX, chunkZ), 0, Unit.INSTANCE)); + } + public static ChunkHolder getPlayerChunk(ServerLevel nmsWorld, final int chunkX, final int chunkZ) { ChunkMap chunkMap = nmsWorld.getChunkSource().chunkMap; try { diff --git a/worldedit-bukkit/adapters/adapter-1_19_4/build.gradle.kts b/worldedit-bukkit/adapters/adapter-1_19_4/build.gradle.kts index 8fb3980de5..cf00f80f2e 100644 --- a/worldedit-bukkit/adapters/adapter-1_19_4/build.gradle.kts +++ b/worldedit-bukkit/adapters/adapter-1_19_4/build.gradle.kts @@ -9,6 +9,6 @@ repositories { } dependencies { - paperDevBundle("1.19.4-R0.1-20230331.112431-38") + paperDevBundle("1.19.4-R0.1-20230423.020222-72") compileOnly("io.papermc:paperlib") } diff --git a/worldedit-bukkit/adapters/adapter-1_19_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_19_R3/PaperweightFaweAdapter.java b/worldedit-bukkit/adapters/adapter-1_19_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_19_R3/PaperweightFaweAdapter.java index 5edcb86188..e23e18a2ec 100644 --- a/worldedit-bukkit/adapters/adapter-1_19_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_19_R3/PaperweightFaweAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_19_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_19_R3/PaperweightFaweAdapter.java @@ -306,54 +306,6 @@ public Set getSupportedSideEffects() { return SideEffectSet.defaults().getSideEffectsToApply(); } - public boolean setBlock(org.bukkit.Chunk chunk, int x, int y, int z, BlockStateHolder state, boolean update) { - CraftChunk craftChunk = (CraftChunk) chunk; - LevelChunk levelChunk = craftChunk.getHandle(); - Level level = levelChunk.getLevel(); - - BlockPos blockPos = new BlockPos(x, y, z); - net.minecraft.world.level.block.state.BlockState blockState = ((PaperweightBlockMaterial) state.getMaterial()).getState(); - LevelChunkSection[] levelChunkSections = levelChunk.getSections(); - int y4 = y >> 4; - LevelChunkSection section = levelChunkSections[y4]; - - net.minecraft.world.level.block.state.BlockState existing; - if (section == null) { - existing = ((PaperweightBlockMaterial) BlockTypes.AIR.getDefaultState().getMaterial()).getState(); - } else { - existing = section.getBlockState(x & 15, y & 15, z & 15); - } - - levelChunk.removeBlockEntity(blockPos); // Force delete the old tile entity - - CompoundBinaryTag compoundTag = state instanceof BaseBlock ? state.getNbt() : null; - if (compoundTag != null || existing instanceof TileEntityBlock) { - level.setBlock(blockPos, blockState, 0); - // remove tile - if (compoundTag != null) { - // We will assume that the tile entity was created for us, - // though we do not do this on the Forge version - BlockEntity blockEntity = level.getBlockEntity(blockPos); - if (blockEntity != null) { - net.minecraft.nbt.CompoundTag tag = (net.minecraft.nbt.CompoundTag) fromNativeBinary(compoundTag); - tag.put("x", IntTag.valueOf(x)); - tag.put("y", IntTag.valueOf(y)); - tag.put("z", IntTag.valueOf(z)); - blockEntity.load(tag); // readTagIntoTileEntity - load data - } - } - } else { - if (existing == blockState) { - return true; - } - levelChunk.setBlockState(blockPos, blockState, false); - } - if (update) { - level.getMinecraftWorld().sendBlockUpdated(blockPos, existing, blockState, 0); - } - return true; - } - @Override public WorldNativeAccess createWorldNativeAccess(org.bukkit.World world) { return new PaperweightFaweWorldNativeAccess( diff --git a/worldedit-bukkit/adapters/adapter-1_19_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_19_R3/PaperweightFaweWorldNativeAccess.java b/worldedit-bukkit/adapters/adapter-1_19_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_19_R3/PaperweightFaweWorldNativeAccess.java index 4dead58fa1..dbe0150a9c 100644 --- a/worldedit-bukkit/adapters/adapter-1_19_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_19_R3/PaperweightFaweWorldNativeAccess.java +++ b/worldedit-bukkit/adapters/adapter-1_19_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_19_R3/PaperweightFaweWorldNativeAccess.java @@ -102,7 +102,7 @@ public synchronized net.minecraft.world.level.block.state.BlockState setBlockSta } // Since FAWE is.. Async we need to do it on the main thread (wooooo.. :( ) cachedChanges.add(new CachedChange(levelChunk, blockPos, blockState)); - cachedChunksToSend.add(new IntPair(levelChunk.bukkitChunk.getX(), levelChunk.bukkitChunk.getZ())); + cachedChunksToSend.add(new IntPair(levelChunk.locX, levelChunk.locZ)); boolean nextTick = lastTick.get() > currentTick; if (nextTick || cachedChanges.size() >= 1024) { if (nextTick) { diff --git a/worldedit-bukkit/adapters/adapter-1_19_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_19_R3/PaperweightPlatformAdapter.java b/worldedit-bukkit/adapters/adapter-1_19_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_19_R3/PaperweightPlatformAdapter.java index 7dfaad6982..07b0a6893d 100644 --- a/worldedit-bukkit/adapters/adapter-1_19_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_19_R3/PaperweightPlatformAdapter.java +++ b/worldedit-bukkit/adapters/adapter-1_19_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_19_R3/PaperweightPlatformAdapter.java @@ -30,10 +30,12 @@ import net.minecraft.server.level.ChunkMap; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.TicketType; import net.minecraft.util.BitStorage; import net.minecraft.util.ExceptionCollector; import net.minecraft.util.SimpleBitStorage; import net.minecraft.util.ThreadingDetector; +import net.minecraft.util.Unit; import net.minecraft.util.ZeroBitStorage; import net.minecraft.world.entity.Entity; import net.minecraft.world.level.ChunkPos; @@ -42,6 +44,8 @@ import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkStatus; import net.minecraft.world.level.chunk.GlobalPalette; import net.minecraft.world.level.chunk.HashMapPalette; import net.minecraft.world.level.chunk.LevelChunk; @@ -58,6 +62,7 @@ import javax.annotation.Nullable; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -75,6 +80,7 @@ import java.util.concurrent.Semaphore; import java.util.function.Function; +import static java.lang.invoke.MethodType.methodType; import static net.minecraft.core.registries.Registries.BIOME; public final class PaperweightPlatformAdapter extends NMSAdapter { @@ -104,6 +110,12 @@ public final class PaperweightPlatformAdapter extends NMSAdapter { private static final MethodHandle methodRemoveGameEventListener; private static final MethodHandle methodremoveTickingBlockEntity; + /* + * This is a workaround for the changes from https://hub.spigotmc.org/stash/projects/SPIGOT/repos/craftbukkit/commits/1fddefce1cdce44010927b888432bf70c0e88cde#src/main/java/org/bukkit/craftbukkit/CraftChunk.java + * and is only needed to support 1.19.4 versions before *and* after this change. + */ + private static final MethodHandle CRAFT_CHUNK_GET_HANDLE; + private static final Field fieldRemove; static final boolean POST_CHUNK_REWRITE; @@ -112,6 +124,7 @@ public final class PaperweightPlatformAdapter extends NMSAdapter { private static Field SERVER_LEVEL_ENTITY_MANAGER; static { + final MethodHandles.Lookup lookup = MethodHandles.lookup(); try { fieldData = PalettedContainer.class.getDeclaredField(Refraction.pickName("data", "d")); fieldData.setAccessible(true); @@ -137,7 +150,7 @@ public final class PaperweightPlatformAdapter extends NMSAdapter { "b" ), long.class); getVisibleChunkIfPresent.setAccessible(true); - methodGetVisibleChunk = MethodHandles.lookup().unreflect(getVisibleChunkIfPresent); + methodGetVisibleChunk = lookup.unreflect(getVisibleChunkIfPresent); Unsafe unsafe = ReflectionUtils.getUnsafe(); if (!PaperLib.isPaper()) { @@ -161,7 +174,7 @@ public final class PaperweightPlatformAdapter extends NMSAdapter { ServerLevel.class ); removeGameEventListener.setAccessible(true); - methodRemoveGameEventListener = MethodHandles.lookup().unreflect(removeGameEventListener); + methodRemoveGameEventListener = lookup.unreflect(removeGameEventListener); Method removeBlockEntityTicker = LevelChunk.class.getDeclaredMethod( Refraction.pickName( @@ -170,7 +183,7 @@ public final class PaperweightPlatformAdapter extends NMSAdapter { ), BlockPos.class ); removeBlockEntityTicker.setAccessible(true); - methodremoveTickingBlockEntity = MethodHandles.lookup().unreflect(removeBlockEntityTicker); + methodremoveTickingBlockEntity = lookup.unreflect(removeBlockEntityTicker); fieldRemove = BlockEntity.class.getDeclaredField(Refraction.pickName("remove", "p")); fieldRemove.setAccessible(true); @@ -209,6 +222,20 @@ public final class PaperweightPlatformAdapter extends NMSAdapter { rethrow.printStackTrace(); throw new RuntimeException(rethrow); } + MethodHandle craftChunkGetHandle; + final MethodType type = methodType(ChunkAccess.class); + try { + craftChunkGetHandle = lookup.findVirtual(CraftChunk.class, "getHandle", type); + } catch (NoSuchMethodException | IllegalAccessException e) { + try { + final MethodType newType = methodType(ChunkAccess.class, ChunkStatus.class); + craftChunkGetHandle = lookup.findVirtual(CraftChunk.class, "getHandle", newType); + craftChunkGetHandle = MethodHandles.insertArguments(craftChunkGetHandle, 1, ChunkStatus.FULL); + } catch (NoSuchMethodException | IllegalAccessException ex) { + throw new RuntimeException(ex); + } + } + CRAFT_CHUNK_GET_HANDLE = craftChunkGetHandle; } static boolean setSectionAtomic( @@ -268,10 +295,12 @@ public static LevelChunk ensureLoaded(ServerLevel serverLevel, int chunkX, int c } else { LevelChunk nmsChunk = serverLevel.getChunkSource().getChunkAtIfCachedImmediately(chunkX, chunkZ); if (nmsChunk != null) { + addTicket(serverLevel, chunkX, chunkZ); return nmsChunk; } nmsChunk = serverLevel.getChunkSource().getChunkAtIfLoadedImmediately(chunkX, chunkZ); if (nmsChunk != null) { + addTicket(serverLevel, chunkX, chunkZ); return nmsChunk; } // Avoid "async" methods from the main thread. @@ -281,7 +310,8 @@ public static LevelChunk ensureLoaded(ServerLevel serverLevel, int chunkX, int c CompletableFuture future = serverLevel.getWorld().getChunkAtAsync(chunkX, chunkZ, true, true); try { CraftChunk chunk = (CraftChunk) future.get(); - return chunk.getHandle(); + addTicket(serverLevel, chunkX, chunkZ); + return (LevelChunk) CRAFT_CHUNK_GET_HANDLE.invoke(chunk); } catch (Throwable e) { e.printStackTrace(); } @@ -289,6 +319,13 @@ public static LevelChunk ensureLoaded(ServerLevel serverLevel, int chunkX, int c return TaskManager.taskManager().sync(() -> serverLevel.getChunk(chunkX, chunkZ)); } + private static void addTicket(ServerLevel serverLevel, int chunkX, int chunkZ) { + // Ensure chunk is definitely loaded before applying a ticket + io.papermc.paper.util.MCUtil.MAIN_EXECUTOR.execute(() -> serverLevel + .getChunkSource() + .addRegionTicket(TicketType.UNLOAD_COOLDOWN, new ChunkPos(chunkX, chunkZ), 0, Unit.INSTANCE)); + } + public static ChunkHolder getPlayerChunk(ServerLevel nmsWorld, final int chunkX, final int chunkZ) { ChunkMap chunkMap = nmsWorld.getChunkSource().chunkMap; try { diff --git a/worldedit-bukkit/adapters/adapter-1_19_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_19_R3/regen/PaperweightRegen.java b/worldedit-bukkit/adapters/adapter-1_19_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_19_R3/regen/PaperweightRegen.java index 282baeb211..4f22e87346 100644 --- a/worldedit-bukkit/adapters/adapter-1_19_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_19_R3/regen/PaperweightRegen.java +++ b/worldedit-bukkit/adapters/adapter-1_19_4/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/fawe/v1_19_R3/regen/PaperweightRegen.java @@ -60,6 +60,7 @@ import net.minecraft.world.level.storage.PrimaryLevelData; import org.apache.logging.log4j.Logger; import org.bukkit.Bukkit; +import org.bukkit.Chunk; import org.bukkit.craftbukkit.v1_19_R3.CraftServer; import org.bukkit.craftbukkit.v1_19_R3.CraftWorld; import org.bukkit.craftbukkit.v1_19_R3.generator.CustomChunkGenerator; @@ -440,7 +441,11 @@ protected List getBlockPopulators() { @Override protected void populate(LevelChunk levelChunk, Random random, BlockPopulator blockPopulator) { // BlockPopulator#populate has to be called synchronously for TileEntity access - TaskManager.taskManager().task(() -> blockPopulator.populate(freshWorld.getWorld(), random, levelChunk.getBukkitChunk())); + TaskManager.taskManager().task(() -> { + final CraftWorld world = freshWorld.getWorld(); + final Chunk chunk = world.getChunkAt(levelChunk.locX, levelChunk.locZ); + blockPopulator.populate(world, random, chunk); + }); } @Override diff --git a/worldedit-bukkit/build.gradle.kts b/worldedit-bukkit/build.gradle.kts index 41242611f1..461d36c996 100644 --- a/worldedit-bukkit/build.gradle.kts +++ b/worldedit-bukkit/build.gradle.kts @@ -3,7 +3,7 @@ import io.papermc.paperweight.userdev.attribute.Obfuscation plugins { `java-library` - id("com.modrinth.minotaur") version "2.+" + id("com.modrinth.minotaur") version "2.7.5" } project.description = "Bukkit" @@ -47,7 +47,13 @@ val adapters = configurations.create("adapters") { isCanBeResolved = true shouldResolveConsistentlyWith(configurations["runtimeClasspath"]) attributes { - attribute(Obfuscation.OBFUSCATION_ATTRIBUTE, objects.named(Obfuscation.OBFUSCATED)) + attribute(Obfuscation.OBFUSCATION_ATTRIBUTE, + if ((project.findProperty("enginehub.obf.none") as String?).toBoolean()) { + objects.named(Obfuscation.NONE) + } else { + objects.named(Obfuscation.OBFUSCATED) + } + ) } } diff --git a/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/adapter/Regenerator.java b/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/adapter/Regenerator.java index 8f3b4d118c..7c00ebb2bf 100644 --- a/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/adapter/Regenerator.java +++ b/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/adapter/Regenerator.java @@ -165,7 +165,11 @@ private boolean generate() throws Exception { .setNameFormat("fawe-regen-%d") .build() ); - } // else using sequential chunk generation, concurrent not supported + } else { // else using sequential chunk generation, concurrent not supported + executor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder() + .setNameFormat("fawe-regen-%d") + .build()); + } //TODO: can we get that required radius down without affecting chunk generation (e.g. strucures, features, ...)? //for now it is working well and fast, if we are bored in the future we could do the research (a lot of it) to reduce the border radius @@ -253,10 +257,13 @@ private boolean generate() throws Exception { e.printStackTrace(); } } else { // Concurrency.NONE or generateConcurrent == false - // run sequential - for (long xz : coords) { - chunkStatus.processChunkSave(xz, worldlimits.get(radius).get(xz)); - } + // run sequential but submit to different thread + // running regen on the main thread otherwise triggers async-only events on the main thread + executor.submit(() -> { + for (long xz : coords) { + chunkStatus.processChunkSave(xz, worldlimits.get(radius).get(xz)); + } + }).get(); // wait until finished this step } } diff --git a/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/listener/ChunkListener.java b/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/listener/ChunkListener.java index 5d2e35335f..7b95990843 100644 --- a/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/listener/ChunkListener.java +++ b/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/listener/ChunkListener.java @@ -371,48 +371,6 @@ public void onBlockChange(EntityChangeBlockEvent event) { } } - /** - * Prevent firework from loading chunks. - */ - @EventHandler(priority = EventPriority.LOWEST) - public void onChunkLoad(ChunkLoadEvent event) { - if (!Settings.settings().TICK_LIMITER.FIREWORKS_LOAD_CHUNKS) { - Chunk chunk = event.getChunk(); - Entity[] entities = chunk.getEntities(); - World world = chunk.getWorld(); - - Exception e = new Exception(); - int start = 14; - int end = 22; - int depth = Math.min(end, getDepth(e)); - - for (int frame = start; frame < depth; frame++) { - StackTraceElement elem = getElement(e, frame); - if (elem == null) { - return; - } - String className = elem.getClassName(); - int len = className.length(); - if (len > 15 && className.charAt(len - 15) == 'E' && className - .endsWith("EntityFireworks")) { - for (Entity ent : world.getEntities()) { - if (ent.getType() == EntityType.FIREWORK) { - Vector velocity = ent.getVelocity(); - double vertical = Math.abs(velocity.getY()); - if (Math.abs(velocity.getX()) > vertical - || Math.abs(velocity.getZ()) > vertical) { - LOGGER.warn( - "[FAWE `tick-limiter`] Detected and cancelled rogue FireWork at {}", - ent.getLocation()); - ent.remove(); - } - } - } - } - } - } - } - @EventHandler(priority = EventPriority.LOWEST) public void onItemSpawn(ItemSpawnEvent event) { if (physicsFreeze) { diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/WorldEditPlugin.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/WorldEditPlugin.java index 876517dd69..f1edf65f7d 100644 --- a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/WorldEditPlugin.java +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/WorldEditPlugin.java @@ -112,11 +112,15 @@ public class WorldEditPlugin extends JavaPlugin { private BukkitServerInterface platform; private BukkitConfiguration config; private BukkitPermissionAttachmentManager permissionAttachmentManager; + // Fawe start + private BukkitCommandSender bukkitConsoleCommandSender; + // Fawe end @Override public void onLoad() { //FAWE start + this.bukkitConsoleCommandSender = new BukkitCommandSender(this, Bukkit.getConsoleSender()); // This is already covered by Spigot, however, a more pesky warning with a proper explanation over "Ambiguous plugin name..." can't hurt. Plugin[] plugins = Bukkit.getServer().getPluginManager().getPlugins(); for (Plugin p : plugins) { @@ -594,7 +598,7 @@ public Actor wrapCommandSender(CommandSender sender) { return new BukkitBlockCommandSender(this, (BlockCommandSender) sender); } - return new BukkitCommandSender(this, sender); + return bukkitConsoleCommandSender; } public BukkitServerInterface getInternalPlatform() { diff --git a/worldedit-cli/src/main/java/com/sk89q/worldedit/cli/CLIExtraCommands.java b/worldedit-cli/src/main/java/com/sk89q/worldedit/cli/CLIExtraCommands.java index 9f63894631..27cc08821c 100644 --- a/worldedit-cli/src/main/java/com/sk89q/worldedit/cli/CLIExtraCommands.java +++ b/worldedit-cli/src/main/java/com/sk89q/worldedit/cli/CLIExtraCommands.java @@ -23,6 +23,7 @@ import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.extension.platform.Actor; import com.sk89q.worldedit.regions.selector.CuboidRegionSelector; +import com.sk89q.worldedit.regions.selector.ExtendingCuboidRegionSelector; import com.sk89q.worldedit.util.formatting.text.TextComponent; import com.sk89q.worldedit.util.task.Task; import com.sk89q.worldedit.world.World; @@ -39,9 +40,12 @@ public class CLIExtraCommands { desc = "Select the entire world" ) public void selectWorld(Actor actor, World world, LocalSession session) { - session.setRegionSelector(world, new CuboidRegionSelector( - world, world.getMinimumPoint(), world.getMaximumPoint() - )); + final CuboidRegionSelector selector; + if (session.getRegionSelector(world) instanceof ExtendingCuboidRegionSelector) { + selector = new ExtendingCuboidRegionSelector(world, world.getMinimumPoint(), world.getMaximumPoint()); + } else { + selector = new CuboidRegionSelector(world, world.getMinimumPoint(), world.getMaximumPoint()); + } actor.printInfo(TextComponent.of("Selected the entire world.")); } diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/Fawe.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/Fawe.java index edde8ed490..8b8e6c5305 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/Fawe.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/Fawe.java @@ -339,9 +339,10 @@ public void setupConfigs() { Settings.settings().QUEUE.TARGET_SIZE, Settings.settings().QUEUE.PARALLEL_THREADS ); - if (Settings.settings().QUEUE.TARGET_SIZE < 2 * Settings.settings().QUEUE.PARALLEL_THREADS) { + if (Settings.settings().QUEUE.TARGET_SIZE < 4 * Settings.settings().QUEUE.PARALLEL_THREADS) { LOGGER.error( - "queue.target_size is {}, and queue.parallel_threads is {}. It is HIGHLY recommended that queue" + ".target_size be at least twice queue.parallel_threads or higher.", + "queue.target_size is {}, and queue.parallel_threads is {}. It is HIGHLY recommended that queue" + + ".target_size be at least four times queue.parallel_threads or greater.", Settings.settings().QUEUE.TARGET_SIZE, Settings.settings().QUEUE.PARALLEL_THREADS ); diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/command/SuggestInputParseException.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/command/SuggestInputParseException.java index c38b388884..bd10e6d200 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/command/SuggestInputParseException.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/command/SuggestInputParseException.java @@ -1,6 +1,8 @@ package com.fastasyncworldedit.core.command; +import com.fastasyncworldedit.core.configuration.Caption; import com.sk89q.worldedit.extension.input.InputParseException; +import com.sk89q.worldedit.util.formatting.text.Component; import java.lang.reflect.InvocationTargetException; import java.util.List; @@ -13,33 +15,81 @@ public class SuggestInputParseException extends InputParseException { private final InputParseException cause; private final Supplier> getSuggestions; - private String prefix; + /** + * @deprecated Use {@link SuggestInputParseException#SuggestInputParseException(Component, Supplier)} + */ + @Deprecated(forRemoval = true, since = "TODO") public SuggestInputParseException(String msg, String prefix, Supplier> getSuggestions) { - this(new InputParseException(msg), prefix, getSuggestions); + this(new InputParseException(msg), getSuggestions); } + /** + * @deprecated Use {@link SuggestInputParseException#of(Throwable, Supplier)} + */ + @Deprecated(forRemoval = true, since = "TODO") public static SuggestInputParseException of(Throwable other, String prefix, Supplier> getSuggestions) { if (other instanceof InputParseException) { - return of((InputParseException) other, prefix, getSuggestions); + return of((InputParseException) other, getSuggestions); } - return of(new InputParseException(other.getMessage()), prefix, getSuggestions); + return of(new InputParseException(other.getMessage()), getSuggestions); } + /** + * @deprecated Use {@link SuggestInputParseException#of(InputParseException, Supplier)} + */ + @Deprecated(forRemoval = true, since = "TODO") public static SuggestInputParseException of(InputParseException other, String prefix, Supplier> getSuggestions) { if (other instanceof SuggestInputParseException) { return (SuggestInputParseException) other; } - return new SuggestInputParseException(other, prefix, getSuggestions); + return new SuggestInputParseException(other, getSuggestions); } + /** + * @deprecated Use {@link SuggestInputParseException#SuggestInputParseException(InputParseException, Supplier)} + */ + @Deprecated(forRemoval = true, since = "TODO") public SuggestInputParseException(InputParseException other, String prefix, Supplier> getSuggestions) { - super(other.getMessage()); + super(other.getRichMessage()); + checkNotNull(getSuggestions); + checkNotNull(other); + this.cause = other; + this.getSuggestions = getSuggestions; + } + + /** + * Create a new SuggestInputParseException instance + * + * @param message Message to send + * @param getSuggestions Supplier of list of sugegstions to give to user + * @since TODO + */ + public SuggestInputParseException(Component message, Supplier> getSuggestions) { + this(new InputParseException(message), getSuggestions); + } + + public static SuggestInputParseException of(Throwable other, Supplier> getSuggestions) { + if (other instanceof InputParseException) { + return of((InputParseException) other, getSuggestions); + } + //noinspection deprecation + return of(new InputParseException(other.getMessage()), getSuggestions); + } + + public static SuggestInputParseException of(InputParseException other, Supplier> getSuggestions) { + if (other instanceof SuggestInputParseException) { + return (SuggestInputParseException) other; + } + return new SuggestInputParseException(other, getSuggestions); + } + + public SuggestInputParseException(InputParseException other, Supplier> getSuggestions) { + super(other.getRichMessage()); checkNotNull(getSuggestions); checkNotNull(other); this.cause = other; this.getSuggestions = getSuggestions; - this.prefix = prefix; } public static SuggestInputParseException get(InvocationTargetException e) { @@ -54,7 +104,7 @@ public static SuggestInputParseException get(InvocationTargetException e) { } public static SuggestInputParseException of(String input, List values) { - throw new SuggestInputParseException("No value: " + input, input, () -> + throw new SuggestInputParseException(Caption.of("fawe.error.no-value-for-input", input), () -> values.stream() .map(Object::toString) .filter(v -> v.startsWith(input)) @@ -76,8 +126,12 @@ public List getSuggestions() { return getSuggestions.get(); } + /** + * @deprecated Unused + */ + @Deprecated(forRemoval = true, since = "TODO") public SuggestInputParseException prepend(String input) { - this.prefix = input + prefix; + // Do nothing return this; } diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/configuration/Settings.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/configuration/Settings.java index c2cb7704d9..5eba1f01fb 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/configuration/Settings.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/configuration/Settings.java @@ -102,11 +102,10 @@ public void reload(File file) { public FaweLimit getLimit(Actor actor) { FaweLimit limit; - if (actor.hasPermission("fawe.limit.*") || actor.hasPermission("fawe.bypass")) { - limit = FaweLimit.MAX.copy(); - } else { - limit = new FaweLimit(); + if (actor.hasPermission("fawe.bypass") || actor.hasPermission("fawe.limit.unlimited")) { + return FaweLimit.MAX.copy(); } + limit = new FaweLimit(); ArrayList keys = new ArrayList<>(LIMITS.getSections()); if (keys.remove("default")) { keys.add("default"); @@ -394,6 +393,7 @@ public static class LIMITS extends ConfigBlock { "Where block properties are specified, any blockstate with the property will be disallowed (e.g. all directions", "of a waterlogged fence). For blocking/remapping of all occurrences of a property like waterlogged, see", "remap-properties below.", + "To generate a blank list, substitute the default content with a set of square brackets [] instead.", "Example block property blocking:", " - \"minecraft:conduit[waterlogged=true]\"", " - \"minecraft:piston[extended=false,facing=west]\"", @@ -520,10 +520,10 @@ public static class QUEUE { " - A smaller value will reduce memory usage", " - A value too small may break some operations (deform?)", " - Values smaller than the configurated parallel-threads are not accepted", - " - It is recommended this option be at least 2x greater than parallel-threads" + " - It is recommended this option be at least 4x greater than parallel-threads" }) - public int TARGET_SIZE = 64; + public int TARGET_SIZE = 8 * Runtime.getRuntime().availableProcessors(); @Comment({ "Force FAWE to start placing chunks regardless of whether an edit is finished processing", " - A larger value will use slightly less CPU time", @@ -671,6 +671,14 @@ public static class WEB { }) public int MAX_IMAGE_SIZE = 8294400; + @Comment({ + "Whitelist of hostnames to allow images to be downloaded from", + " - Adding '*' to the list will allow any host, but this is NOT adviseable", + " - Crash exploits exist with malformed images", + " - See: https://medium.com/chargebee-engineering/perils-of-parsing-pixel-flood-attack-on-java-imageio-a97aeb06637d" + }) + public List ALLOWED_IMAGE_HOSTS = new ArrayList<>(Collections.singleton(("i.imgur.com"))); + } public static class EXTENT { @@ -694,7 +702,7 @@ public static class EXTENT { public static class TICK_LIMITER { @Comment("Enable the limiter") - public boolean ENABLED = true; + public boolean ENABLED = false; @Comment("The interval in ticks") public int INTERVAL = 20; @Comment("Max falling blocks per interval (per chunk)") @@ -703,12 +711,6 @@ public static class TICK_LIMITER { public int PHYSICS_MS = 10; @Comment("Max item spawns per interval (per chunk)") public int ITEMS = 256; - @Comment({ - "Whether fireworks can load chunks", - " - Fireworks usually travel vertically so do not load any chunks", - " - Horizontal fireworks can be hacked in to crash a server" - }) - public boolean FIREWORKS_LOAD_CHUNKS = false; } diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extension/factory/parser/mask/RichMaskParser.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extension/factory/parser/mask/RichMaskParser.java index 31c8f3d3e8..4e0fbfa918 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extension/factory/parser/mask/RichMaskParser.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extension/factory/parser/mask/RichMaskParser.java @@ -50,7 +50,7 @@ public RichMaskParser(WorldEdit worldEdit) { @Override public Mask parseFromInput(String input, ParserContext context) throws InputParseException { if (input.isEmpty()) { - throw new SuggestInputParseException("No input provided", "", () -> Stream + throw new SuggestInputParseException(Caption.of("fawe.error.no-input-provided"), () -> Stream .of("#", ",", "&") .map(n -> n + ":") .collect(Collectors.toList()) @@ -95,7 +95,6 @@ public Mask parseFromInput(String input, ParserContext context) throws InputPars "https://intellectualsites.github.io/fastasyncworldedit-documentation/patterns/patterns" )) )), - full, () -> { if (full.length() == 1) { return new ArrayList<>(worldEdit.getMaskFactory().getSuggestions("")); @@ -148,6 +147,7 @@ public Mask parseFromInput(String input, ParserContext context) throws InputPars try { builder.addRegex(full); } catch (InputParseException ignored) { + builder.clear(); context.setPreferringWildcard(false); context.setRestricted(false); BaseBlock block = worldEdit.getBlockFactory().parseFromInput(full, context); @@ -162,7 +162,6 @@ public Mask parseFromInput(String input, ParserContext context) throws InputPars "https://intellectualsites.github.io/fastasyncworldedit-documentation/masks/masks" )) )), - full, () -> { if (full.length() == 1) { return new ArrayList<>(worldEdit.getMaskFactory().getSuggestions("")); diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extension/factory/parser/pattern/RichPatternParser.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extension/factory/parser/pattern/RichPatternParser.java index 4971ee8781..3ebd84fa6e 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extension/factory/parser/pattern/RichPatternParser.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extension/factory/parser/pattern/RichPatternParser.java @@ -47,8 +47,7 @@ public RichPatternParser(WorldEdit worldEdit) { public Pattern parseFromInput(String input, ParserContext context) throws InputParseException { if (input.isEmpty()) { throw new SuggestInputParseException( - "No input provided", - "", + Caption.of("fawe.error.no-input-provided"), () -> Stream .concat(Stream.of("#", ",", "&"), BlockTypes.getNameSpaces().stream().map(n -> n + ":")) .collect(Collectors.toList()) @@ -88,7 +87,6 @@ public Pattern parseFromInput(String input, ParserContext context) throws InputP "https://intellectualsites.github.io/fastasyncworldedit-documentation/patterns/patterns" )) )), - full, () -> { if (full.length() == 1) { return new ArrayList<>(worldEdit.getPatternFactory().getSuggestions("")); diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/DisallowedBlocksExtent.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/DisallowedBlocksExtent.java index a60b11bc2e..7e5d54c984 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/DisallowedBlocksExtent.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/DisallowedBlocksExtent.java @@ -142,7 +142,7 @@ public IChunkSet processSet(final IChunk chunk, final IChunkGet get, final IChun BlockState state = BlockTypesCache.states[block]; if (blockedBlocks != null) { if (blockedBlocks.contains(state.getBlockType().getId())) { - blocks.setAt(i, 0); + blocks.setAt(i, BlockTypesCache.ReservedIDs.__RESERVED__); continue; } } @@ -151,7 +151,7 @@ public IChunkSet processSet(final IChunk chunk, final IChunkGet get, final IChun } for (FuzzyBlockState fuzzy : blockedStates) { if (fuzzy.equalsFuzzy(state)) { - blocks.setAt(i, 0); + blocks.setAt(i, BlockTypesCache.ReservedIDs.__RESERVED__); continue it; } } @@ -179,7 +179,7 @@ public Extent construct(final Extent child) { @Override public ProcessorScope getScope() { - return ProcessorScope.CHANGING_BLOCKS; + return ProcessorScope.REMOVING_BLOCKS; } } diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/FaweRegionExtent.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/FaweRegionExtent.java index 6fa2767e0e..84044f9b2e 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/FaweRegionExtent.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/FaweRegionExtent.java @@ -168,7 +168,7 @@ public Entity createEntity(Location location, BaseEntity entity, UUID uuid) { @Override public ProcessorScope getScope() { - return ProcessorScope.READING_SET_BLOCKS; + return ProcessorScope.REMOVING_BLOCKS; } } diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/DiskOptimizedClipboard.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/DiskOptimizedClipboard.java index 100e3265f8..b54c075fc8 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/DiskOptimizedClipboard.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/DiskOptimizedClipboard.java @@ -173,7 +173,6 @@ public DiskOptimizedClipboard(File file, int versionOverride) { nbtMap = new HashMap<>(); try { this.file = file; - checkFileLength(file); this.braf = new RandomAccessFile(file, "rw"); braf.setLength(file.length()); this.nbtBytesRemaining = Integer.MAX_VALUE - (int) file.length(); @@ -202,32 +201,6 @@ public DiskOptimizedClipboard(File file, int versionOverride) { } } - private void checkFileLength(File file) throws IOException { - long expectedFileSize = headerSize + ((long) getVolume() << 1); - if (file.length() > Integer.MAX_VALUE) { - if (expectedFileSize >= Integer.MAX_VALUE) { - throw new IOException(String.format( - "Cannot load clipboard of file size: %d > 2147483647 bytes (2.147 GiB), " + "volume: %d blocks", - file.length(), - getVolume() - )); - } else { - throw new IOException(String.format( - "Cannot load clipboard of file size > 2147483647 bytes (2.147 GiB). Possible corrupt file? Mismatch" + - " between volume `%d` and file length `%d`!", - file.length(), - getVolume() - )); - } - } else if (expectedFileSize != file.length()) { - throw new IOException(String.format( - "Possible corrupt clipboard file? Mismatch between expected file size `%d` and actual file size `%d`!", - expectedFileSize, - file.length() - )); - } - } - /** * Attempt to load a file into a new {@link DiskOptimizedClipboard} instance. Will attempt to recover on version mismatch * failure. diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/processor/MultiBatchProcessor.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/processor/MultiBatchProcessor.java index 85fe20986b..e57ccb4903 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/processor/MultiBatchProcessor.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/processor/MultiBatchProcessor.java @@ -257,7 +257,7 @@ public ProcessorScope getScope() { for (IBatchProcessor processor : processors) { scope = Math.max(scope, processor.getScope().intValue()); } - return ProcessorScope.valueOf(0); + return ProcessorScope.valueOf(scope); } /** diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/processor/ProcessorScope.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/processor/ProcessorScope.java index 7fdd3e67ac..e3f09e0a33 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/processor/ProcessorScope.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/processor/ProcessorScope.java @@ -3,9 +3,9 @@ /** * The scope of a processor. * Order in which processors are executed: - * - ADDING_BLOCKS (processors that strictly ADD blocks to an edit ONLY) - * - CHANGING_BLOCKS (processors that strictly ADD or CHANGE blocks being set) - * - REMOVING_BLOCKS (processors that string ADD, CHANGE or REMOVE blocks being set) + * - ADDING_BLOCKS (processors that may ADD blocks to an edit ONLY) + * - CHANGING_BLOCKS (processors that may ADD or CHANGE blocks being set) + * - REMOVING_BLOCKS (processors that may ADD, CHANGE or REMOVE blocks being set) * - CUSTOM (processors that do not specify a SCOPE) * - READING_SET_BLOCKS (processors that do not alter blocks at all, and read the blocks that are actually going to set, e.g. history processors) */ diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/function/mask/BlockMaskBuilder.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/function/mask/BlockMaskBuilder.java index 0b89e0adad..5bfeb766ca 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/function/mask/BlockMaskBuilder.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/function/mask/BlockMaskBuilder.java @@ -178,8 +178,7 @@ public BlockMaskBuilder addRegex(final String input) throws InputParseException } if (operator == null) { throw new SuggestInputParseException( - "No operator for " + input, - "", + Caption.of("fawe.error.no-operator-for-input", input), () -> Arrays.asList("=", "~", "!", "<", ">", "<=", ">=") ); } @@ -195,7 +194,7 @@ public BlockMaskBuilder addRegex(final String input) throws InputParseException String value = charSequence.toString(); final PropertyKey fKey = key; Collection types = type != null ? Collections.singleton(type) : blockTypeList; - throw new SuggestInputParseException("No value for " + input, input, () -> { + throw new SuggestInputParseException(Caption.of("fawe.error.no-value-for-input", input), () -> { HashSet values = new HashSet<>(); types.stream().filter(t -> t.hasProperty(fKey)).forEach(t -> { Property p = t.getProperty(fKey); @@ -287,7 +286,7 @@ private boolean has(BlockType type, Property property, int index) { } private void suggest(String input, String property, Collection finalTypes) throws InputParseException { - throw new SuggestInputParseException(input + " does not have: " + property, input, () -> { + throw new SuggestInputParseException(Caption.of("worldedit.error.parser.unknown-property", property, input), () -> { Set keys = PropertyKeySet.empty(); finalTypes.forEach(t -> t.getProperties().forEach(p -> keys.add(p.getKey()))); return keys.stream().map(PropertyKey::getName) diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/limit/FaweLimit.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/limit/FaweLimit.java index c899c668d7..00a47178ca 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/limit/FaweLimit.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/limit/FaweLimit.java @@ -2,6 +2,7 @@ import com.fastasyncworldedit.core.FaweCache; +import java.util.Collections; import java.util.Set; public class FaweLimit { @@ -114,10 +115,10 @@ public void THROW_MAX_ENTITIES(int amt) { MAX.FAST_PLACEMENT = true; MAX.CONFIRM_LARGE = true; MAX.RESTRICT_HISTORY_TO_REGIONS = false; - MAX.STRIP_NBT = null; + MAX.STRIP_NBT = Collections.emptySet(); MAX.UNIVERSAL_DISALLOWED_BLOCKS = false; - MAX.DISALLOWED_BLOCKS = null; - MAX.REMAP_PROPERTIES = null; + MAX.DISALLOWED_BLOCKS = Collections.emptySet(); + MAX.REMAP_PROPERTIES = Collections.emptySet(); } public boolean MAX_CHANGES() { @@ -241,7 +242,7 @@ public boolean isUnlimited() { && FAST_PLACEMENT && !RESTRICT_HISTORY_TO_REGIONS && (STRIP_NBT == null || STRIP_NBT.isEmpty()) - && !UNIVERSAL_DISALLOWED_BLOCKS + // && !UNIVERSAL_DISALLOWED_BLOCKS --> do not include this, it effectively has no relevance && (DISALLOWED_BLOCKS == null || DISALLOWED_BLOCKS.isEmpty()) && (REMAP_PROPERTIES == null || REMAP_PROPERTIES.isEmpty()); } diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/math/BlockVector3ChunkMap.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/math/BlockVector3ChunkMap.java index 775f96e4d0..a9f8a0210b 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/math/BlockVector3ChunkMap.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/math/BlockVector3ChunkMap.java @@ -9,7 +9,20 @@ public class BlockVector3ChunkMap implements IAdaptedMap { - private final Int2ObjectArrayMap map = new Int2ObjectArrayMap<>(); + private final Int2ObjectArrayMap map; + + public BlockVector3ChunkMap() { + map = new Int2ObjectArrayMap<>(); + } + + /** + * Create a new instance that is a copy of an existing map + * + * @param map existing map to copy + */ + public BlockVector3ChunkMap(BlockVector3ChunkMap map) { + this.map = new Int2ObjectArrayMap<>(map.getParent()); + } @Override public Map getParent() { diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/IChunkSet.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/IChunkSet.java index f19a4ff59a..763b65fc35 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/IChunkSet.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/IChunkSet.java @@ -9,8 +9,9 @@ import com.sk89q.worldedit.world.biome.BiomeType; import com.sk89q.worldedit.world.block.BlockStateHolder; +import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.util.HashMap; +import java.util.EnumMap; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -96,7 +97,7 @@ default int getBitMask() { } default Map getHeightMaps() { - return new HashMap<>(); + return new EnumMap<>(HeightMapType.class); } @Override @@ -116,4 +117,15 @@ default Operation commit() { */ boolean hasBiomes(int layer); + /** + * Create an entirely distinct copy of this SET instance. All mutable data must be copied to sufficiently prevent leakage + * between the copy and the original. + * + * @return distinct new {@link IChunkSet instance} + */ + @Nonnull + default IChunkSet createCopy() { + return this; + } + } diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/QueueHandler.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/QueueHandler.java index 49ebd84ca6..2091a0bb9a 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/QueueHandler.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/QueueHandler.java @@ -53,7 +53,6 @@ public abstract class QueueHandler implements Trimable, Runnable { */ private long last; private long allocate = 50; - private double targetTPS = 18; public QueueHandler() { TaskManager.taskManager().repeat(this, 1); @@ -87,7 +86,7 @@ public boolean isUnderutilized() { private long getAllocate() { long now = System.currentTimeMillis(); - targetTPS = 18 - Math.max(Settings.settings().QUEUE.EXTRA_TIME_MS * 0.05, 0); + double targetTPS = 18 - Math.max(Settings.settings().QUEUE.EXTRA_TIME_MS * 0.05, 0); long diff = 50 + this.last - (this.last = now); long absDiff = Math.abs(diff); if (diff == 0) { diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/SingleThreadQueueExtent.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/SingleThreadQueueExtent.java index 56f82fd509..d13b85fe2d 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/SingleThreadQueueExtent.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/SingleThreadQueueExtent.java @@ -275,8 +275,8 @@ public synchronized boolean trim(boolean aggressive) { * Get a new IChunk from either the pool, or create a new one
+ Initialize it at the * coordinates * - * @param chunkX - * @param chunkZ + * @param chunkX X chunk coordinate + * @param chunkZ Z chunk coordinate * @return IChunk */ private ChunkHolder poolOrCreate(int chunkX, int chunkZ) { @@ -309,19 +309,11 @@ public final IQueueChunk getOrCreateChunk(int x, int z) { // If queueing is enabled AND either of the following // - memory is low & queue size > num threads + 8 // - queue size > target size and primary queue has less than num threads submissions - if (enabledQueue && ((lowMem && size > Settings.settings().QUEUE.PARALLEL_THREADS + 8) || (size > Settings.settings().QUEUE.TARGET_SIZE && Fawe - .instance() - .getQueueHandler() - .isUnderutilized()))) { + int targetSize = lowMem ? Settings.settings().QUEUE.PARALLEL_THREADS + 8 : Settings.settings().QUEUE.TARGET_SIZE; + if (enabledQueue && size > targetSize && (lowMem || Fawe.instance().getQueueHandler().isUnderutilized())) { chunk = chunks.removeFirst(); final Future future = submitUnchecked(chunk); if (future != null && !future.isDone()) { - final int targetSize; - if (lowMem) { - targetSize = Settings.settings().QUEUE.PARALLEL_THREADS + 8; - } else { - targetSize = Settings.settings().QUEUE.TARGET_SIZE; - } pollSubmissions(targetSize, lowMem); submissions.add(future); } diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/blocks/DataArraySetBlocks.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/blocks/DataArraySetBlocks.java index d56c3d7acc..e6920ee487 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/blocks/DataArraySetBlocks.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/blocks/DataArraySetBlocks.java @@ -15,12 +15,11 @@ import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; +import java.util.EnumMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.UUID; -import java.util.stream.IntStream; public class DataArraySetBlocks extends DataArrayBlocks implements IChunkSet { @@ -40,7 +39,7 @@ public static DataArraySetBlocks newInstance() { public BlockVector3ChunkMap tiles; public HashSet entities; public HashSet entityRemoves; - public Map heightMaps; + public EnumMap heightMaps; private boolean fastMode = false; private int bitMask = -1; @@ -87,7 +86,7 @@ public Set getEntityRemoves() { @Override public Map getHeightMaps() { - return heightMaps == null ? new HashMap<>() : heightMaps; + return heightMaps == null ? new EnumMap<>(HeightMapType.class) : heightMaps; } @Override @@ -171,7 +170,7 @@ public void setSkyLight(int x, int y, int z, int value) { @Override public void setHeightMap(HeightMapType type, int[] heightMap) { if (heightMaps == null) { - heightMaps = new HashMap<>(); + heightMaps = new EnumMap<>(HeightMapType.class); } heightMaps.put(type, heightMap); } @@ -300,8 +299,12 @@ public boolean isEmpty() { || (heightMaps != null && !heightMaps.isEmpty())) { return false; } - //noinspection SimplifyStreamApiCallChains - this is faster than using #noneMatch - return !IntStream.range(minSectionPosition, maxSectionPosition + 1).anyMatch(this::hasSection); + for (int i = minSectionPosition; i <= maxSectionPosition; i++) { + if (hasSection(i)) { + return false; + } + } + return true; } @Override @@ -310,6 +313,9 @@ public IChunkSet reset() { tiles = null; entities = null; entityRemoves = null; + light = null; + skyLight = null; + heightMaps = null; super.reset(); return null; } @@ -323,6 +329,62 @@ public boolean hasBiomes(int layer) { return biomes != null && biomes[layer] != null; } + @Override + public ThreadUnsafeCharBlocks createCopy() { + char[][] blocksCopy = new char[sectionCount][]; + for (int i = 0; i < sectionCount; i++) { + if (blocks[i] != null) { + blocksCopy[i] = new char[FaweCache.INSTANCE.BLOCKS_PER_LAYER]; + System.arraycopy(blocks[i], 0, blocksCopy[i], 0, FaweCache.INSTANCE.BLOCKS_PER_LAYER); + } + } + BiomeType[][] biomesCopy; + if (biomes == null) { + biomesCopy = null; + } else { + biomesCopy = new BiomeType[sectionCount][]; + for (int i = 0; i < sectionCount; i++) { + if (biomes[i] != null) { + biomesCopy[i] = new BiomeType[biomes[i].length]; + System.arraycopy(biomes[i], 0, biomesCopy[i], 0, biomes[i].length); + } + } + } + char[][] lightCopy = createLightCopy(light, sectionCount); + char[][] skyLightCopy = createLightCopy(skyLight, sectionCount); + return new ThreadUnsafeCharBlocks( + blocksCopy, + minSectionPosition, + maxSectionPosition, + biomesCopy, + sectionCount, + lightCopy, + skyLightCopy, + tiles != null ? new BlockVector3ChunkMap<>(tiles) : null, + entities != null ? new HashSet<>(entities) : null, + entityRemoves != null ? new HashSet<>(entityRemoves) : null, + heightMaps != null ? new EnumMap<>(heightMaps) : null, + defaultOrdinal(), + fastMode, + bitMask + ); + } + + static char[][] createLightCopy(char[][] lightArr, int sectionCount) { + if (lightArr == null) { + return null; + } else { + char[][] lightCopy = new char[sectionCount][]; + for (int i = 0; i < sectionCount; i++) { + if (lightArr[i] != null) { + lightCopy[i] = new char[lightArr[i].length]; + System.arraycopy(lightArr[i], 0, lightCopy[i], 0, lightArr[i].length); + } + } + return lightCopy; + } + } + @Override public DataArray load(final int layer) { updateSectionIndexRange(layer); diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/blocks/ThreadUnsafeCharBlocks.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/blocks/ThreadUnsafeCharBlocks.java new file mode 100644 index 0000000000..a423b32862 --- /dev/null +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/blocks/ThreadUnsafeCharBlocks.java @@ -0,0 +1,536 @@ +package com.fastasyncworldedit.core.queue.implementation.blocks; + +import com.fastasyncworldedit.core.Fawe; +import com.fastasyncworldedit.core.FaweCache; +import com.fastasyncworldedit.core.extent.processor.heightmap.HeightMapType; +import com.fastasyncworldedit.core.math.BlockVector3ChunkMap; +import com.fastasyncworldedit.core.queue.IBlocks; +import com.fastasyncworldedit.core.queue.IChunkSet; +import com.sk89q.jnbt.CompoundTag; +import com.sk89q.worldedit.internal.util.LogManagerCompat; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.world.biome.BiomeType; +import com.sk89q.worldedit.world.block.BlockState; +import com.sk89q.worldedit.world.block.BlockStateHolder; +import com.sk89q.worldedit.world.block.BlockTypesCache; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/** + * Equivalent to {@link CharSetBlocks} without any attempt to make thread-safe for improved performance. + * This is currently only used as a "copy" of {@link CharSetBlocks} to provide to + * {@link com.fastasyncworldedit.core.queue.IBatchProcessor} instances for processing without overlapping the continuing edit. + * + * @since TODO + */ +public class ThreadUnsafeCharBlocks implements IChunkSet, IBlocks { + + private static final Logger LOGGER = LogManagerCompat.getLogger(); + + private final char defaultOrdinal; + private char[][] blocks; + private int minSectionPosition; + private int maxSectionPosition; + private int sectionCount; + private BiomeType[][] biomes; + private char[][] light; + private char[][] skyLight; + private BlockVector3ChunkMap tiles; + private HashSet entities; + private HashSet entityRemoves; + private Map heightMaps; + private boolean fastMode; + private int bitMask; + + /** + * New instance given the data stored in a {@link CharSetBlocks} instance. + * + * @since TODO + */ + ThreadUnsafeCharBlocks( + char[][] blocks, + int minSectionPosition, + int maxSectionPosition, + BiomeType[][] biomes, + int sectionCount, + char[][] light, + char[][] skyLight, + BlockVector3ChunkMap tiles, + HashSet entities, + HashSet entityRemoves, + Map heightMaps, + char defaultOrdinal, + boolean fastMode, + int bitMask + ) { + this.blocks = blocks; + this.minSectionPosition = minSectionPosition; + this.maxSectionPosition = maxSectionPosition; + this.biomes = biomes; + this.sectionCount = sectionCount; + this.light = light; + this.skyLight = skyLight; + this.tiles = tiles; + this.entities = entities; + this.entityRemoves = entityRemoves; + this.heightMaps = heightMaps; + this.defaultOrdinal = defaultOrdinal; + this.fastMode = fastMode; + this.bitMask = bitMask; + } + + @Override + public boolean hasSection(int layer) { + layer -= minSectionPosition; + return layer >= 0 && layer < blocks.length && blocks[layer] != null && blocks[layer].length == FaweCache.INSTANCE.BLOCKS_PER_LAYER; + } + + @Override + public char[] load(int layer) { + updateSectionIndexRange(layer); + layer -= minSectionPosition; + return blocks[layer]; + } + + @Nullable + @Override + public char[] loadIfPresent(int layer) { + layer -= minSectionPosition; + return blocks[layer]; + } + + @Override + public Map getTiles() { + return tiles == null ? Collections.emptyMap() : tiles; + } + + @Override + public CompoundTag getTile(int x, int y, int z) { + return tiles.get(x, y, z); + } + + @Override + public Set getEntities() { + return entities == null ? Collections.emptySet() : entities; + } + + @Override + public Map getHeightMaps() { + return heightMaps == null ? new HashMap<>() : heightMaps; + } + + @Override + public void removeSectionLighting(int layer, boolean sky) { + updateSectionIndexRange(layer); + layer -= minSectionPosition; + if (light == null) { + light = new char[sectionCount][]; + } + if (light[layer] == null) { + light[layer] = new char[4096]; + } + Arrays.fill(light[layer], (char) 0); + if (sky) { + if (skyLight == null) { + skyLight = new char[sectionCount][]; + } + if (skyLight[layer] == null) { + skyLight[layer] = new char[4096]; + } + Arrays.fill(skyLight[layer], (char) 0); + } + } + + @Override + public boolean trim(boolean aggressive, int layer) { + return false; + } + + @Override + public int getSectionCount() { + return sectionCount; + } + + @Override + public int getMaxSectionPosition() { + return maxSectionPosition; + } + + @Override + public int getMinSectionPosition() { + return minSectionPosition; + } + + public char get(int x, int y, int z) { + int layer = (y >> 4); + if (!hasSection(layer)) { + return defaultOrdinal; + } + final int index = (y & 15) << 8 | z << 4 | x; + return blocks[layer - minSectionPosition][index]; + } + + @Override + public BiomeType getBiomeType(int x, int y, int z) { + int layer; + if (biomes == null || (y >> 4) < minSectionPosition || (y >> 4) > maxSectionPosition) { + return null; + } else if (biomes[(layer = (y >> 4) - minSectionPosition)] == null) { + return null; + } + return biomes[layer][(y & 15) >> 2 | (z >> 2) << 2 | x >> 2]; + } + + @Override + public BlockState getBlock(int x, int y, int z) { + return BlockTypesCache.states[get(x, y, z)]; + } + + @Override + public boolean setBiome(int x, int y, int z, BiomeType biome) { + updateSectionIndexRange(y >> 4); + int layer = (y >> 4) - minSectionPosition; + if (biomes == null) { + biomes = new BiomeType[sectionCount][]; + biomes[layer] = new BiomeType[64]; + } else if (biomes[layer] == null) { + biomes[layer] = new BiomeType[64]; + } + biomes[layer][(y & 12) << 2 | (z & 12) | (x & 12) >> 2] = biome; + return true; + } + + @Override + public boolean setBiome(BlockVector3 position, BiomeType biome) { + return setBiome(position.getX(), position.getY(), position.getZ(), biome); + } + + public void set(int x, int y, int z, char value) { + final int layer = y >> 4; + final int index = (y & 15) << 8 | z << 4 | x; + try { + blocks[layer][index] = value; + } catch (ArrayIndexOutOfBoundsException exception) { + LOGGER.error("Tried setting block at coordinates (" + x + "," + y + "," + z + ")"); + assert Fawe.platform() != null; + LOGGER.error("Layer variable was = {}", layer, exception); + } + } + + @Override + public > boolean setBlock(int x, int y, int z, T holder) { + updateSectionIndexRange(y >> 4); + set(x, y, z, holder.getOrdinalChar()); + holder.applyTileEntity(this, x, y, z); + return true; + } + + @Override + public void setBlocks(int layer, char[] data) { + updateSectionIndexRange(layer); + layer -= minSectionPosition; + this.blocks[layer] = data; + } + + @Override + public boolean isEmpty() { + if (biomes != null + || light != null + || skyLight != null + || (entities != null && !entities.isEmpty()) + || (tiles != null && !tiles.isEmpty()) + || (entityRemoves != null && !entityRemoves.isEmpty()) + || (heightMaps != null && !heightMaps.isEmpty())) { + return false; + } + for (int i = minSectionPosition; i <= maxSectionPosition; i++) { + if (hasSection(i)) { + return false; + } + } + return true; + } + + @Override + public boolean setTile(int x, int y, int z, CompoundTag tile) { + updateSectionIndexRange(y >> 4); + if (tiles == null) { + tiles = new BlockVector3ChunkMap<>(); + } + tiles.put(x, y, z, tile); + return true; + } + + @Override + public void setBlockLight(int x, int y, int z, int value) { + updateSectionIndexRange(y >> 4); + if (light == null) { + light = new char[sectionCount][]; + } + final int layer = (y >> 4) - minSectionPosition; + if (light[layer] == null) { + char[] c = new char[4096]; + Arrays.fill(c, (char) 16); + light[layer] = c; + } + final int index = (y & 15) << 8 | (z & 15) << 4 | (x & 15); + light[layer][index] = (char) value; + } + + @Override + public void setSkyLight(int x, int y, int z, int value) { + updateSectionIndexRange(y >> 4); + if (skyLight == null) { + skyLight = new char[sectionCount][]; + } + final int layer = (y >> 4) - minSectionPosition; + if (skyLight[layer] == null) { + char[] c = new char[4096]; + Arrays.fill(c, (char) 16); + skyLight[layer] = c; + } + final int index = (y & 15) << 8 | (z & 15) << 4 | (x & 15); + skyLight[layer][index] = (char) value; + } + + @Override + public void setHeightMap(HeightMapType type, int[] heightMap) { + if (heightMaps == null) { + heightMaps = new EnumMap<>(HeightMapType.class); + } + heightMaps.put(type, heightMap); + } + + @Override + public void setLightLayer(int layer, char[] toSet) { + updateSectionIndexRange(layer); + if (light == null) { + light = new char[sectionCount][]; + } + layer -= minSectionPosition; + light[layer] = toSet; + } + + @Override + public void setSkyLightLayer(int layer, char[] toSet) { + updateSectionIndexRange(layer); + if (skyLight == null) { + skyLight = new char[sectionCount][]; + } + layer -= minSectionPosition; + skyLight[layer] = toSet; + } + + @Override + public void setFullBright(int layer) { + updateSectionIndexRange(layer); + layer -= minSectionPosition; + if (light == null) { + light = new char[sectionCount][]; + } + if (light[layer] == null) { + light[layer] = new char[4096]; + } + if (skyLight == null) { + skyLight = new char[sectionCount][]; + } + if (skyLight[layer] == null) { + skyLight[layer] = new char[4096]; + } + Arrays.fill(light[layer], (char) 15); + Arrays.fill(skyLight[layer], (char) 15); + } + + @Override + public void setEntity(CompoundTag tag) { + if (entities == null) { + entities = new HashSet<>(); + } + entities.add(tag); + } + + @Override + public void removeEntity(UUID uuid) { + if (entityRemoves == null) { + entityRemoves = new HashSet<>(); + } + entityRemoves.add(uuid); + } + + @Override + public void setFastMode(boolean fastMode) { + this.fastMode = fastMode; + } + + @Override + public boolean isFastMode() { + return fastMode; + } + + @Override + public void setBitMask(int bitMask) { + this.bitMask = bitMask; + } + + @Override + public int getBitMask() { + return bitMask; + } + + @Override + public Set getEntityRemoves() { + return entityRemoves == null ? Collections.emptySet() : entityRemoves; + } + + @Override + public BiomeType[][] getBiomes() { + return biomes; + } + + @Override + public boolean hasBiomes() { + return IChunkSet.super.hasBiomes(); + } + + @Override + public char[][] getLight() { + return light; + } + + @Override + public char[][] getSkyLight() { + return skyLight; + } + + @Override + public boolean hasLight() { + return IChunkSet.super.hasLight(); + } + + @Override + public IChunkSet reset() { + blocks = new char[sectionCount][]; + biomes = new BiomeType[sectionCount][]; + light = new char[sectionCount][]; + skyLight = new char[sectionCount][]; + tiles.clear(); + entities.clear(); + entityRemoves.clear(); + heightMaps.clear(); + return this; + } + + @Override + public boolean hasBiomes(int layer) { + layer -= minSectionPosition; + return layer >= 0 && layer < biomes.length && biomes[layer] != null && biomes[layer].length > 0; + } + + @Override + public IChunkSet createCopy() { + char[][] blocksCopy = new char[sectionCount][]; + for (int i = 0; i < sectionCount; i++) { + if (blocks[i] != null) { + blocksCopy[i] = new char[FaweCache.INSTANCE.BLOCKS_PER_LAYER]; + System.arraycopy(blocks[i], 0, blocksCopy[i], 0, FaweCache.INSTANCE.BLOCKS_PER_LAYER); + } + } + BiomeType[][] biomesCopy; + if (biomes == null) { + biomesCopy = null; + } else { + biomesCopy = new BiomeType[sectionCount][]; + for (int i = 0; i < sectionCount; i++) { + if (biomes[i] != null) { + biomesCopy[i] = new BiomeType[biomes[i].length]; + System.arraycopy(biomes[i], 0, biomesCopy[i], 0, biomes[i].length); + } + } + } + char[][] lightCopy = CharSetBlocks.createLightCopy(light, sectionCount); + char[][] skyLightCopy = CharSetBlocks.createLightCopy(skyLight, sectionCount); + return new ThreadUnsafeCharBlocks( + blocksCopy, + minSectionPosition, + maxSectionPosition, + biomesCopy, + sectionCount, + lightCopy, + skyLightCopy, + tiles != null ? new BlockVector3ChunkMap<>(tiles) : null, + entities != null ? new HashSet<>(entities) : null, + entityRemoves != null ? new HashSet<>(entityRemoves) : null, + heightMaps != null ? new HashMap<>(heightMaps) : null, + defaultOrdinal, + fastMode, + bitMask + ); + } + + @Override + public boolean trim(boolean aggressive) { + return false; + } + + // Checks and updates the various section arrays against the new layer index + private void updateSectionIndexRange(int layer) { + if (layer >= minSectionPosition && layer <= maxSectionPosition) { + return; + } + if (layer < minSectionPosition) { + int diff = minSectionPosition - layer; + sectionCount += diff; + char[][] tmpBlocks = new char[sectionCount][]; + System.arraycopy(blocks, 0, tmpBlocks, diff, blocks.length); + blocks = tmpBlocks; + minSectionPosition = layer; + if (biomes != null) { + BiomeType[][] tmpBiomes = new BiomeType[sectionCount][64]; + System.arraycopy(biomes, 0, tmpBiomes, diff, biomes.length); + biomes = tmpBiomes; + } + if (light != null) { + char[][] tmplight = new char[sectionCount][]; + System.arraycopy(light, 0, tmplight, diff, light.length); + light = tmplight; + } + if (skyLight != null) { + char[][] tmplight = new char[sectionCount][]; + System.arraycopy(skyLight, 0, tmplight, diff, skyLight.length); + skyLight = tmplight; + } + } else { + int diff = layer - maxSectionPosition; + sectionCount += diff; + char[][] tmpBlocks = new char[sectionCount][]; + System.arraycopy(blocks, 0, tmpBlocks, 0, blocks.length); + blocks = tmpBlocks; + maxSectionPosition = layer; + if (biomes != null) { + BiomeType[][] tmpBiomes = new BiomeType[sectionCount][64]; + System.arraycopy(biomes, 0, tmpBiomes, 0, biomes.length); + biomes = tmpBiomes; + } + if (light != null) { + char[][] tmplight = new char[sectionCount][]; + System.arraycopy(light, 0, tmplight, 0, light.length); + light = tmplight; + } + if (skyLight != null) { + char[][] tmplight = new char[sectionCount][]; + System.arraycopy(skyLight, 0, tmplight, 0, skyLight.length); + skyLight = tmplight; + } + } + } + +} diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/chunk/ChunkHolder.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/chunk/ChunkHolder.java index cbbdbc92e4..c5f671cb91 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/chunk/ChunkHolder.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/queue/implementation/chunk/ChunkHolder.java @@ -1045,7 +1045,7 @@ public synchronized T call() { if (chunkSet != null && !chunkSet.isEmpty()) { chunkSet.setBitMask(bitMask); try { - return this.call(chunkSet, () -> { + return this.call(chunkSet.createCopy(), () -> { this.delegate = NULL; chunkSet = null; calledLock.unlock(stamp); diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/MainUtil.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/MainUtil.java index 8deb59d138..8122a840c6 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/MainUtil.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/MainUtil.java @@ -52,6 +52,7 @@ import java.lang.reflect.Array; import java.net.HttpURLConnection; import java.net.MalformedURLException; +import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; @@ -68,7 +69,6 @@ import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Arrays; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -81,10 +81,8 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.regex.Pattern; -import java.util.zip.DataFormatException; import java.util.zip.Deflater; import java.util.zip.GZIPInputStream; -import java.util.zip.Inflater; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -533,6 +531,21 @@ public static BufferedImage readImage(File file) throws IOException { return readImage(new FileInputStream(file)); } + public static void checkImageHost(URI uri) throws IOException { + if (Settings.settings().WEB.ALLOWED_IMAGE_HOSTS.contains("*")) { + return; + } + String host = uri.getHost(); + if (Settings.settings().WEB.ALLOWED_IMAGE_HOSTS.stream().anyMatch(host::equalsIgnoreCase)) { + return; + } + throw new IOException(String.format( + "Host `%s` not allowed! Whitelisted image hosts are: %s", + host, + StringMan.join(Settings.settings().WEB.ALLOWED_IMAGE_HOSTS, ", ") + )); + } + public static BufferedImage toRGB(BufferedImage src) { if (src == null) { return src; diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/WEManager.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/WEManager.java index 85f4eb2d1c..439b486e36 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/WEManager.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/WEManager.java @@ -130,7 +130,9 @@ public Region[] getMask(Player player, FaweMaskManager.MaskType type, final bool backupRegions.add(region); } } else { - player.print(Caption.of("fawe.error.region-mask-invalid", mask.getClass().getSimpleName())); + if (Settings.settings().ENABLED_COMPONENTS.DEBUG) { + player.printDebug(Caption.of("fawe.error.region-mask-invalid", mask.getClass().getSimpleName())); + } removed = true; iterator.remove(); } diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/collection/SimpleRandomCollection.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/collection/SimpleRandomCollection.java index d67227abed..dc21075598 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/collection/SimpleRandomCollection.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/collection/SimpleRandomCollection.java @@ -36,7 +36,7 @@ public void add(double weight, E result) { @Override public E next(int x, int y, int z) { - return map.ceilingEntry(getRandom().nextDouble(x, y, z)).getValue(); + return map.ceilingEntry(getRandom().nextDouble(x, y, z) * this.total).getValue(); } } diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/image/ImageUtil.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/image/ImageUtil.java index a8998e1f05..f99a17e6f1 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/image/ImageUtil.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/image/ImageUtil.java @@ -203,6 +203,7 @@ public static BufferedImage getImage(String arg) throws InputParseException { arg = "https://i.imgur.com/" + arg.split("imgur.com/")[1] + ".png"; } URL url = new URL(arg); + MainUtil.checkImageHost(url.toURI()); BufferedImage img = MainUtil.readImage(url); if (img == null) { throw new IOException("Failed to read " + url + ", please try again later"); @@ -218,7 +219,7 @@ public static BufferedImage getImage(String arg) throws InputParseException { return MainUtil.readImage(file); } throw new InputParseException(Caption.of("fawe.error.invalid-image", TextComponent.of(arg))); - } catch (IOException e) { + } catch (IOException | URISyntaxException e) { throw new InputParseException(TextComponent.of(e.getMessage())); } } @@ -229,7 +230,9 @@ public static URI getImageURI(String arg) throws InputParseException { if (arg.contains("imgur.com") && !arg.contains("i.imgur.com")) { arg = "https://i.imgur.com/" + arg.split("imgur.com/")[1] + ".png"; } - return new URL(arg).toURI(); + URI uri = new URI(arg); + MainUtil.checkImageHost(uri); + return uri; } if (arg.startsWith("file:/")) { arg = arg.replaceFirst("file:/+", ""); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/EditSessionBuilder.java b/worldedit-core/src/main/java/com/sk89q/worldedit/EditSessionBuilder.java index c235ca7845..2512af96c0 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/EditSessionBuilder.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/EditSessionBuilder.java @@ -531,16 +531,14 @@ public EditSessionBuilder compile() { } if (allowedRegions == null && Settings.settings().REGION_RESTRICTIONS) { if (actor != null && !actor.hasPermission("fawe.bypass.regions")) { - if (actor instanceof Player) { - Player player = (Player) actor; + if (actor instanceof Player player) { allowedRegions = player.getAllowedRegions(); } } } if (disallowedRegions == null && Settings.settings().REGION_RESTRICTIONS && Settings.settings().REGION_RESTRICTIONS_OPTIONS.ALLOW_BLACKLISTS) { if (actor != null && !actor.hasPermission("fawe.bypass.regions")) { - if (actor instanceof Player) { - Player player = (Player) actor; + if (actor instanceof Player player) { disallowedRegions = player.getDisallowedRegions(); } } @@ -561,6 +559,9 @@ public EditSessionBuilder compile() { } } } + if (placeChunks && regionExtent != null) { + queue.addProcessor(regionExtent); + } // There's no need to do the below (and it'll also just be a pain to implement) if we're not placing chunks if (placeChunks) { if (((relightMode != null && relightMode != RelightMode.NONE) || (relightMode == null && Settings.settings().LIGHTING.MODE > 0))) { @@ -597,15 +598,14 @@ public EditSessionBuilder compile() { this.extent = regionExtent; } if (this.limit != null && this.limit.STRIP_NBT != null && !this.limit.STRIP_NBT.isEmpty()) { + this.extent = new StripNBTExtent(this.extent, this.limit.STRIP_NBT); if (placeChunks) { - queue.addProcessor(new StripNBTExtent(this.extent, this.limit.STRIP_NBT)); - } else { - this.extent = new StripNBTExtent(this.extent, this.limit.STRIP_NBT); + queue.addProcessor((IBatchProcessor) this.extent); } } if (this.limit != null && !this.limit.isUnlimited()) { Set limitBlocks = new HashSet<>(); - if ((getActor() == null || getActor().hasPermission("worldedit.anyblock")) && this.limit.UNIVERSAL_DISALLOWED_BLOCKS) { + if (getActor() != null && !getActor().hasPermission("worldedit.anyblock") && this.limit.UNIVERSAL_DISALLOWED_BLOCKS) { limitBlocks.addAll(WorldEdit.getInstance().getConfiguration().disallowedBlocks); } if (this.limit.DISALLOWED_BLOCKS != null && !this.limit.DISALLOWED_BLOCKS.isEmpty()) { @@ -613,10 +613,9 @@ public EditSessionBuilder compile() { } Set> remaps = this.limit.REMAP_PROPERTIES; if (!limitBlocks.isEmpty() || (remaps != null && !remaps.isEmpty())) { + this.extent = new DisallowedBlocksExtent(this.extent, limitBlocks, remaps); if (placeChunks) { - queue.addProcessor(new DisallowedBlocksExtent(this.extent, limitBlocks, remaps)); - } else { - this.extent = new DisallowedBlocksExtent(this.extent, limitBlocks, remaps); + queue.addProcessor((IBatchProcessor) this.extent); } } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/BiomeCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/BiomeCommands.java index 149af4eed2..0b2bd576d7 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/BiomeCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/BiomeCommands.java @@ -82,7 +82,10 @@ public BiomeCommands() { aliases = {"biomels", "/biomelist", "/listbiomes"}, desc = "Gets all biomes available." ) - @CommandPermissions("worldedit.biome.list") + @CommandPermissions( + value = "worldedit.biome.list", + queued = false + ) public void biomeList( Actor actor, @ArgFlag(name = 'p', desc = "Page number.", def = "1") diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/BrushCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/BrushCommands.java index 5c87d91368..358f754139 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/BrushCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/BrushCommands.java @@ -134,6 +134,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.nio.file.FileSystems; import java.util.List; @@ -310,11 +311,11 @@ public void lineBrush( }, desc = "Join multiple objects together in a curve", descFooter = - "Click to select some objects,click the same block twice to connect the objects.\n" - + "Insufficient brush radius, or clicking the the wrong spot will result in undesired shapes. The shapes must be simple lines or loops.\n" - + "Pic1: http://i.imgur.com/CeRYAoV.jpg -> http://i.imgur.com/jtM0jA4.png\n" - + "Pic2: http://i.imgur.com/bUeyc72.png -> http://i.imgur.com/tg6MkcF.png" - + "Tutorial: https://www.planetminecraft.com/blog/fawe-tutorial/" + """ + Click to select some objects,click the same block twice to connect the objects. + Insufficient brush radius, or clicking the the wrong spot will result in undesired shapes. The shapes must be simple lines or loops. + Pic1: http://i.imgur.com/CeRYAoV.jpg -> http://i.imgur.com/jtM0jA4.png + Pic2: http://i.imgur.com/bUeyc72.png -> http://i.imgur.com/tg6MkcF.pngTutorial: https://www.planetminecraft.com/blog/fawe-tutorial/""" ) @CommandPermissions("worldedit.brush.spline") public void splineBrush( @@ -337,9 +338,10 @@ public void splineBrush( "vaesweep" }, desc = "Sweep your clipboard content along a curve", - descFooter = "Sweeps your clipboard content along a curve.\n" - + "Define a curve by selecting the individual points with a brush\n" - + "Set [copies] to a value > 0 if you want to have your selection pasted a limited amount of times equally spaced on the curve" + descFooter = """ + Sweeps your clipboard content along a curve. + Define a curve by selecting the individual points with a brush + Set [copies] to a value > 0 if you want to have your selection pasted a limited amount of times equally spaced on the curve""" ) @CommandPermissions("worldedit.brush.sweep") public void sweepBrush( @@ -520,11 +522,9 @@ public void imageBrush( @Switch(name = 'a', desc = "Use image Alpha") boolean alpha, @Switch(name = 'f', desc = "Blend the image with existing terrain") boolean fadeOut ) - throws WorldEditException, IOException { + throws WorldEditException, IOException, URISyntaxException { URL url = new URL(imageURL); - if (!url.getHost().equalsIgnoreCase("i.imgur.com")) { - throw new IOException("Only i.imgur.com links are allowed!"); - } + MainUtil.checkImageHost(url.toURI()); BufferedImage image = MainUtil.readImage(url); worldEdit.checkMaxBrushRadius(radius); if (yscale != 1) { @@ -636,10 +636,10 @@ public void scatterSchemBrush( @Command( name = "layer", desc = "Replaces terrain with a layer.", - descFooter = "Replaces terrain with a layer.\n" - + "Example: /br layer 5 oak_planks,orange_stained_glass,magenta_stained_glass,black_wool - Places several " + - "layers on a surface\n" - + "Pic: https://i.imgur.com/XV0vYoX.png" + descFooter = """ + Replaces terrain with a layer. + Example: /br layer 5 oak_planks,orange_stained_glass,magenta_stained_glass,black_wool - Places several layers on a surface + Pic: https://i.imgur.com/XV0vYoX.png""" ) @CommandPermissions("worldedit.brush.layer") public void surfaceLayer( @@ -658,11 +658,11 @@ public void surfaceLayer( @Command( name = "splatter", desc = "Splatter a pattern on a surface", - descFooter = "Sets a bunch of blocks randomly on a surface.\n" - + "Pic: https://i.imgur.com/hMD29oO.png\n" - + "Example: /br splatter stone,dirt 30 15\n" - + "Note: The seeds define how many splotches there are, recursion defines how large, " - + "solid defines whether the pattern is applied per seed, else per block." + descFooter = """ + Sets a bunch of blocks randomly on a surface. + Pic: https://i.imgur.com/hMD29oO.png + Example: /br splatter stone,dirt 30 15 + Note: The seeds define how many splotches there are, recursion defines how large, solid defines whether the pattern is applied per seed, else per block.""" ) @CommandPermissions("worldedit.brush.splatter") public void splatterBrush( @@ -691,9 +691,10 @@ public void splatterBrush( "scommand" }, desc = "Run commands at random points on a surface", - descFooter = "Run commands at random points on a surface\n" - + " - Your selection will be expanded to the specified size around each point\n" - + " - Placeholders: {x}, {y}, {z}, {world}, {size}" + descFooter = """ + Run commands at random points on a surface + - Your selection will be expanded to the specified size around each point + - Placeholders: {x}, {y}, {z}, {world}, {size}""" ) @CommandPermissions("worldedit.brush.scattercommand") public void scatterCommandBrush( @@ -723,9 +724,10 @@ public void scatterCommandBrush( name = "height", aliases = {"heightmap"}, desc = "Raise or lower terrain using a heightmap", - descFooter = "This brush raises and lowers land.\n" - + "Note: Use a negative yscale to reduce height\n" - + "Snow Pic: https://i.imgur.com/Hrzn0I4.png" + descFooter = """ + This brush raises and lowers land. + Note: Use a negative yscale to reduce height + Snow Pic: https://i.imgur.com/Hrzn0I4.png""" ) @CommandPermissions("worldedit.brush.height") public void heightBrush( @@ -890,9 +892,11 @@ private InputStream getHeightmapStream(String filename) throws FileNotFoundExcep "copypasta" }, desc = "Copy Paste brush", - descFooter = "Left click the base of an object to copy.\n" + "Right click to paste\n" - + "Note: Works well with the clipboard scroll action\n" - + "Video: https://www.youtube.com/watch?v=RPZIaTbqoZw" + descFooter = """ + Left click the base of an object to copy. + Right click to paste + Note: Works well with the clipboard scroll action + Video: https://www.youtube.com/watch?v=RPZIaTbqoZw""" ) @CommandPermissions("worldedit.brush.copy") public void copy( @@ -914,9 +918,10 @@ public void copy( name = "command", aliases = {"cmd"}, desc = "Command brush", - descFooter = "Run the commands at the clicked position.\n" - + " - Your selection will be expanded to the specified size around each point\n" - + " - Placeholders: {x}, {y}, {z}, {world}, {size}" + descFooter = """ + Run the commands at the clicked position. + - Your selection will be expanded to the specified size around each point + - Placeholders: {x}, {y}, {z}, {world}, {size}""" ) @CommandPermissions("worldedit.brush.command") public void command( @@ -1036,7 +1041,7 @@ static void setOperationBasedBrush( ) throws WorldEditException { WorldEdit.getInstance().checkMaxBrushRadius(radius); - BrushTool tool = session.getBrushTool(player.getItemInHand(HandSide.MAIN_HAND).getType()); + BrushTool tool = session.getBrushTool(player); tool.setSize(radius); tool.setFill(null); tool.setBrush(new OperationFactoryBrush(factory, shape, session), permission); @@ -1196,17 +1201,12 @@ public void sphereBrush( brush = new HollowSphereBrush(); } else { //FAWE start - Suggest different brush material if sand or gravel is used - if (pattern instanceof BlockStateHolder) { - BlockType type = ((BlockStateHolder) pattern).getBlockType(); - switch (type.getId()) { - case "minecraft:sand": - case "minecraft:gravel": - player.print( - Caption.of("fawe.worldedit.brush.brush.try.other")); - falling = true; - break; - default: - break; + if (pattern instanceof BlockStateHolder holder) { + BlockType type = holder.getBlockType(); + if (type == BlockTypes.SAND || type == BlockTypes.GRAVEL) { + player.print( + Caption.of("fawe.worldedit.brush.brush.try.other")); + falling = true; } } if (falling) { @@ -1258,13 +1258,12 @@ public void cylinderBrush( @Command( name = "clipboard", - desc = "@Deprecated use instead: `/br copypaste`)", + desc = "Paste your clipboard at the brush location. Includes any transforms.", descFooter = "Choose the clipboard brush.\n" + "Without the -o flag, the paste will appear centered at the target location. " + "With the flag, then the paste will appear relative to where you had " + "stood relative to the copied area when you copied it." ) - @Deprecated @CommandPermissions("worldedit.brush.clipboard") public void clipboardBrush( Player player, LocalSession session, @@ -1278,7 +1277,11 @@ public void clipboardBrush( boolean pasteBiomes, @ArgFlag(name = 'm', desc = "Skip blocks matching this mask in the clipboard") @ClipboardMask - Mask sourceMask, InjectedValueAccess context + Mask sourceMask, InjectedValueAccess context, + //FAWE start - random rotation + @Switch(name = 'r', desc = "Apply random rotation on paste, combines with existing clipboard transforms") + boolean randomRotate + //FAWE end ) throws WorldEditException { ClipboardHolder holder = session.getClipboard(); @@ -1294,9 +1297,9 @@ public void clipboardBrush( set( context, - new ClipboardBrush(newHolder, ignoreAir, usingOrigin, pasteEntities, pasteBiomes, - sourceMask - ), + //FAWE start - random rotation + new ClipboardBrush(newHolder, ignoreAir, usingOrigin, pasteEntities, pasteBiomes, sourceMask, randomRotate), + //FAWE end "worldedit.brush.clipboard" ); } @@ -1361,7 +1364,7 @@ public void snowSmoothBrush( iterations = Math.min(limit.MAX_ITERATIONS, iterations); //FAWE end - set(context, new SnowSmoothBrush(iterations, mask), "worldedit.brush.snowsmooth").setSize(radius); + set(context, new SnowSmoothBrush(iterations, snowBlockCount, mask), "worldedit.brush.snowsmooth").setSize(radius); player.print(Caption.of( "worldedit.brush.smooth.equip", radius, diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/ChunkCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/ChunkCommands.java index d1b9084fb0..c36252f859 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/ChunkCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/ChunkCommands.java @@ -79,7 +79,10 @@ public ChunkCommands(WorldEdit worldEdit) { aliases = {"/chunkinfo"}, desc = "Get information about the chunk you're inside" ) - @CommandPermissions("worldedit.chunkinfo") + @CommandPermissions( + value = "worldedit.chunkinfo", + queued = false + ) public void chunkInfo(Player player) { Location pos = player.getBlockLocation(); int chunkX = (int) Math.floor(pos.getBlockX() / 16.0); @@ -99,7 +102,10 @@ public void chunkInfo(Player player) { aliases = {"/listchunks"}, desc = "List chunks that your selection includes" ) - @CommandPermissions("worldedit.listchunks") + @CommandPermissions( + value = "worldedit.listchunks", + queued = false + ) public void listChunks( Actor actor, World world, LocalSession session, @ArgFlag(name = 'p', desc = "Page number.", def = "1") int page diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/ClipboardCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/ClipboardCommands.java index 0cb9f6798f..40ca530c82 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/ClipboardCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/ClipboardCommands.java @@ -74,6 +74,7 @@ import com.sk89q.worldedit.regions.RegionIntersection; import com.sk89q.worldedit.regions.RegionSelector; import com.sk89q.worldedit.regions.selector.CuboidRegionSelector; +import com.sk89q.worldedit.regions.selector.ExtendingCuboidRegionSelector; import com.sk89q.worldedit.session.ClipboardHolder; import com.sk89q.worldedit.util.formatting.text.Component; import com.sk89q.worldedit.util.formatting.text.TextComponent; @@ -159,35 +160,7 @@ public void copy( session.getPlacementPosition(actor)); ForwardExtentCopy copy = new ForwardExtentCopy(editSession, region, clipboard, region.getMinimumPoint()); copy.setCopyingEntities(copyEntities); - copy.setCopyingBiomes(copyBiomes); - - Mask sourceMask = editSession.getSourceMask(); - Region[] regions = editSession.getAllowedRegions(); - Region allowedRegion; - if (regions == null || regions.length == 0) { - allowedRegion = new NullRegion(); - } else { - allowedRegion = new RegionIntersection(regions); - } - final Mask firstSourceMask = mask != null ? mask : sourceMask; - final Mask finalMask = MaskIntersection.of(firstSourceMask, new RegionMask(allowedRegion)).optimize(); - if (finalMask != Masks.alwaysTrue()) { - copy.setSourceMask(finalMask); - } - if (sourceMask != null) { - editSession.setSourceMask(null); - new MaskTraverser(sourceMask).reset(editSession); - editSession.setSourceMask(null); - } - - try { - Operations.completeLegacy(copy); - } catch (Throwable e) { - throw e; - } finally { - clipboard.flush(); - } - session.setClipboard(new ClipboardHolder(clipboard)); + createCopy(session, editSession, copyBiomes, mask, clipboard, copy); copy.getStatusMessages().forEach(actor::print); //FAWE end @@ -298,7 +271,25 @@ public void cut( copy.setSourceFunction(new BlockReplace(editSession, leavePattern)); copy.setCopyingEntities(copyEntities); copy.setRemovingEntities(true); + createCopy(session, editSession, copyBiomes, mask, clipboard, copy); + + if (!actor.hasPermission("fawe.tips")) { + actor.print(Caption.of("fawe.tips.tip.lazycut")); + } + copy.getStatusMessages().forEach(actor::print); + //FAWE end + } + + private void createCopy( + final LocalSession session, + final EditSession editSession, + final boolean copyBiomes, + final Mask mask, + final Clipboard clipboard, + final ForwardExtentCopy copy + ) { copy.setCopyingBiomes(copyBiomes); + Mask sourceMask = editSession.getSourceMask(); Region[] regions = editSession.getAllowedRegions(); Region allowedRegion; @@ -317,20 +308,13 @@ public void cut( new MaskTraverser(sourceMask).reset(editSession); editSession.setSourceMask(null); } + try { Operations.completeLegacy(copy); - } catch (Throwable e) { - throw e; } finally { clipboard.flush(); } session.setClipboard(new ClipboardHolder(clipboard)); - - if (!actor.hasPermission("fawe.tips")) { - actor.print(Caption.of("fawe.tips.tip.lazycut")); - } - copy.getStatusMessages().forEach(actor::print); - //FAWE end } //FAWE start @@ -546,7 +530,12 @@ public void paste( Vector3 max = realTo.add(holder .getTransform() .apply(region.getMaximumPoint().subtract(region.getMinimumPoint()).toVector3())); - RegionSelector selector = new CuboidRegionSelector(world, realTo.toBlockPoint(), max.toBlockPoint()); + final CuboidRegionSelector selector; + if (session.getRegionSelector(world) instanceof ExtendingCuboidRegionSelector) { + selector = new ExtendingCuboidRegionSelector(world, realTo.toBlockPoint(), max.toBlockPoint()); + } else { + selector = new CuboidRegionSelector(world, realTo.toBlockPoint(), max.toBlockPoint()); + } session.setRegionSelector(world, selector); selector.learnChanges(); selector.explainRegionAdjust(actor, session); @@ -577,9 +566,10 @@ private void checkPaste(Actor player, EditSession editSession, BlockVector3 to, @Command( name = "/rotate", desc = "Rotate the contents of the clipboard", - descFooter = "Non-destructively rotate the contents of the clipboard.\n" - + "Angles are provided in degrees and a positive angle will result in a clockwise rotation. " - + "Multiple rotations can be stacked. Interpolation is not performed so angles should be a multiple of 90 degrees.\n" + descFooter = """ + Non-destructively rotate the contents of the clipboard. + Angles are provided in degrees and a positive angle will result in a clockwise rotation. Multiple rotations can be stacked. Interpolation is not performed so angles should be a multiple of 90 degrees. + """ ) @CommandPermissions("worldedit.clipboard.rotate") public void rotate( diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/GeneralCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/GeneralCommands.java index 3d54e40a73..0cf5b1a784 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/GeneralCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/GeneralCommands.java @@ -149,14 +149,11 @@ private static Component replaceFastForPerf( String arg0 = args.get(0).toLowerCase(Locale.ENGLISH); String flipped; switch (arg0) { - case "on": - flipped = "off"; - break; - case "off": - flipped = "on"; - break; - default: + case "on" -> flipped = "off"; + case "off" -> flipped = "on"; + default -> { return TextComponent.of("There is no replacement for //fast " + arg0); + } } return CommandUtil.createNewCommandReplacementText("//perf " + flipped); } @@ -362,7 +359,10 @@ public void world( descFooter = "This is dependent on platform implementation. " + "Not all platforms support watchdog hooks, or contain a watchdog." ) - @CommandPermissions("worldedit.watchdog") + @CommandPermissions( + value = "worldedit.watchdog", + queued = false + ) public void watchdog( Actor actor, LocalSession session, @Arg(desc = "The mode to set the watchdog hook to", def = "") @@ -424,7 +424,10 @@ public void togglePlace(Actor actor, LocalSession session) { aliases = {"/searchitem", "/l", "/search"}, desc = "Search for an item" ) - @CommandPermissions("worldedit.searchitem") + @CommandPermissions( + value = "worldedit.searchitem", + queued = false + ) public void searchItem( Actor actor, @Switch(name = 'b', desc = "Only search for blocks") @@ -573,7 +576,10 @@ public void gtransform( aliases = {"tips"}, desc = "Toggle FAWE tips" ) - @CommandPermissions("fawe.tips") + @CommandPermissions( + value = "fawe.tips", + queued = false + ) public void tips(Actor actor, LocalSession session) throws WorldEditException { if (actor.togglePermission("fawe.tips")) { actor.print(Caption.of("fawe.info.worldedit.toggle.tips.on")); @@ -627,7 +633,6 @@ public Component call() throws Exception { String command = "/searchitem " + (blocksOnly ? "-b " : "") + (itemsOnly ? "-i " : "") + "-p %page% " + search; Map results = new TreeMap<>(); String idMatch = search.replace(' ', '_'); - String nameMatch = search.toLowerCase(Locale.ROOT); for (ItemType searchType : ItemType.REGISTRY) { if (blocksOnly && !searchType.hasBlockType()) { continue; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/GenerationCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/GenerationCommands.java index dabc1dc920..ca67ad132e 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/GenerationCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/GenerationCommands.java @@ -65,6 +65,7 @@ import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.io.IOException; +import java.net.URISyntaxException; import java.net.URL; import java.util.List; import java.util.concurrent.ExecutorService; @@ -119,18 +120,15 @@ public int hcyl( final double radiusX; final double radiusZ; switch (radii.size()) { - case 1: - radiusX = radiusZ = Math.max(1, radii.get(0)); - break; - - case 2: + case 1 -> radiusX = radiusZ = Math.max(1, radii.get(0)); + case 2 -> { radiusX = Math.max(1, radii.get(0)); radiusZ = Math.max(1, radii.get(1)); - break; - - default: + } + default -> { actor.print(Caption.of("worldedit.cyl.invalid-radius")); return 0; + } } worldEdit.checkMaxRadius(radiusX); worldEdit.checkMaxRadius(radiusZ); @@ -169,18 +167,15 @@ public int cyl( final double radiusX; final double radiusZ; switch (radii.size()) { - case 1: - radiusX = radiusZ = Math.max(1, radii.get(0)); - break; - - case 2: + case 1 -> radiusX = radiusZ = Math.max(1, radii.get(0)); + case 2 -> { radiusX = Math.max(1, radii.get(0)); radiusZ = Math.max(1, radii.get(1)); - break; - - default: + } + default -> { actor.print(Caption.of("worldedit.cyl.invalid-radius")); return 0; + } } worldEdit.checkMaxRadius(radiusX); @@ -234,19 +229,16 @@ public int sphere( final double radiusY; final double radiusZ; switch (radii.size()) { - case 1: - radiusX = radiusY = radiusZ = Math.max(0, radii.get(0)); - break; - - case 3: + case 1 -> radiusX = radiusY = radiusZ = Math.max(0, radii.get(0)); + case 3 -> { radiusX = Math.max(0, radii.get(0)); radiusY = Math.max(0, radii.get(1)); radiusZ = Math.max(0, radii.get(2)); - break; - - default: + } + default -> { actor.print(Caption.of("worldedit.sphere.invalid-radius")); return 0; + } } worldEdit.checkMaxRadius(radiusX); @@ -437,9 +429,10 @@ public int generate( name = "/generatebiome", aliases = {"/genbiome", "/gb"}, desc = "Sets biome according to a formula.", - descFooter = "Formula must return positive numbers (true) if the point is inside the shape\n" - + "Sets the biome of blocks in that shape.\n" - + "For details, see https://ehub.to/we/expr" + descFooter = """ + Formula must return positive numbers (true) if the point is inside the shape + Sets the biome of blocks in that shape. + For details, see https://ehub.to/we/expr""" ) @CommandPermissions("worldedit.generation.shape.biome") @Logging(ALL) @@ -588,12 +581,10 @@ public void image( @Arg(desc = "boolean", def = "true") boolean randomize, @Arg(desc = "TODO", def = "100") int threshold, @Arg(desc = "BlockVector2", def = "") BlockVector2 dimensions - ) throws WorldEditException, IOException { + ) throws WorldEditException, IOException, URISyntaxException { TextureUtil tu = Fawe.instance().getCachedTextureUtil(randomize, 0, threshold); URL url = new URL(imageURL); - if (!url.getHost().equalsIgnoreCase("i.imgur.com")) { - throw new IOException("Only i.imgur.com links are allowed!"); - } + MainUtil.checkImageHost(url.toURI()); if (dimensions != null) { checkCommandArgument( (long) dimensions.getX() * dimensions.getZ() <= Settings.settings().WEB.MAX_IMAGE_SIZE, @@ -626,14 +617,12 @@ public void image( BlockVector3 pos1 = session.getPlacementPosition(actor); BlockVector3 pos2 = pos1.add(image.getWidth() - 1, 0, image.getHeight() - 1); CuboidRegion region = new CuboidRegion(pos1, pos2); - int[] count = new int[1]; final BufferedImage finalImage = image; RegionVisitor visitor = new RegionVisitor(region, pos -> { int x = pos.getBlockX() - pos1.getBlockX(); int z = pos.getBlockZ() - pos1.getBlockZ(); int color = finalImage.getRGB(x, z); BlockType block = tu.getNearestBlock(color); - count[0]++; if (block != null) { return editSession.setBlock(pos, block.getDefaultState()); } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/HistorySubCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/HistorySubCommands.java index 3688041d49..452d5084ca 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/HistorySubCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/HistorySubCommands.java @@ -223,7 +223,10 @@ public synchronized void importdb(Actor actor) throws WorldEditException { aliases = {"summary", "summarize"}, desc = "Summarize an edit" ) - @CommandPermissions("worldedit.history.info") + @CommandPermissions( + value = "worldedit.history.info", + queued = false + ) public synchronized void summary( Player player, RollbackDatabase database, Arguments arguments, @Arg(desc = "Player uuid/name") @@ -314,8 +317,7 @@ private PaginationBox list( public Component apply(@Nullable Supplier input) { ChangeSet edit = input.get(); - if (edit instanceof RollbackOptimizedHistory) { - RollbackOptimizedHistory rollback = (RollbackOptimizedHistory) edit; + if (edit instanceof RollbackOptimizedHistory rollback) { UUID uuid = rollback.getUUID(); int index = rollback.getIndex(); @@ -368,7 +370,10 @@ public Component apply(@Nullable Supplier input) { aliases = {"inspect", "search", "near"}, desc = "Find nearby edits" ) - @CommandPermissions("worldedit.history.find") + @CommandPermissions( + value = "worldedit.history.find", + queued = false + ) public synchronized void find( Player player, World world, RollbackDatabase database, Arguments arguments, @ArgFlag(name = 'u', def = "", desc = "String user") @@ -429,7 +434,10 @@ public synchronized void find( aliases = {"distribution"}, desc = "View block distribution for an edit" ) - @CommandPermissions("worldedit.history.distr") + @CommandPermissions( + value = "worldedit.history.distr", + queued = false + ) public void distr( Player player, LocalSession session, RollbackDatabase database, Arguments arguments, @Arg(desc = "Player uuid/name") @@ -468,7 +476,10 @@ public void distr( name = "list", desc = "List your history" ) - @CommandPermissions("worldedit.history.list") + @CommandPermissions( + value = "worldedit.history.list", + queued = false + ) public void list( Player player, LocalSession session, RollbackDatabase database, Arguments arguments, @Arg(desc = "Player uuid/name") @@ -476,7 +487,6 @@ public void list( @ArgFlag(name = 'p', desc = "Page to view.", def = "") Integer page ) { - int index = session.getHistoryIndex(); List> history = Lists.transform( session.getHistory(), (Function>) input -> () -> input diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/NavigationCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/NavigationCommands.java index 22e8a12ecb..239105cd2d 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/NavigationCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/NavigationCommands.java @@ -60,7 +60,10 @@ public NavigationCommands(WorldEdit worldEdit) { aliases = {"!", "/unstuck"}, desc = "Escape from being stuck inside a block" ) - @CommandPermissions("worldedit.navigation.unstuck") + @CommandPermissions( + value = "worldedit.navigation.unstuck", + queued = false + ) public void unstuck(Player player) throws WorldEditException { player.findFreePosition(); player.print(Caption.of("worldedit.unstuck.moved")); @@ -71,7 +74,10 @@ public void unstuck(Player player) throws WorldEditException { aliases = {"asc", "/asc", "/ascend"}, desc = "Go up a floor" ) - @CommandPermissions("worldedit.navigation.ascend") + @CommandPermissions( + value = "worldedit.navigation.ascend", + queued = false + ) public void ascend( Player player, @Arg(desc = "# of levels to ascend", def = "1") @@ -96,7 +102,10 @@ public void ascend( aliases = {"desc", "/desc", "/descend"}, desc = "Go down a floor" ) - @CommandPermissions("worldedit.navigation.descend") + @CommandPermissions( + value = "worldedit.navigation.descend", + queued = false + ) public void descend( Player player, @Arg(desc = "# of levels to descend", def = "1") @@ -147,7 +156,10 @@ public void ceiling( aliases = {"/thru"}, desc = "Pass through walls" ) - @CommandPermissions("worldedit.navigation.thru.command") + @CommandPermissions( + value = "worldedit.navigation.thru.command", + queued = false + ) public void thru(Player player) throws WorldEditException { if (player.passThroughForwardWall(6)) { player.print(Caption.of("worldedit.thru.moved")); @@ -161,7 +173,10 @@ public void thru(Player player) throws WorldEditException { aliases = {"j", "/jumpto", "/j"}, desc = "Teleport to a location" ) - @CommandPermissions("worldedit.navigation.jumpto.command") + @CommandPermissions( + value = "worldedit.navigation.jumpto.command", + queued = false + ) public void jumpTo( Player player, @Arg(desc = "Location to jump to", def = "") diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/RegionCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/RegionCommands.java index 5d3fb30e8a..a72cba4b37 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/RegionCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/RegionCommands.java @@ -140,7 +140,10 @@ public void air(Actor actor, EditSession editSession, @Selection Region region) name = "/test", desc = "test region" ) - @CommandPermissions("worldedit.region.test") + @CommandPermissions( + value = "worldedit.region.test", + queued = false + ) @Logging(REGION) public void test( Actor actor, EditSession editSession, @@ -175,7 +178,10 @@ public void removeLighting(Actor actor, LocalSession session, @Selection Region aliases = "/nbt", desc = "View nbt info for a block" ) - @CommandPermissions("worldedit.nbtinfo") + @CommandPermissions( + value = "worldedit.nbtinfo", + queued = false + ) public void nbtinfo(Player player, EditSession editSession) { Location pos = player.getBlockTrace(128); if (pos == null) { @@ -228,13 +234,12 @@ public int line( @Switch(name = 'h', desc = "Generate only a shell") boolean shell ) throws WorldEditException { - if (!(region instanceof CuboidRegion)) { + if (!(region instanceof CuboidRegion cuboidregion)) { actor.print(Caption.of("worldedit.line.cuboid-only")); return 0; } checkCommandArgument(thickness >= 0, "Thickness must be >= 0"); - CuboidRegion cuboidregion = (CuboidRegion) region; BlockVector3 pos1 = cuboidregion.getPos1(); BlockVector3 pos2 = cuboidregion.getPos2(); int blocksChanged = editSession.drawLine(pattern, pos1, pos2, thickness, !shell); @@ -261,13 +266,12 @@ public int curve( @Switch(name = 'h', desc = "Generate only a shell") boolean shell ) throws WorldEditException { - if (!(region instanceof ConvexPolyhedralRegion)) { + if (!(region instanceof ConvexPolyhedralRegion cpregion)) { actor.print(Caption.of("worldedit.curve.invalid-type")); return 0; } checkCommandArgument(thickness >= 0, "Thickness must be >= 0"); - ConvexPolyhedralRegion cpregion = (ConvexPolyhedralRegion) region; List vectors = new ArrayList<>(cpregion.getVertices()); int blocksChanged = editSession.drawSpline(pattern, vectors, 0, 0, 0, 10, thickness, !shell); @@ -468,7 +472,10 @@ public int smooth( desc = "Bypass region restrictions", descFooter = "Bypass region restrictions" ) - @CommandPermissions("fawe.admin") + @CommandPermissions( + value = "fawe.admin", + queued = false + ) public void wea(Actor actor) throws WorldEditException { if (actor.togglePermission("fawe.bypass")) { actor.print(Caption.of("fawe.info.worldedit.bypassed")); @@ -697,7 +704,7 @@ void regenerate( actor.print(Caption.of("fawe.regen.time")); //FAWE end RegenOptions options = RegenOptions.builder() - .seed(!randomSeed ? seed : new Long(ThreadLocalRandom.current().nextLong())) + .seed(!randomSeed ? seed : Long.valueOf(ThreadLocalRandom.current().nextLong())) .regenBiomes(regenBiomes) .biomeType(biomeType) .build(); @@ -718,9 +725,10 @@ void regenerate( @Command( name = "/deform", desc = "Deforms a selected region with an expression", - descFooter = "The expression is executed for each block and is expected\n" - + "to modify the variables x, y and z to point to a new block\n" - + "to fetch. For details, see https://ehub.to/we/expr" + descFooter = """ + The expression is executed for each block and is expected + to modify the variables x, y and z to point to a new block + to fetch. For details, see https://ehub.to/we/expr""" ) @CommandPermissions("worldedit.region.deform") @Logging(ALL) @@ -794,9 +802,10 @@ public int deform( @Command( name = "/hollow", desc = "Hollows out the object contained in this selection", - descFooter = "Hollows out the object contained in this selection.\n" - + "Optionally fills the hollowed out part with the given block.\n" - + "Thickness is measured in manhattan distance." + descFooter = """ + Hollows out the object contained in this selection. + Optionally fills the hollowed out part with the given block. + Thickness is measured in manhattan distance.""" ) @CommandPermissions("worldedit.region.hollow") @Logging(REGION) diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java index 03d19e72c5..c677734ef8 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java @@ -26,7 +26,6 @@ import com.fastasyncworldedit.core.extent.clipboard.URIClipboardHolder; import com.fastasyncworldedit.core.extent.clipboard.io.schematic.MinecraftStructure; import com.fastasyncworldedit.core.util.MainUtil; -import com.google.common.base.Function; import com.google.common.collect.Multimap; import com.sk89q.worldedit.LocalConfiguration; import com.sk89q.worldedit.LocalSession; @@ -46,6 +45,7 @@ import com.sk89q.worldedit.extent.clipboard.io.ClipboardWriter; import com.sk89q.worldedit.function.operation.Operations; import com.sk89q.worldedit.internal.util.LogManagerCompat; +import com.sk89q.worldedit.math.transform.AffineTransform; import com.sk89q.worldedit.math.transform.Transform; import com.sk89q.worldedit.session.ClipboardHolder; import com.sk89q.worldedit.util.formatting.component.ErrorFormat; @@ -90,6 +90,8 @@ import java.util.Objects; import java.util.UUID; import java.util.concurrent.Callable; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Function; import java.util.regex.Pattern; import static com.fastasyncworldedit.core.util.ReflectionUtils.as; @@ -209,11 +211,9 @@ public void unload( } ClipboardHolder clipboard = session.getClipboard(); - if (clipboard instanceof URIClipboardHolder) { - URIClipboardHolder identifiable = (URIClipboardHolder) clipboard; + if (clipboard instanceof URIClipboardHolder identifiable) { if (identifiable.contains(uri)) { - if (identifiable instanceof MultiClipboardHolder) { - MultiClipboardHolder multi = (MultiClipboardHolder) identifiable; + if (identifiable instanceof MultiClipboardHolder multi) { multi.remove(uri); if (multi.getHolders().isEmpty()) { session.setClipboard(null); @@ -314,12 +314,16 @@ public void load( @Arg(desc = "File name.") String filename, @Arg(desc = "Format name.", def = "fast") - String formatName + String formatName, + //FAWE start - random rotation + @Switch(name = 'r', desc = "Apply random rotation to the clipboard") + boolean randomRotate + //FAWE end ) throws FilenameException { LocalConfiguration config = worldEdit.getConfiguration(); //FAWE start - ClipboardFormat format = null; + ClipboardFormat format; InputStream in = null; try { URI uri; @@ -396,6 +400,12 @@ public void load( uri = file.toURI(); } format.hold(actor, uri, in); + if (randomRotate) { + AffineTransform transform = new AffineTransform(); + int rotate = 90 * ThreadLocalRandom.current().nextInt(4); + transform = transform.rotateY(rotate); + session.getClipboard().setTransform(transform); + } actor.print(Caption.of("fawe.worldedit.schematic.schematic.loaded", filename)); } catch (IllegalArgumentException e) { actor.print(Caption.of("worldedit.schematic.unknown-filename", TextComponent.of(filename))); @@ -526,7 +536,10 @@ public void save( aliases = {"listformats", "f"}, desc = "List available formats" ) - @CommandPermissions("worldedit.schematic.formats") + @CommandPermissions( + value = "worldedit.schematic.formats", + queued = false + ) public void formats(Actor actor) { actor.print(Caption.of("worldedit.schematic.formats.title")); StringBuilder builder; @@ -552,7 +565,10 @@ public void formats(Actor actor) { desc = "List saved schematics", descFooter = "Note: Format is not fully verified until loading." ) - @CommandPermissions("worldedit.schematic.list") + @CommandPermissions( + value = "worldedit.schematic.list", + queued = false + ) public void list( Actor actor, LocalSession session, @ArgFlag(name = 'p', desc = "Page to view.", def = "1") @@ -823,7 +839,6 @@ public Void call() throws Exception { final String SCHEMATIC_NAME = file.getName(); double oldKbOverwritten = 0; - String overwrittenPath = curFilepath; int numFiles = -1; if (checkFilesize) { @@ -839,10 +854,10 @@ public Void call() throws Exception { if (overwrite) { oldKbOverwritten = Files.size(Paths.get(file.getAbsolutePath())) / 1000.0; int iter = 1; - while (new File(overwrittenPath + "." + iter + "." + format.getPrimaryFileExtension()).exists()) { + while (new File(curFilepath + "." + iter + "." + format.getPrimaryFileExtension()).exists()) { iter++; } - file = new File(overwrittenPath + "." + iter + "." + format.getPrimaryFileExtension()); + file = new File(curFilepath + "." + iter + "." + format.getPrimaryFileExtension()); } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SelectionCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SelectionCommands.java index e6e06e9a23..64e3c8a81a 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SelectionCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SelectionCommands.java @@ -314,7 +314,10 @@ public void chunk( name = "/wand", desc = "Get the wand object" ) - @CommandPermissions("worldedit.wand") + @CommandPermissions( + value = "worldedit.wand", + queued = false + ) public void wand( Player player, LocalSession session, @Switch(name = 'n', desc = "Get a navigation wand") boolean navWand @@ -348,7 +351,10 @@ public void wand( aliases = {"/toggleeditwand"}, desc = "Remind the user that the wand is now a tool and can be unbound with /tool none." ) - @CommandPermissions("worldedit.wand.toggle") + @CommandPermissions( + value = "worldedit.wand.toggle", + queued = false + ) public void toggleWand(Player player) { player.print( Caption.of( @@ -499,7 +505,10 @@ private BlockVector3[] getChangesForEachDir(int amount, boolean onlyHorizontal, name = "/size", desc = "Get information about the selection" ) - @CommandPermissions("worldedit.selection.size") + @CommandPermissions( + value = "worldedit.selection.size", + queued = false + ) public void size( Actor actor, World world, LocalSession session, @Switch(name = 'c', desc = "Get clipboard info instead") @@ -734,6 +743,7 @@ public void select( box.appendCommand("sphere", Caption.of("worldedit.select.sphere.description"), "//sel sphere"); box.appendCommand("cyl", Caption.of("worldedit.select.cyl.description"), "//sel cyl"); box.appendCommand("convex", Caption.of("worldedit.select.convex.description"), "//sel convex"); + //FAWE start box.appendCommand("polyhedral", Caption.of("fawe.selection.sel.polyhedral"), "//sel polyhedral"); box.appendCommand("fuzzy[=]", Caption.of("fawe.selection.sel.fuzzy-instruction"), "//sel fuzzy[=]"); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SnapshotCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SnapshotCommands.java index 66cfdd853d..6d9bda0849 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SnapshotCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SnapshotCommands.java @@ -98,7 +98,10 @@ static URI resolveSnapshotName(LocalConfiguration config, String name) { name = "list", desc = "List snapshots" ) - @CommandPermissions("worldedit.snapshots.list") + @CommandPermissions( + value = "worldedit.snapshots.list", + queued = false + ) void list( Actor actor, World world, @ArgFlag(name = 'p', desc = "Page of results to return", def = "1") @@ -127,8 +130,7 @@ void list( TextComponent.of(world.getName()) )); - if (config.snapshotDatabase instanceof FileSystemSnapshotDatabase) { - FileSystemSnapshotDatabase db = (FileSystemSnapshotDatabase) config.snapshotDatabase; + if (config.snapshotDatabase instanceof FileSystemSnapshotDatabase db) { Path root = db.getRoot(); if (Files.isDirectory(root)) { WorldEdit.logger.info("No snapshots were found for world '" diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SnapshotUtilCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SnapshotUtilCommands.java index 8de52f6aa5..b987856314 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SnapshotUtilCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SnapshotUtilCommands.java @@ -95,7 +95,7 @@ public void restore( if (snapshotName != null) { URI uri = resolveSnapshotName(config, snapshotName); Optional snapOpt = config.snapshotDatabase.getSnapshot(uri); - if (!snapOpt.isPresent()) { + if (snapOpt.isEmpty()) { actor.print(Caption.of("worldedit.restore.not-available")); return; } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/ToolUtilCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/ToolUtilCommands.java index 6f47132f7a..0694b2af31 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/ToolUtilCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/ToolUtilCommands.java @@ -140,7 +140,7 @@ public void range( @Arg(desc = "The range of the brush") int range ) throws WorldEditException { - session.getBrushTool(player.getItemInHand(HandSide.MAIN_HAND).getType()).setRange(range); + session.getBrushTool(player).setRange(range); player.print(Caption.of("worldedit.tool.range.set")); } @@ -156,7 +156,7 @@ public void size( ) throws WorldEditException { we.checkMaxBrushRadius(size); - session.getBrushTool(player.getItemInHand(HandSide.MAIN_HAND).getType()).setSize(size); + session.getBrushTool(player).setSize(size); player.print(Caption.of("worldedit.tool.size.set")); } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/UtilityCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/UtilityCommands.java index b3403bba47..f064868b4c 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/UtilityCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/UtilityCommands.java @@ -29,7 +29,6 @@ import com.fastasyncworldedit.core.util.TaskManager; import com.fastasyncworldedit.core.util.image.ImageUtil; import com.fastasyncworldedit.core.util.task.DelegateConsumer; -import com.google.common.base.Function; import com.sk89q.worldedit.EditSession; import com.sk89q.worldedit.IncompleteRegionException; import com.sk89q.worldedit.LocalConfiguration; @@ -98,6 +97,7 @@ import java.util.Map; import java.util.UUID; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -130,7 +130,10 @@ public void macro(Actor actor) { aliases = {"/hmi", "hmi"}, desc = "Generate the heightmap interface: https://github.com/IntellectualSites/HeightMap" ) - @CommandPermissions("fawe.admin") + @CommandPermissions( + value = "fawe.admin", + queued = false + ) public void heightmapInterface( Actor actor, @Arg(name = "min", desc = "int", def = "100") int min, @@ -145,12 +148,9 @@ public void heightmapInterface( final int sub = srcFolder.getAbsolutePath().length(); List images = new ArrayList<>(); MainUtil.iterateFiles(srcFolder, file -> { - switch (file.getName().substring(file.getName().lastIndexOf('.')).toLowerCase(Locale.ROOT)) { - case ".png": - case ".jpeg": - break; - default: - return; + String s = file.getName().substring(file.getName().lastIndexOf('.')).toLowerCase(Locale.ROOT); + if (!s.equals(".png") && !s.equals(".jpeg")) { + return; } try { String name = file.getAbsolutePath().substring(sub); @@ -187,7 +187,7 @@ public void heightmapInterface( StringBuilder config = new StringBuilder(); config.append("var images = [\n"); for (String image : images) { - config.append('"' + image.replace(File.separator, "/") + "\",\n"); + config.append('"').append(image.replace(File.separator, "/")).append("\",\n"); } config.append("];\n"); config.append("// The low res images (they should all be the same size)\n"); @@ -805,7 +805,10 @@ public void calc( name = "/help", desc = "Displays help for WorldEdit commands" ) - @CommandPermissions("worldedit.help") + @CommandPermissions( + value = "worldedit.help", + queued = false + ) public void help( Actor actor, @Switch(name = 's', desc = "List sub-commands of the given command, if applicable") @@ -859,7 +862,6 @@ public static List entryToComponent( URI uri = input.getKey(); String path = input.getValue(); - boolean url = false; boolean loaded = isLoaded.apply(uri); URIType type = URIType.FILE; @@ -959,21 +961,13 @@ public static void getFiles( if (len > 0) { for (String arg : args) { switch (arg.toLowerCase(Locale.ROOT)) { - case "me": - case "mine": - case "local": - case "private": + case "me", "mine", "local", "private" -> listMine = true; + case "public", "global" -> listGlobal = true; + case "all" -> { listMine = true; - break; - case "public": - case "global": listGlobal = true; - break; - case "all": - listMine = true; - listGlobal = true; - break; - default: + } + default -> { if (arg.endsWith("/") || arg.endsWith(File.separator)) { arg = arg.replace("/", File.separator); String newDirFilter = dirFilter + arg; @@ -995,7 +989,7 @@ public static void getFiles( } else { filters.add(arg); } - break; + } } } } @@ -1005,7 +999,7 @@ public static void getFiles( List toFilter = new ArrayList<>(); if (!filters.isEmpty()) { - forEachFile = new DelegateConsumer(forEachFile) { + forEachFile = new DelegateConsumer<>(forEachFile) { @Override public void accept(File file) { toFilter.add(file); @@ -1015,7 +1009,7 @@ public void accept(File file) { if (formatName != null) { final ClipboardFormat cf = ClipboardFormats.findByAlias(formatName); - forEachFile = new DelegateConsumer(forEachFile) { + forEachFile = new DelegateConsumer<>(forEachFile) { @Override public void accept(File file) { if (cf.isFormat(file)) { @@ -1024,7 +1018,7 @@ public void accept(File file) { } }; } else { - forEachFile = new DelegateConsumer(forEachFile) { + forEachFile = new DelegateConsumer<>(forEachFile) { @Override public void accept(File file) { if (!file.toString().endsWith(".cached")) { @@ -1062,7 +1056,7 @@ public void accept(File f) { } if (listGlobal) { File rel = MainUtil.resolveRelative(new File(dir, dirFilter)); - forEachFile = new DelegateConsumer(forEachFile) { + forEachFile = new DelegateConsumer<>(forEachFile) { @Override public void accept(File f) { try { @@ -1172,7 +1166,7 @@ public static String getPath(File root, File file, UUID uuid) { StringBuilder name = new StringBuilder(); if (relative.isAbsolute()) { relative = root.toURI().relativize(file.toURI()); - name.append(".." + File.separator); + name.append("..").append(File.separator); } name.append(relative.getPath()); return name.toString(); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/tool/brush/ClipboardBrush.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/tool/brush/ClipboardBrush.java index aac36df2f0..b9c7dc45cf 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/tool/brush/ClipboardBrush.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/tool/brush/ClipboardBrush.java @@ -27,9 +27,13 @@ import com.sk89q.worldedit.function.operation.Operations; import com.sk89q.worldedit.function.pattern.Pattern; import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.math.transform.AffineTransform; +import com.sk89q.worldedit.math.transform.Transform; import com.sk89q.worldedit.regions.Region; import com.sk89q.worldedit.session.ClipboardHolder; +import java.util.concurrent.ThreadLocalRandom; + public class ClipboardBrush implements Brush { private final ClipboardHolder holder; @@ -38,6 +42,9 @@ public class ClipboardBrush implements Brush { private final boolean pasteEntities; private final boolean pasteBiomes; private final Mask sourceMask; + //FAWE start - random rotation + private final boolean randomRotate; + //FAWE end public ClipboardBrush(ClipboardHolder holder, boolean ignoreAirBlocks, boolean usingOrigin) { this.holder = holder; @@ -46,23 +53,48 @@ public ClipboardBrush(ClipboardHolder holder, boolean ignoreAirBlocks, boolean u this.pasteBiomes = false; this.pasteEntities = false; this.sourceMask = null; + //FAWE start - random rotation + this.randomRotate = false; + //FAWE end } public ClipboardBrush( ClipboardHolder holder, boolean ignoreAirBlocks, boolean usingOrigin, boolean pasteEntities, boolean pasteBiomes, Mask sourceMask ) { + //FAWE start - random rotation + this(holder, ignoreAirBlocks, usingOrigin, pasteEntities, pasteBiomes, sourceMask, false); + } + + public ClipboardBrush( + ClipboardHolder holder, boolean ignoreAirBlocks, boolean usingOrigin, boolean pasteEntities, + boolean pasteBiomes, Mask sourceMask, boolean randomRotate + ) { + //FAWE end this.holder = holder; this.ignoreAirBlocks = ignoreAirBlocks; this.usingOrigin = usingOrigin; this.pasteEntities = pasteEntities; this.pasteBiomes = pasteBiomes; this.sourceMask = sourceMask; + this.randomRotate = true; } @Override public void build(EditSession editSession, BlockVector3 position, Pattern pattern, double size) throws MaxChangedBlocksException { + //FAWE start - random rotation + Transform originalTransform = holder.getTransform(); + Transform transform = new AffineTransform(); + if (this.randomRotate) { + int rotate = 90 * ThreadLocalRandom.current().nextInt(4); + transform = ((AffineTransform) transform).rotateY(rotate); + if (originalTransform != null) { + transform = originalTransform.combine(transform); + } + } + holder.setTransform(transform); + //FAWE end Clipboard clipboard = holder.getClipboard(); Region region = clipboard.getRegion(); BlockVector3 centerOffset = region.getCenter().toBlockPoint().subtract(clipboard.getOrigin()); @@ -77,6 +109,10 @@ public void build(EditSession editSession, BlockVector3 position, Pattern patter .build(); Operations.completeLegacy(operation); + //FAWE start - random rotation + // reset transform + holder.setTransform(originalTransform); + //FAWE end } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/util/annotation/ConfirmHandler.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/util/annotation/ConfirmHandler.java index 5bf1ccc6ce..067f9cfebf 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/util/annotation/ConfirmHandler.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/util/annotation/ConfirmHandler.java @@ -24,7 +24,7 @@ public void beforeCall(Method method, CommandParameters parameters) { } Optional actorOpt = parameters.injectedValue(Key.of(Actor.class)); - if (!actorOpt.isPresent()) { + if (actorOpt.isEmpty()) { return; } Actor actor = actorOpt.get(); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/PlatformCommandManager.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/PlatformCommandManager.java index 3922710fc7..7b493cf985 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/PlatformCommandManager.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/PlatformCommandManager.java @@ -678,31 +678,26 @@ public void handleCommand(CommandEvent event) { Actor actor = event.getActor(); String args = event.getArguments(); - TaskManager.taskManager().taskNow(() -> { - if (!Fawe.isMainThread()) { - Thread.currentThread().setName("FAWE Thread for player: " + actor.getName()); - } - int space0 = args.indexOf(' '); - String arg0 = space0 == -1 ? args : args.substring(0, space0); - Optional optional = commandManager.getCommand(arg0); - if (!optional.isPresent()) { - return; - } - Command cmd = optional.get(); - PermissionCondition queued = cmd.getCondition().as(PermissionCondition.class).orElse(null); - if (queued != null && !queued.isQueued()) { - handleCommandOnCurrentThread(event); - return; - } else { - actor.decline(); + int space0 = args.indexOf(' '); + String arg0 = space0 == -1 ? args : args.substring(0, space0); + Optional optional = commandManager.getCommand(arg0); + if (optional.isEmpty()) { + return; + } + Command cmd = optional.get(); + PermissionCondition queued = cmd.getCondition().as(PermissionCondition.class).orElse(null); + if (queued != null && !queued.isQueued()) { + TaskManager.taskManager().taskNow(() -> handleCommandOnCurrentThread(event), Fawe.isMainThread()); + return; + } else { + actor.decline(); + } + actor.runAction(() -> { + SessionKey key = actor.getSessionKey(); + if (key.isActive()) { + PlatformCommandManager.this.handleCommandOnCurrentThread(event); } - actor.runAction(() -> { - SessionKey key = actor.getSessionKey(); - if (key.isActive()) { - PlatformCommandManager.this.handleCommandOnCurrentThread(event); - } - }, false, true); - }, Fawe.isMainThread()); + }, false, true); } public void handleCommandOnCurrentThread(CommandEvent event) { diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/PlatformManager.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/PlatformManager.java index 406964c244..9f798e21c4 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/PlatformManager.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/PlatformManager.java @@ -321,10 +321,24 @@ public LocalConfiguration getConfiguration() { return queryCapability(Capability.CONFIGURATION).getConfiguration(); } + /** + * Get the current supported {@link SideEffect}s. + * + * @return the supported side effects + * @throws NoCapablePlatformException thrown if no platform is capable + */ public Collection getSupportedSideEffects() { return queryCapability(Capability.WORLD_EDITING).getSupportedSideEffects(); } + /** + * Get the initialized state of the Platform. + * {@return if the platform manager is initialized} + */ + public boolean isInitialized() { + return initialized.get(); + } + /** * You shouldn't have been calling this anyways, but this is now deprecated. Either don't * fire this event at all, or fire the new event via the event bus if you're a platform. diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SpongeSchematicReader.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SpongeSchematicReader.java index 9ad799b4ce..464ef3e4f9 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SpongeSchematicReader.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SpongeSchematicReader.java @@ -266,8 +266,8 @@ private BlockArrayClipboard readVersion1(CompoundTag schematicTag) throws IOExce if (id == null) { continue; } + values.put("id", id); //FAWE end - values.put("id", values.get("Id")); values.remove("Id"); values.remove("Pos"); if (fixer != null) { diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Functions.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Functions.java index 369694f0b3..6f3a637876 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Functions.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Functions.java @@ -75,6 +75,7 @@ private static MethodHandle clean(MethodHandle handle) { ); handle = handle.asType(handle.type().changeReturnType(Number.class)); handle = filterReturnValue(handle, DOUBLE_VALUE); + handle = handle.asType(handle.type().wrap()); } // return vararg-ity if (wasVarargs) { diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/regions/selector/ConvexPolyhedralRegionSelector.java b/worldedit-core/src/main/java/com/sk89q/worldedit/regions/selector/ConvexPolyhedralRegionSelector.java index ced04eddf3..70471ceaea 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/regions/selector/ConvexPolyhedralRegionSelector.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/regions/selector/ConvexPolyhedralRegionSelector.java @@ -26,6 +26,7 @@ import com.sk89q.worldedit.internal.cui.CUIRegion; import com.sk89q.worldedit.internal.cui.SelectionPointEvent; import com.sk89q.worldedit.internal.cui.SelectionPolygonEvent; +import com.sk89q.worldedit.internal.cui.SelectionShapeEvent; import com.sk89q.worldedit.math.BlockVector2; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.regions.ConvexPolyhedralRegion; @@ -206,6 +207,7 @@ public void explainPrimarySelection(Actor player, LocalSession session, BlockVec checkNotNull(session); checkNotNull(pos); + session.dispatchCUIEvent(player, new SelectionShapeEvent(getTypeID())); session.describeCUI(player); player.print(Caption.of("worldedit.selection.convex.explain.primary", TextComponent.of(pos.toString()))); @@ -226,6 +228,7 @@ public void explainSecondarySelection(Actor player, LocalSession session, BlockV public void explainRegionAdjust(Actor player, LocalSession session) { checkNotNull(player); checkNotNull(session); + session.dispatchCUIEvent(player, new SelectionShapeEvent(getTypeID())); session.describeCUI(player); } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/registry/NamespacedRegistry.java b/worldedit-core/src/main/java/com/sk89q/worldedit/registry/NamespacedRegistry.java index 5fca657992..e6715907c3 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/registry/NamespacedRegistry.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/registry/NamespacedRegistry.java @@ -47,8 +47,16 @@ public NamespacedRegistry(final String name) { this(name, MINECRAFT_NAMESPACE); } + public NamespacedRegistry(final String name, final boolean checkInitialized) { + this(name, MINECRAFT_NAMESPACE, checkInitialized); + } + public NamespacedRegistry(final String name, final String defaultNamespace) { - super(name); + this(name, defaultNamespace, false); + } + + public NamespacedRegistry(final String name, final String defaultNamespace, final boolean checkInitialized) { + super(name, checkInitialized); this.defaultNamespace = defaultNamespace; } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/registry/Registry.java b/worldedit-core/src/main/java/com/sk89q/worldedit/registry/Registry.java index 2bb20010a9..b29af49e8a 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/registry/Registry.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/registry/Registry.java @@ -19,6 +19,8 @@ package com.sk89q.worldedit.registry; +import com.sk89q.worldedit.WorldEdit; + import javax.annotation.Nullable; import java.util.Collection; import java.util.Collections; @@ -35,9 +37,15 @@ public class Registry implements Iterable { private final Map map = new HashMap<>(); private final String name; + private final boolean checkInitialized; public Registry(final String name) { + this(name, false); + } + + public Registry(final String name, final boolean checkInitialized) { this.name = name; + this.checkInitialized = checkInitialized; } public String getName() { @@ -53,6 +61,11 @@ public Map getMap() { @Nullable public V get(final String key) { checkState(key.equals(key.toLowerCase(Locale.ROOT)), "key must be lowercase: %s", key); + if (this.checkInitialized) { + checkState( + WorldEdit.getInstance().getPlatformManager().isInitialized(), + "WorldEdit is not initialized yet."); + } return this.map.get(key); } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/Int2BaseBlockMap.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/Int2BaseBlockMap.java index bf8e3bed5e..fca089a68e 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/Int2BaseBlockMap.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/collection/Int2BaseBlockMap.java @@ -170,7 +170,7 @@ public BaseBlock put(int key, BaseBlock value) { return old; } int oldId = commonMap.put(key, internalId); - return assumeAsBlock(oldId); + return BlockStateIdAccess.isValidInternalId(oldId) ? assumeAsBlock(oldId) : uncommonMap.remove(key); } @Override diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/biome/BiomeType.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/biome/BiomeType.java index ab503e1fbb..e8501da690 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/biome/BiomeType.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/biome/BiomeType.java @@ -32,7 +32,7 @@ public class BiomeType implements RegistryItem, Keyed, BiomePattern { //FAWE end - public static final NamespacedRegistry REGISTRY = new NamespacedRegistry<>("biome type"); + public static final NamespacedRegistry REGISTRY = new NamespacedRegistry<>("biome type", true); private final String id; private int legacyId = -1; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockCategory.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockCategory.java index 3ed6f7a3ff..29d8ee53bc 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockCategory.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockCategory.java @@ -36,7 +36,7 @@ public class BlockCategory extends Category implements Keyed { //FAWE start private boolean[] flatMap; //FAWE end - public static final NamespacedRegistry REGISTRY = new NamespacedRegistry<>("block tag"); + public static final NamespacedRegistry REGISTRY = new NamespacedRegistry<>("block tag", true); public BlockCategory(final String id) { super(id); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockState.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockState.java index 87959ebe67..f08675fc59 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockState.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockState.java @@ -20,6 +20,7 @@ package com.sk89q.worldedit.world.block; import com.fastasyncworldedit.core.command.SuggestInputParseException; +import com.fastasyncworldedit.core.configuration.Caption; import com.fastasyncworldedit.core.function.mask.SingleBlockStateMask; import com.fastasyncworldedit.core.queue.ITileInput; import com.fastasyncworldedit.core.registry.state.PropertyKey; @@ -43,6 +44,7 @@ import com.sk89q.worldedit.registry.state.AbstractProperty; import com.sk89q.worldedit.registry.state.Property; import com.sk89q.worldedit.util.concurrency.LazyReference; +import com.sk89q.worldedit.util.formatting.text.TextComponent; import com.sk89q.worldedit.util.nbt.CompoundBinaryTag; import com.sk89q.worldedit.world.registry.BlockMaterial; @@ -150,7 +152,7 @@ public static BlockState get(@Nullable BlockType type, String state, BlockState type = BlockTypes.get(key); if (type == null) { String input = key.toString(); - throw new SuggestInputParseException("Does not match a valid block type: " + input, input, () -> Stream.of( + throw new SuggestInputParseException(Caption.of("fawe.error.invalid-block-type", TextComponent.of(input)), () -> Stream.of( BlockTypesCache.values) .map(BlockType::getId) .filter(id -> StringMan.blockStateMatches(input, id)) @@ -211,8 +213,7 @@ public static BlockState get(@Nullable BlockType type, String state, BlockState String input = charSequence.toString(); BlockType finalType = type; throw new SuggestInputParseException( - "Invalid property " + key + ":" + input + " for type " + type, - input, + Caption.of("worldedit.error.parser.unknown-property", key + ":" + input, type), () -> finalType.getProperties().stream() .map(Property::getName) @@ -222,8 +223,7 @@ public static BlockState get(@Nullable BlockType type, String state, BlockState ); } else { throw new SuggestInputParseException( - "No operator for " + state, - "", + Caption.of("fawe.error.no-operator-for-input", state), () -> Collections.singletonList("=") ); } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockType.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockType.java index 043a89bf57..01a5d5c8a1 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockType.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockType.java @@ -56,7 +56,7 @@ public class BlockType implements Keyed, Pattern { //FAWE end - public static final NamespacedRegistry REGISTRY = new NamespacedRegistry<>("block type"); + public static final NamespacedRegistry REGISTRY = new NamespacedRegistry<>("block type", true); private static final Logger LOGGER = LogManagerCompat.getLogger(); private final String id; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockTypes.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockTypes.java index e79a9af5ae..b8af4bce53 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockTypes.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockTypes.java @@ -20,9 +20,11 @@ package com.sk89q.worldedit.world.block; import com.fastasyncworldedit.core.command.SuggestInputParseException; +import com.fastasyncworldedit.core.configuration.Caption; import com.fastasyncworldedit.core.util.StringMan; import com.sk89q.worldedit.extension.input.InputParseException; import com.sk89q.worldedit.extension.input.ParserContext; +import com.sk89q.worldedit.util.formatting.text.TextComponent; import com.sk89q.worldedit.world.registry.LegacyMapper; import javax.annotation.Nullable; @@ -1967,7 +1969,7 @@ public static BlockType parse(final String type, final ParserContext context) th } } - throw new SuggestInputParseException("Does not match a valid block type: " + inputLower, inputLower, () -> Stream.of( + throw new SuggestInputParseException(Caption.of("fawe.error.invalid-block-type", TextComponent.of(input)), () -> Stream.of( BlockTypesCache.values) .filter(b -> StringMan.blockStateMatches(inputLower, b.getId())) .map(BlockType::getId) diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/entity/EntityType.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/entity/EntityType.java index 98a70b7e56..5a530c8a61 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/entity/EntityType.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/entity/EntityType.java @@ -27,7 +27,7 @@ public class EntityType implements RegistryItem, Keyed { //FAWE end - public static final NamespacedRegistry REGISTRY = new NamespacedRegistry<>("entity type"); + public static final NamespacedRegistry REGISTRY = new NamespacedRegistry<>("entity type", true); private final String id; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/item/ItemType.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/item/ItemType.java index 97549320df..ecba92f8be 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/item/ItemType.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/item/ItemType.java @@ -39,7 +39,7 @@ public class ItemType implements RegistryItem, Keyed { //FAWE end - public static final NamespacedRegistry REGISTRY = new NamespacedRegistry<>("item type"); + public static final NamespacedRegistry REGISTRY = new NamespacedRegistry<>("item type", true); private final String id; @SuppressWarnings("deprecation") diff --git a/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/BaseExpressionTest.java b/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/BaseExpressionTest.java index 0fbf9556a4..3491347a4b 100644 --- a/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/BaseExpressionTest.java +++ b/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/BaseExpressionTest.java @@ -35,6 +35,7 @@ import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -63,6 +64,7 @@ public void load() { }); WorldEdit.getInstance().getPlatformManager().register(mockPlat); WorldEdit.getInstance().getEventBus().post(new PlatformsRegisteredEvent()); + assertTrue(WorldEdit.getInstance().getPlatformManager().isInitialized(), "Platform is not initialized"); WorldEdit.getInstance().getConfiguration().calculationTimeout = 1_000; } diff --git a/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/ExpressionTest.java b/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/ExpressionTest.java index ec175bd721..a1d418f85e 100644 --- a/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/ExpressionTest.java +++ b/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/ExpressionTest.java @@ -449,4 +449,14 @@ public void testTimeout() { assertTrue(e.getMessage().contains("Calculations exceeded time limit")); } + @Test + public void testRound() { + checkTestCase("round(1.3)", 1); + checkTestCase("round(0.9)", 1); + checkTestCase("round(-1.1)", -1); + checkTestCase("round(-0.9)", -1); + checkTestCase("round(1.5)", 2); + checkTestCase("round(-1.5)", -1); + } + } diff --git a/worldedit-core/src/test/java/com/sk89q/worldedit/util/collection/BlockMapTest.java b/worldedit-core/src/test/java/com/sk89q/worldedit/util/collection/BlockMapTest.java index 1bb09ee178..9558ac0bb2 100644 --- a/worldedit-core/src/test/java/com/sk89q/worldedit/util/collection/BlockMapTest.java +++ b/worldedit-core/src/test/java/com/sk89q/worldedit/util/collection/BlockMapTest.java @@ -21,6 +21,8 @@ package com.sk89q.worldedit.util.collection; import com.google.common.collect.ImmutableMap; +import com.sk89q.jnbt.CompoundTag; +import com.sk89q.jnbt.StringTag; import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.event.platform.PlatformsRegisteredEvent; import com.sk89q.worldedit.extension.platform.Capability; @@ -87,12 +89,20 @@ static void setupFakePlatform() { Stream.of(Capability.values()) .collect(Collectors.toMap(Function.identity(), __ -> Preference.NORMAL)) ); + when(MOCKED_PLATFORM.getConfiguration()).thenReturn(new LocalConfiguration() { + @Override + public void load() { + } + }); PlatformManager platformManager = WorldEdit.getInstance().getPlatformManager(); platformManager.register(MOCKED_PLATFORM); WorldEdit.getInstance().getEventBus().post(new PlatformsRegisteredEvent()); + assertTrue(WorldEdit.getInstance().getPlatformManager().isInitialized(), "Platform is not initialized"); + registerBlock("minecraft:air"); registerBlock("minecraft:oak_wood"); + registerBlock("minecraft:chest"); } @AfterAll @@ -116,6 +126,7 @@ private static void registerBlock(String id) { private final BaseBlock air = checkNotNull(BlockTypes.AIR).getDefaultState().toBaseBlock(); private final BaseBlock oakWood = checkNotNull(BlockTypes.OAK_WOOD).getDefaultState().toBaseBlock(); + private final BaseBlock chestWithNbt = checkNotNull(BlockTypes.CHEST).getDefaultState().toBaseBlock(new CompoundTag(ImmutableMap.of("dummy", new StringTag("value")))); private AutoCloseable mocks; @@ -741,6 +752,22 @@ void insertsOnMerge() { }); } + @SuppressWarnings("OverwrittenKey") + @Test + @DisplayName("put with valid and invalid keys doesn't duplicate") + void putWithInvalidAndValid() { + generator.makeVectorsStream().forEach(vec -> { + BlockMap map = BlockMap.createForBaseBlock(); + // This tests https://github.com/EngineHub/WorldEdit/issues/2250 + // Due to two internal maps, a bug existed where both could have the same value + map.put(vec, chestWithNbt); + map.put(vec, air); + + assertEquals(1, map.size()); + assertEquals(air, map.get(vec)); + }); + } + } @Test diff --git a/worldedit-sponge/build.gradle.kts b/worldedit-sponge/build.gradle.kts index 76828b320d..e895e29891 100644 --- a/worldedit-sponge/build.gradle.kts +++ b/worldedit-sponge/build.gradle.kts @@ -28,7 +28,7 @@ dependencies { }) api("org.apache.logging.log4j:log4j-api") api("org.bstats:bstats-sponge:1.7") - testImplementation("org.mockito:mockito-core:5.2.0") + testImplementation("org.mockito:mockito-core:5.3.1") } <<<<<<< HEAD