WETS-OG 1.1

This commit is contained in:
NotAlexNoyle committed 2024-04-26 13:10:29 +00:00
1 parent b637afb3a3
commit a4aadb9950
20 files changed
+494 -491

No files matched your search

+2 -2
View File
@@ -4,5 +4,5 @@
.settings/
build
run
bukkit/.classpath
common/.classpath
.classpath
bin/
+1 -1
View File
@@ -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.
+79 -25
View File
@@ -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<JavaPluginExtension>("java") {
toolchain.languageVersion.set(JavaLanguageVersion.of(17))
}
val apiVersion = "1.19"
repositories {
mavenCentral().mavenContent { releasesOnly() }
}
tasks.named<ProcessResources>("processResources") {
val props = mapOf(
"version" to version,
"apiVersion" to apiVersion,
)
tasks {
named<JavaCompile>("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<AbstractArchiveTask>().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<JavaCompile>().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>("processResources") {
inputs.property("version", this@subprojects.version)
filesNotMatching("**/icon.png") {
expand("version" to this@subprojects.version)
}
}
withType<Jar> {
metaInf.from(rootProject.file("LICENSE")).into("ar.emily/wets")
}
}
}
-1
View File
@@ -1 +0,0 @@
/main/
-28
View File
@@ -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)
}
}
@@ -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<String> 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<String> 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();
}
}
-1
View File
@@ -1 +0,0 @@
/main/
-13
View File
@@ -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")
}
@@ -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<AbstractTask> pendingTasks = new ArrayDeque<>();
private final Queue<AbstractTask> tasks = new ArrayDeque<>();
private final Consumer<Runnable> underlyingScheduler;
public AbstractScheduler(final IntSupplier currentTickGetter, final Consumer<Runnable> underlyingScheduler) {
this.currentTickGetter = currentTickGetter;
this.underlyingScheduler = underlyingScheduler;
}
public void setup() {
this.underlyingScheduler.accept(() -> {
processPendingTasks();
processTasks();
});
}
@Override
public void runPeriodically(final Consumer<Task> 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<AbstractTask> 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<AbstractTask> 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<AbstractTask> 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> task;
final long initialDelay;
final long period;
long registerTick;
AbstractTask(final Consumer<Task> 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);
}
}
}
@@ -1,15 +0,0 @@
package ar.emily.wets.common;
import java.util.function.Consumer;
public interface Scheduler {
void flush();
void runPeriodically(Consumer<Task> action, long initialDelay, long period);
@FunctionalInterface
interface Task {
void cancel();
}
}
@@ -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() {
}
}
@@ -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 (<blocks per tick> | 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<UUID> blocksPerTickMap = new Object2LongOpenHashMap<>();
private final Set<UUID> 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<String> 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<BaseBlock> 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 <T extends BlockStateHolder<T>> 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<Map.Entry<BlockVector3, BaseBlock>> it;
{
Stream<Map.Entry<BlockVector3, BaseBlock>> 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() {
}
};
}
}
}
+2
View File
@@ -0,0 +1,2 @@
rootProject.name = "WETS-OG"
-13
View File
@@ -1,13 +0,0 @@
rootProject.name = "WETS-OG"
pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
}
listOf("common", "bukkit").forEach {
include(it)
project(":$it").name = "wets-$it"
}
@@ -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<AbstractTask> pendingTasks = new ArrayDeque<>();
private final Queue<AbstractTask> tasks = new ArrayDeque<>();
private final Consumer<Runnable> underlyingScheduler;
public AbstractScheduler(final IntSupplier currentTickGetter, final Consumer<Runnable> underlyingScheduler) {
this.currentTickGetter = currentTickGetter;
this.underlyingScheduler = underlyingScheduler;
}
public void setup() {
this.underlyingScheduler.accept(() -> {
processPendingTasks();
processTasks();
});
}
@Override
public void runPeriodically(final Consumer<Task> 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<AbstractTask> 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<AbstractTask> 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<AbstractTask> 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> task;
final long initialDelay;
final long period;
long registerTick;
AbstractTask(final Consumer<Task> 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);
}
}
}
@@ -0,0 +1,16 @@
package ar.emily.wets.bukkit;
import java.util.function.Consumer;
public interface Scheduler {
void flush();
void runPeriodically(Consumer<Task> action, long initialDelay, long period);
@FunctionalInterface
interface Task {
void cancel();
}
}
@@ -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() {
}
}
@@ -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 (<blocks per tick> | 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<UUID> blocksPerTickMap = new Object2LongOpenHashMap<>();
private final Set<UUID> 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<String> 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<BaseBlock> 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 <T extends BlockStateHolder<T>> 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<Map.Entry<BlockVector3, BaseBlock>> it;
{
Stream<Map.Entry<BlockVector3, BaseBlock>> 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() {
}
};
}
}
}
@@ -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<String> 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<String> 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();
}
}
@@ -15,4 +15,4 @@ commands:
permissions:
wets:
default: op
default: op