From a4aadb9950811715293b0b9535833ac5a6845843 Mon Sep 17 00:00:00 2001 From: NotAlexNoyle Date: Fri, 26 Apr 2024 13:10:29 +0000 Subject: [PATCH] WETS-OG 1.1 --- .gitignore | 4 +- README.md | 2 +- build.gradle.kts | 104 +++++++--- bukkit/bin/.gitignore | 1 - bukkit/build.gradle.kts | 28 --- .../ar/emily/wets/bukkit/WESpreadPlugin.java | 67 ------- common/bin/.gitignore | 1 - common/build.gradle.kts | 13 -- .../emily/wets/common/AbstractScheduler.java | 84 -------- .../java/ar/emily/wets/common/Scheduler.java | 15 -- .../java/ar/emily/wets/common/V5UUID.java | 55 ------ .../java/ar/emily/wets/common/WESpread.java | 185 ----------------- settings.gradle | 2 + settings.gradle.kts | 13 -- .../emily/wets/bukkit/AbstractScheduler.java | 85 ++++++++ .../java/ar/emily/wets/bukkit/Scheduler.java | 16 ++ .../java/ar/emily/wets/bukkit/V5UUID.java | 55 ++++++ .../java/ar/emily/wets/bukkit/WESpread.java | 187 ++++++++++++++++++ .../ar/emily/wets/bukkit/WESpreadPlugin.java | 66 +++++++ {bukkit/src => src}/main/resources/plugin.yml | 2 +- 20 files changed, 494 insertions(+), 491 deletions(-) delete mode 100644 bukkit/bin/.gitignore delete mode 100644 bukkit/build.gradle.kts delete mode 100644 bukkit/src/main/java/ar/emily/wets/bukkit/WESpreadPlugin.java delete mode 100644 common/bin/.gitignore delete mode 100644 common/build.gradle.kts delete mode 100644 common/src/main/java/ar/emily/wets/common/AbstractScheduler.java delete mode 100644 common/src/main/java/ar/emily/wets/common/Scheduler.java delete mode 100644 common/src/main/java/ar/emily/wets/common/V5UUID.java delete mode 100644 common/src/main/java/ar/emily/wets/common/WESpread.java create mode 100644 settings.gradle create mode 100644 src/main/java/ar/emily/wets/bukkit/AbstractScheduler.java create mode 100644 src/main/java/ar/emily/wets/bukkit/Scheduler.java create mode 100644 src/main/java/ar/emily/wets/bukkit/V5UUID.java create mode 100644 src/main/java/ar/emily/wets/bukkit/WESpread.java create mode 100644 src/main/java/ar/emily/wets/bukkit/WESpreadPlugin.java rename {bukkit/src => src}/main/resources/plugin.yml (95%) diff --git a/.gitignore b/.gitignore index 71dfeab..33f9e58 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,5 @@ .settings/ build run -bukkit/.classpath -common/.classpath +.classpath +bin/ diff --git a/README.md b/README.md index 3477d89..49df3cd 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A fork of [WETS](https://github.com/emilyy-dev/wets) **without Fabric support** maintained for [TrueOG](https://true-og.net/). -The default blocks per tick is 100. +Modified to build with TrueOG's standard gradle configuration for 1.19.4. The default blocks per tick is manually set to 100. Spreads WorldEdit operations across multiple ticks. diff --git a/build.gradle.kts b/build.gradle.kts index 524b9bd..361b4e1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,33 +1,87 @@ +import java.io.BufferedOutputStream +import java.io.FileOutputStream +import java.nio.file.Files +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + plugins { - id("com.github.johnrengelman.shadow") version "8.1.1" apply false + id("org.jetbrains.kotlin.jvm") version "1.9.23" + id("com.github.johnrengelman.shadow") version "8.1.1" + id("eclipse") } -subprojects { - apply(plugin = "java") - apply(plugin = "eclipse") +group = "net.trueog.wets-og" // Declare bundle identifier. +version = "1.1" - extensions.configure("java") { - toolchain.languageVersion.set(JavaLanguageVersion.of(17)) - } +val apiVersion = "1.19" - repositories { - mavenCentral().mavenContent { releasesOnly() } - } +tasks.named("processResources") { + val props = mapOf( + "version" to version, + "apiVersion" to apiVersion, + ) - tasks { - named("compileJava") { - options.release.set(17) + filesMatching("plugin.yml") { + expand(props) + } +} + +repositories { + + mavenCentral() + + maven { + url = uri("https://repo.purpurmc.org/snapshots") + } + maven { + url = uri("https://maven.enginehub.org/repo") // Get WorldEdit API from EngineHub Repository. + } + +} + +dependencies { + compileOnly("org.purpurmc.purpur:purpur-api:1.19.4-R0.1-SNAPSHOT") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") + implementation("com.sk89q.worldedit:worldedit-bukkit:7.3.0-SNAPSHOT") +} + +tasks.withType().configureEach { + isPreserveFileTimestamps = false + isReproducibleFileOrder = true +} + +tasks.shadowJar { + minimize() +} + +tasks.shadowJar.configure { + archiveClassifier.set("") + from("LICENSE") { + into("/") + } +} + +tasks.jar { + dependsOn("shadowJar") +} + +tasks.jar.configure { + archiveClassifier.set("part") +} + +tasks.withType().configureEach { + options.compilerArgs.add("-parameters") + options.encoding = "UTF-8" + options.forkOptions.executable = File(options.forkOptions.javaHome, "bin/javac").path +} + +kotlin { + jvmToolchain(17) +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + vendor = JvmVendorSpec.GRAAL_VM } - - named("processResources") { - inputs.property("version", this@subprojects.version) - filesNotMatching("**/icon.png") { - expand("version" to this@subprojects.version) - } - } - - withType { - metaInf.from(rootProject.file("LICENSE")).into("ar.emily/wets") - } - } } diff --git a/bukkit/bin/.gitignore b/bukkit/bin/.gitignore deleted file mode 100644 index ddf9c65..0000000 --- a/bukkit/bin/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/main/ diff --git a/bukkit/build.gradle.kts b/bukkit/build.gradle.kts deleted file mode 100644 index d49ef11..0000000 --- a/bukkit/build.gradle.kts +++ /dev/null @@ -1,28 +0,0 @@ -plugins { - id("com.github.johnrengelman.shadow") -} - -repositories { - maven("https://repo.purpurmc.org/snapshots") - maven("https://maven.enginehub.org/repo/") -} - -dependencies { - // this is all set up the wrong way around but shadowJar is special - compileOnly("org.purpurmc.purpur:purpur-api:1.19.4-R0.1-SNAPSHOT") - implementation("com.sk89q.worldedit:worldedit-bukkit:7.2.14") - shadow(project(":wets-common")) { isTransitive = false } -} - -tasks { - shadowJar { - configurations = listOf(project.configurations["shadow"]) - archiveBaseName.set("WETS") - archiveClassifier.set("bukkit") - } - - assemble { - dependsOn(shadowJar) - } -} - diff --git a/bukkit/src/main/java/ar/emily/wets/bukkit/WESpreadPlugin.java b/bukkit/src/main/java/ar/emily/wets/bukkit/WESpreadPlugin.java deleted file mode 100644 index ee8fce4..0000000 --- a/bukkit/src/main/java/ar/emily/wets/bukkit/WESpreadPlugin.java +++ /dev/null @@ -1,67 +0,0 @@ -package ar.emily.wets.bukkit; - -import ar.emily.wets.common.AbstractScheduler; -import ar.emily.wets.common.WESpread; -import com.sk89q.worldedit.bukkit.BukkitAdapter; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.plugin.java.JavaPlugin; -import org.bukkit.util.StringUtil; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -public final class WESpreadPlugin extends JavaPlugin implements Listener { - - private static final List COMPLETIONS = List.of("sorted", "not-sorted"); - - @SuppressWarnings("ThisEscapedInObjectConstruction") - private final AbstractScheduler scheduler = - new AbstractScheduler( - getServer()::getCurrentTick, - task -> getServer().getScheduler().runTaskTimer(this, task, 1L, 1L) - ); - private final WESpread plugin = new WESpread(this.scheduler); - - @EventHandler - public void on(final PlayerQuitEvent event) { - this.plugin.playerLogout(event.getPlayer().getUniqueId()); - } - - @Override - public void onLoad() { - this.plugin.load(); - } - - @Override - public void onEnable() { - this.scheduler.setup(); - getServer().getPluginManager().registerEvents(this, this); - } - - @Override - public boolean onCommand( - final @NotNull CommandSender sender, - final @NotNull Command command, - final @NotNull String label, - final String @NotNull [] args - ) { - this.plugin.command(BukkitAdapter.adapt(sender), Arrays.asList(args)); - return true; - } - - @Override - public @NotNull List onTabComplete( - final @NotNull CommandSender sender, - final @NotNull Command command, - final @NotNull String alias, - final String @NotNull [] args - ) { - return args.length == 1 ? StringUtil.copyPartialMatches(args[0], COMPLETIONS, new ArrayList<>(1)) : List.of(); - } -} diff --git a/common/bin/.gitignore b/common/bin/.gitignore deleted file mode 100644 index ddf9c65..0000000 --- a/common/bin/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/main/ diff --git a/common/build.gradle.kts b/common/build.gradle.kts deleted file mode 100644 index 28f687f..0000000 --- a/common/build.gradle.kts +++ /dev/null @@ -1,13 +0,0 @@ -plugins { - `java-library` -} - -repositories { - maven("https://maven.enginehub.org/repo/") -} - -dependencies { - api("com.sk89q.worldedit:worldedit-core:7.2.14") - api("it.unimi.dsi:fastutil:8.5.9") - compileOnlyApi("org.jetbrains:annotations:23.0.0") -} \ No newline at end of file diff --git a/common/src/main/java/ar/emily/wets/common/AbstractScheduler.java b/common/src/main/java/ar/emily/wets/common/AbstractScheduler.java deleted file mode 100644 index 8440687..0000000 --- a/common/src/main/java/ar/emily/wets/common/AbstractScheduler.java +++ /dev/null @@ -1,84 +0,0 @@ -package ar.emily.wets.common; - -import java.util.ArrayDeque; -import java.util.Iterator; -import java.util.Queue; -import java.util.function.Consumer; -import java.util.function.IntSupplier; - -public final class AbstractScheduler implements Scheduler { - - private final IntSupplier currentTickGetter; - private final Queue pendingTasks = new ArrayDeque<>(); - private final Queue tasks = new ArrayDeque<>(); - private final Consumer underlyingScheduler; - - public AbstractScheduler(final IntSupplier currentTickGetter, final Consumer underlyingScheduler) { - this.currentTickGetter = currentTickGetter; - this.underlyingScheduler = underlyingScheduler; - } - - public void setup() { - this.underlyingScheduler.accept(() -> { - processPendingTasks(); - processTasks(); - }); - } - - @Override - public void runPeriodically(final Consumer action, final long initialDelay, final long period) { - this.pendingTasks.add(new AbstractTask(action, this.currentTickGetter.getAsInt(), initialDelay, period)); - } - - public void flush() { - this.tasks.addAll(this.pendingTasks); - this.pendingTasks.clear(); - - final Iterator it = this.tasks.iterator(); - while (it.hasNext()) { - final AbstractTask task = it.next(); - task.task.accept(it::remove); - } - - this.tasks.clear(); - } - - private void processPendingTasks() { - final int currentTick = this.currentTickGetter.getAsInt(); - final Iterator it = this.pendingTasks.iterator(); - while (it.hasNext()) { - final AbstractTask task = it.next(); - if (task.registerTick + task.initialDelay >= currentTick) { - this.tasks.add(task); - task.registerTick = currentTick; - it.remove(); - } - } - } - - private void processTasks() { - final int currentTick = this.currentTickGetter.getAsInt(); - final Iterator it = this.tasks.iterator(); - while (it.hasNext()) { - final AbstractTask task = it.next(); - if ((currentTick - task.registerTick) % task.period == 0) { - task.task.accept(it::remove); - } - } - } - - private static final class AbstractTask { - - final Consumer task; - final long initialDelay; - final long period; - long registerTick; - - AbstractTask(final Consumer task, final long registerTick, final long initialDelay, final long period) { - this.task = task; - this.registerTick = registerTick; - this.initialDelay = Math.min(initialDelay, 1); - this.period = Math.min(period, 1); - } - } -} diff --git a/common/src/main/java/ar/emily/wets/common/Scheduler.java b/common/src/main/java/ar/emily/wets/common/Scheduler.java deleted file mode 100644 index 90cafc6..0000000 --- a/common/src/main/java/ar/emily/wets/common/Scheduler.java +++ /dev/null @@ -1,15 +0,0 @@ -package ar.emily.wets.common; - -import java.util.function.Consumer; - -public interface Scheduler { - - void flush(); - void runPeriodically(Consumer action, long initialDelay, long period); - - @FunctionalInterface - interface Task { - - void cancel(); - } -} diff --git a/common/src/main/java/ar/emily/wets/common/V5UUID.java b/common/src/main/java/ar/emily/wets/common/V5UUID.java deleted file mode 100644 index 5fa80ac..0000000 --- a/common/src/main/java/ar/emily/wets/common/V5UUID.java +++ /dev/null @@ -1,55 +0,0 @@ -package ar.emily.wets.common; - -import java.lang.invoke.VarHandle; -import java.nio.ByteOrder; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.UUID; - -import static java.lang.invoke.MethodHandles.byteArrayViewVarHandle; - -// why am i overcomplicating this -final class V5UUID { - - private static final VarHandle AS_LONG_ARRAY = byteArrayViewVarHandle(long[].class, ByteOrder.BIG_ENDIAN); - private static final MessageDigest MD; - - static { - try { - MD = MessageDigest.getInstance("SHA-1"); - } catch (final NoSuchAlgorithmException ex) { - // shouldn't throw, SHA-1 is a standard algorithm that is required by Java SE - // https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html - throw new ExceptionInInitializerError(ex); - } - } - - // MessageDigest isn't thread safe, that's why it's being cloned - @SuppressWarnings("AccessToNonThreadSafeStaticField") - private static MessageDigest md() { - try { - return (MessageDigest) MD.clone(); - } catch (final CloneNotSupportedException ex) { - // shouldn't throw - throw new RuntimeException(ex); - } - } - - // use V5 UUID as to not collide with V3 or V4 UUIDs, which are used by the game - static UUID create(final byte[] bs) { - final var md = md(); - md.update(bs); - final byte[] digest = md.digest(); - digest[6] &= 0x0f; // clear version - digest[6] |= 0x50; // set version 5 - digest[8] &= 0x3f; // clear variant - digest[8] |= (byte) 0x80; // set variant 1 - return new UUID( - (long) AS_LONG_ARRAY.get(digest, 0), - (long) AS_LONG_ARRAY.get(digest, 8) - ); - } - - private V5UUID() { - } -} diff --git a/common/src/main/java/ar/emily/wets/common/WESpread.java b/common/src/main/java/ar/emily/wets/common/WESpread.java deleted file mode 100644 index 84ee506..0000000 --- a/common/src/main/java/ar/emily/wets/common/WESpread.java +++ /dev/null @@ -1,185 +0,0 @@ -package ar.emily.wets.common; - -import com.sk89q.worldedit.EditSession; -import com.sk89q.worldedit.WorldEdit; -import com.sk89q.worldedit.WorldEditException; -import com.sk89q.worldedit.event.extent.EditSessionEvent; -import com.sk89q.worldedit.event.platform.PlatformUnreadyEvent; -import com.sk89q.worldedit.extension.platform.Actor; -import com.sk89q.worldedit.extent.AbstractBufferingExtent; -import com.sk89q.worldedit.extent.Extent; -import com.sk89q.worldedit.function.operation.Operation; -import com.sk89q.worldedit.function.operation.RunContext; -import com.sk89q.worldedit.math.BlockVector3; -import com.sk89q.worldedit.util.Identifiable; -import com.sk89q.worldedit.util.collection.BlockMap; -import com.sk89q.worldedit.util.eventbus.EventHandler; -import com.sk89q.worldedit.util.eventbus.Subscribe; -import com.sk89q.worldedit.util.formatting.text.Component; -import com.sk89q.worldedit.util.formatting.text.TextComponent; -import com.sk89q.worldedit.world.block.BaseBlock; -import com.sk89q.worldedit.world.block.BlockStateHolder; -import it.unimi.dsi.fastutil.objects.Object2LongMap; -import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; -import org.jetbrains.annotations.Nullable; - -import java.nio.charset.StandardCharsets; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; -import java.util.function.LongSupplier; -import java.util.stream.Stream; - -public final class WESpread { - - public static final UUID NON_PLAYER_ACTOR_ID = - V5UUID.create(WESpread.class.descriptorString().getBytes(StandardCharsets.UTF_8)); - - // @formatter:off - private static final Component INVALID_ARGUMENT = TextComponent.of("Blocks per tick must be a valid positive number"); - private static final Component COMMAND_USAGE = TextComponent.of("Usage: /wets ( | sorted | not-sorted)"); - private static final Component COMMAND_SORTED = TextComponent.of("Block placement of new operations will now be sorted"); - private static final Component COMMAND_NOT_SORTED = TextComponent.of("Block placement of new operations will now be unsorted"); - private static final Component COMMAND_BPT_UPDATED = TextComponent.of("Blocks per tick placement updated"); - // @formatter:on - - private final Scheduler scheduler; - private final Object2LongMap blocksPerTickMap = new Object2LongOpenHashMap<>(); - private final Set actorsWhosePlacementIsNotSorted = new HashSet<>(); - private long defaultBlocksPerTick = 100; - - public WESpread(final Scheduler scheduler) { - this.scheduler = scheduler; - } - - public void playerLogout(final UUID id) { - this.blocksPerTickMap.removeLong(id); - this.actorsWhosePlacementIsNotSorted.remove(id); - } - - public void load() { - WorldEdit.getInstance().getEventBus().register(this); - } - - // we really really really want to flush the queue before worldedit itself unloads - @Subscribe(priority = EventHandler.Priority.VERY_EARLY) - public void on(final PlatformUnreadyEvent event) { - WorldEdit.getInstance().getEventBus().unregister(this); - this.defaultBlocksPerTick = Long.MAX_VALUE; - this.blocksPerTickMap.clear(); - this.actorsWhosePlacementIsNotSorted.clear(); - this.scheduler.flush(); - } - - @Subscribe - public void on(final EditSessionEvent event) { - if (event.getStage() == EditSession.Stage.BEFORE_CHANGE) { - final UUID actorId = - Optional.ofNullable(event.getActor()) - // use a set ID for non-player actors, such as console or potentially API users - .filter(Actor::isPlayer) - .map(Identifiable::getUniqueId) - .orElse(NON_PLAYER_ACTOR_ID); - event.setExtent(new SchedulingExtent(event.getExtent(), actorId)); - } - } - - public void command(final Actor source, final List args) { - if (args.size() != 1) { - source.print(COMMAND_USAGE); - return; - } - - final UUID id = source.isPlayer() ? source.getUniqueId() : NON_PLAYER_ACTOR_ID; - final String arg = args.iterator().next(); - if ("sorted".equals(arg)) { - this.actorsWhosePlacementIsNotSorted.remove(id); - source.print(COMMAND_SORTED); - return; - } else if ("not-sorted".equals(arg)) { - this.actorsWhosePlacementIsNotSorted.add(id); - source.print(COMMAND_NOT_SORTED); - return; - } - - try { - long blocksPerTick = Long.parseLong(arg); - if (blocksPerTick < 0) { blocksPerTick = Long.MAX_VALUE; } - this.blocksPerTickMap.put(id, 100); - source.print(COMMAND_BPT_UPDATED); - } catch (final NumberFormatException exception) { - source.printError(INVALID_ARGUMENT); - source.print(COMMAND_USAGE); - } - } - - private final class SchedulingExtent extends AbstractBufferingExtent { - - private final BlockMap buffer = BlockMap.createForBaseBlock(); - private final LongSupplier blocksPerTick; - private final boolean sorted; - - private SchedulingExtent(final Extent delegate, final UUID actor) { - super(delegate); - this.sorted = !WESpread.this.actorsWhosePlacementIsNotSorted.contains(actor); - this.blocksPerTick = () -> WESpread.this.blocksPerTickMap.getOrDefault(actor, WESpread.this.defaultBlocksPerTick); - } - - @Override - protected @Nullable BaseBlock getBufferedFullBlock(final BlockVector3 position) { - return this.buffer.computeIfAbsent(position, getExtent()::getFullBlock); - } - - @Override - public > boolean setBlock(final BlockVector3 position, final T block) { - this.buffer.remove(position); // workaround silly WorldEdit bug, see https://github.com/emilyy-dev/wets/issues/1 - this.buffer.put(position, block.toBaseBlock()); - return true; - } - - @Override - protected Operation commitBefore() { - final Iterator> it; - { - Stream> stream = this.buffer.entrySet().stream(); - if (this.sorted) { stream = stream.sorted(Map.Entry.comparingByKey(BlockVector3.sortByCoordsYzx())); } - it = stream.iterator(); - } - - return new Operation() { - boolean started = false; - - @Override - public Operation resume(final RunContext run) { - if (!this.started && it.hasNext()) { - this.started = true; - WESpread.this.scheduler.runPeriodically(this::putBlock, 1L, 1L); - } - - return null; - } - - private void putBlock(final Scheduler.Task task) { - try { - for (long i = 0; it.hasNext() && i < SchedulingExtent.this.blocksPerTick.getAsLong(); ++i) { - final var entry = it.next(); - setDelegateBlock(entry.getKey(), entry.getValue()); - } - } catch (final WorldEditException exception) { - throw new RuntimeException(exception); - } finally { - if (!it.hasNext()) { task.cancel(); } - } - } - - @Override - public void cancel() { - } - }; - } - } -} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..db06faa --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = "WETS-OG" + diff --git a/settings.gradle.kts b/settings.gradle.kts index 2c42e7a..e69de29 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,13 +0,0 @@ -rootProject.name = "WETS-OG" - -pluginManagement { - repositories { - mavenCentral() - gradlePluginPortal() - } -} - -listOf("common", "bukkit").forEach { - include(it) - project(":$it").name = "wets-$it" -} diff --git a/src/main/java/ar/emily/wets/bukkit/AbstractScheduler.java b/src/main/java/ar/emily/wets/bukkit/AbstractScheduler.java new file mode 100644 index 0000000..baff43c --- /dev/null +++ b/src/main/java/ar/emily/wets/bukkit/AbstractScheduler.java @@ -0,0 +1,85 @@ +package ar.emily.wets.bukkit; + +import java.util.ArrayDeque; +import java.util.Iterator; +import java.util.Queue; +import java.util.function.Consumer; +import java.util.function.IntSupplier; + +public final class AbstractScheduler implements Scheduler { + + private final IntSupplier currentTickGetter; + private final Queue pendingTasks = new ArrayDeque<>(); + private final Queue tasks = new ArrayDeque<>(); + private final Consumer underlyingScheduler; + + public AbstractScheduler(final IntSupplier currentTickGetter, final Consumer underlyingScheduler) { + this.currentTickGetter = currentTickGetter; + this.underlyingScheduler = underlyingScheduler; + } + + public void setup() { + this.underlyingScheduler.accept(() -> { + processPendingTasks(); + processTasks(); + }); + } + + @Override + public void runPeriodically(final Consumer action, final long initialDelay, final long period) { + this.pendingTasks.add(new AbstractTask(action, this.currentTickGetter.getAsInt(), initialDelay, period)); + } + + public void flush() { + this.tasks.addAll(this.pendingTasks); + this.pendingTasks.clear(); + + final Iterator it = this.tasks.iterator(); + while (it.hasNext()) { + final AbstractTask task = it.next(); + task.task.accept(it::remove); + } + + this.tasks.clear(); + } + + private void processPendingTasks() { + final int currentTick = this.currentTickGetter.getAsInt(); + final Iterator it = this.pendingTasks.iterator(); + while (it.hasNext()) { + final AbstractTask task = it.next(); + if (task.registerTick + task.initialDelay >= currentTick) { + this.tasks.add(task); + task.registerTick = currentTick; + it.remove(); + } + } + } + + private void processTasks() { + final int currentTick = this.currentTickGetter.getAsInt(); + final Iterator it = this.tasks.iterator(); + while (it.hasNext()) { + final AbstractTask task = it.next(); + if ((currentTick - task.registerTick) % task.period == 0) { + task.task.accept(it::remove); + } + } + } + + private static final class AbstractTask { + + final Consumer task; + final long initialDelay; + final long period; + long registerTick; + + AbstractTask(final Consumer task, final long registerTick, final long initialDelay, final long period) { + this.task = task; + this.registerTick = registerTick; + this.initialDelay = Math.min(initialDelay, 1); + this.period = Math.min(period, 1); + } + } + +} \ No newline at end of file diff --git a/src/main/java/ar/emily/wets/bukkit/Scheduler.java b/src/main/java/ar/emily/wets/bukkit/Scheduler.java new file mode 100644 index 0000000..d193c4c --- /dev/null +++ b/src/main/java/ar/emily/wets/bukkit/Scheduler.java @@ -0,0 +1,16 @@ +package ar.emily.wets.bukkit; + +import java.util.function.Consumer; + +public interface Scheduler { + + void flush(); + void runPeriodically(Consumer action, long initialDelay, long period); + + @FunctionalInterface + interface Task { + + void cancel(); + } + +} \ No newline at end of file diff --git a/src/main/java/ar/emily/wets/bukkit/V5UUID.java b/src/main/java/ar/emily/wets/bukkit/V5UUID.java new file mode 100644 index 0000000..0a36be1 --- /dev/null +++ b/src/main/java/ar/emily/wets/bukkit/V5UUID.java @@ -0,0 +1,55 @@ +package ar.emily.wets.bukkit; + +import static java.lang.invoke.MethodHandles.byteArrayViewVarHandle; + +import java.lang.invoke.VarHandle; +import java.nio.ByteOrder; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.UUID; + +// why am I overcomplicating this? +final class V5UUID { + + private static final VarHandle AS_LONG_ARRAY = byteArrayViewVarHandle(long[].class, ByteOrder.BIG_ENDIAN); + private static final MessageDigest MD; + + static { + try { + MD = MessageDigest.getInstance("SHA-1"); + } catch (final NoSuchAlgorithmException ex) { + // shouldn't throw, SHA-1 is a standard algorithm that is required by Java SE + // https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html + throw new ExceptionInInitializerError(ex); + } + } + + // MessageDigest isn't thread safe, that's why it's being cloned + private static MessageDigest md() { + try { + return (MessageDigest) MD.clone(); + } catch (final CloneNotSupportedException ex) { + // shouldn't throw + throw new RuntimeException(ex); + } + } + + // use V5 UUID as to not collide with V3 or V4 UUIDs, which are used by the game + static UUID create(final byte[] bs) { + final var md = md(); + md.update(bs); + final byte[] digest = md.digest(); + digest[6] &= 0x0f; // clear version + digest[6] |= 0x50; // set version 5 + digest[8] &= 0x3f; // clear variant + digest[8] |= (byte) 0x80; // set variant 1 + return new UUID( + (long) AS_LONG_ARRAY.get(digest, 0), + (long) AS_LONG_ARRAY.get(digest, 8) + ); + } + + private V5UUID() { + } + +} diff --git a/src/main/java/ar/emily/wets/bukkit/WESpread.java b/src/main/java/ar/emily/wets/bukkit/WESpread.java new file mode 100644 index 0000000..c157e86 --- /dev/null +++ b/src/main/java/ar/emily/wets/bukkit/WESpread.java @@ -0,0 +1,187 @@ +package ar.emily.wets.bukkit; + +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.function.LongSupplier; +import java.util.stream.Stream; + +import org.jetbrains.annotations.Nullable; + +import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.WorldEditException; +import com.sk89q.worldedit.event.extent.EditSessionEvent; +import com.sk89q.worldedit.event.platform.PlatformUnreadyEvent; +import com.sk89q.worldedit.extension.platform.Actor; +import com.sk89q.worldedit.extent.AbstractBufferingExtent; +import com.sk89q.worldedit.extent.Extent; +import com.sk89q.worldedit.function.operation.Operation; +import com.sk89q.worldedit.function.operation.RunContext; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.util.Identifiable; +import com.sk89q.worldedit.util.collection.BlockMap; +import com.sk89q.worldedit.util.eventbus.EventHandler; +import com.sk89q.worldedit.util.eventbus.Subscribe; +import com.sk89q.worldedit.util.formatting.text.Component; +import com.sk89q.worldedit.util.formatting.text.TextComponent; +import com.sk89q.worldedit.world.block.BaseBlock; +import com.sk89q.worldedit.world.block.BlockStateHolder; + +import it.unimi.dsi.fastutil.objects.Object2LongMap; +import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; + +public final class WESpread { + + public static final UUID NON_PLAYER_ACTOR_ID = + V5UUID.create(WESpread.class.descriptorString().getBytes(StandardCharsets.UTF_8)); + + // @formatter:off + private static final Component INVALID_ARGUMENT = TextComponent.of("Blocks per tick must be a valid positive number"); + private static final Component COMMAND_USAGE = TextComponent.of("Usage: /wets ( | sorted | not-sorted)"); + private static final Component COMMAND_SORTED = TextComponent.of("Block placement of new operations will now be sorted"); + private static final Component COMMAND_NOT_SORTED = TextComponent.of("Block placement of new operations will now be unsorted"); + private static final Component COMMAND_BPT_UPDATED = TextComponent.of("Blocks per tick placement updated"); + // @formatter:on + + private final Scheduler scheduler; + private final Object2LongMap blocksPerTickMap = new Object2LongOpenHashMap<>(); + private final Set actorsWhosePlacementIsNotSorted = new HashSet<>(); + private long defaultBlocksPerTick = 100; + + public WESpread(final Scheduler scheduler) { + this.scheduler = scheduler; + } + + public void playerLogout(final UUID id) { + this.blocksPerTickMap.removeLong(id); + this.actorsWhosePlacementIsNotSorted.remove(id); + } + + public void load() { + WorldEdit.getInstance().getEventBus().register(this); + } + + // we really really really want to flush the queue before worldedit itself unloads + @Subscribe(priority = EventHandler.Priority.VERY_EARLY) + public void on(final PlatformUnreadyEvent event) { + WorldEdit.getInstance().getEventBus().unregister(this); + this.defaultBlocksPerTick = Long.MAX_VALUE; + this.blocksPerTickMap.clear(); + this.actorsWhosePlacementIsNotSorted.clear(); + this.scheduler.flush(); + } + + @Subscribe + public void on(final EditSessionEvent event) { + if (event.getStage() == EditSession.Stage.BEFORE_CHANGE) { + final UUID actorId = + Optional.ofNullable(event.getActor()) + // use a set ID for non-player actors, such as console or potentially API users + .filter(Actor::isPlayer) + .map(Identifiable::getUniqueId) + .orElse(NON_PLAYER_ACTOR_ID); + event.setExtent(new SchedulingExtent(event.getExtent(), actorId)); + } + } + + public void command(final Actor source, final List args) { + if (args.size() != 1) { + source.print(COMMAND_USAGE); + return; + } + + final UUID id = source.isPlayer() ? source.getUniqueId() : NON_PLAYER_ACTOR_ID; + final String arg = args.iterator().next(); + if ("sorted".equals(arg)) { + this.actorsWhosePlacementIsNotSorted.remove(id); + source.print(COMMAND_SORTED); + return; + } else if ("not-sorted".equals(arg)) { + this.actorsWhosePlacementIsNotSorted.add(id); + source.print(COMMAND_NOT_SORTED); + return; + } + + try { + long blocksPerTick = Long.parseLong(arg); + if (blocksPerTick < 0) { blocksPerTick = Long.MAX_VALUE; } + this.blocksPerTickMap.put(id, 100); + source.print(COMMAND_BPT_UPDATED); + } catch (final NumberFormatException exception) { + source.printError(INVALID_ARGUMENT); + source.print(COMMAND_USAGE); + } + } + + private final class SchedulingExtent extends AbstractBufferingExtent { + + private final BlockMap buffer = BlockMap.createForBaseBlock(); + private final LongSupplier blocksPerTick; + private final boolean sorted; + + private SchedulingExtent(final Extent delegate, final UUID actor) { + super(delegate); + this.sorted = !WESpread.this.actorsWhosePlacementIsNotSorted.contains(actor); + this.blocksPerTick = () -> WESpread.this.blocksPerTickMap.getOrDefault(actor, WESpread.this.defaultBlocksPerTick); + } + + @Override + protected @Nullable BaseBlock getBufferedFullBlock(final BlockVector3 position) { + return this.buffer.computeIfAbsent(position, getExtent()::getFullBlock); + } + + @Override + public > boolean setBlock(final BlockVector3 position, final T block) { + this.buffer.remove(position); // workaround silly WorldEdit bug, see https://github.com/emilyy-dev/wets/issues/1 + this.buffer.put(position, block.toBaseBlock()); + return true; + } + + @Override + protected Operation commitBefore() { + final Iterator> it; + { + Stream> stream = this.buffer.entrySet().stream(); + if (this.sorted) { stream = stream.sorted(Map.Entry.comparingByKey(BlockVector3.sortByCoordsYzx())); } + it = stream.iterator(); + } + + return new Operation() { + boolean started = false; + + @Override + public Operation resume(final RunContext run) { + if (!this.started && it.hasNext()) { + this.started = true; + WESpread.this.scheduler.runPeriodically(this::putBlock, 1L, 1L); + } + + return null; + } + + private void putBlock(final Scheduler.Task task) { + try { + for (long i = 0; it.hasNext() && i < SchedulingExtent.this.blocksPerTick.getAsLong(); ++i) { + final var entry = it.next(); + setDelegateBlock(entry.getKey(), entry.getValue()); + } + } catch (final WorldEditException exception) { + throw new RuntimeException(exception); + } finally { + if (!it.hasNext()) { task.cancel(); } + } + } + + @Override + public void cancel() { + } + }; + } + } +} diff --git a/src/main/java/ar/emily/wets/bukkit/WESpreadPlugin.java b/src/main/java/ar/emily/wets/bukkit/WESpreadPlugin.java new file mode 100644 index 0000000..b7cb65e --- /dev/null +++ b/src/main/java/ar/emily/wets/bukkit/WESpreadPlugin.java @@ -0,0 +1,66 @@ +package ar.emily.wets.bukkit; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.util.StringUtil; +import org.jetbrains.annotations.NotNull; + +import com.sk89q.worldedit.bukkit.BukkitAdapter; + +public final class WESpreadPlugin extends JavaPlugin implements Listener { + + private static final List COMPLETIONS = List.of("sorted", "not-sorted"); + + private final AbstractScheduler scheduler = + new AbstractScheduler( + getServer()::getCurrentTick, + task -> getServer().getScheduler().runTaskTimer(this, task, 1L, 1L) + ); + private final WESpread plugin = new WESpread(this.scheduler); + + @EventHandler + public void on(final PlayerQuitEvent event) { + this.plugin.playerLogout(event.getPlayer().getUniqueId()); + } + + @Override + public void onLoad() { + this.plugin.load(); + } + + @Override + public void onEnable() { + this.scheduler.setup(); + getServer().getPluginManager().registerEvents(this, this); + } + + @Override + public boolean onCommand( + final @NotNull CommandSender sender, + final @NotNull Command command, + final @NotNull String label, + final String @NotNull [] args + ) { + this.plugin.command(BukkitAdapter.adapt(sender), Arrays.asList(args)); + return true; + } + + @Override + public @NotNull List onTabComplete( + final @NotNull CommandSender sender, + final @NotNull Command command, + final @NotNull String alias, + final String @NotNull [] args + ) { + return args.length == 1 ? StringUtil.copyPartialMatches(args[0], COMPLETIONS, new ArrayList<>(1)) : List.of(); + } + +} \ No newline at end of file diff --git a/bukkit/src/main/resources/plugin.yml b/src/main/resources/plugin.yml similarity index 95% rename from bukkit/src/main/resources/plugin.yml rename to src/main/resources/plugin.yml index 6b190c6..9c09344 100644 --- a/bukkit/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -15,4 +15,4 @@ commands: permissions: wets: - default: op \ No newline at end of file + default: op