WETS-OG 1.1
This commit is contained in:
1 parent
b637afb3a3
commit
a4aadb9950
20 files changed
+494
-491
No files matched your search
+2
-2
@@ -4,5 +4,5 @@
|
||||
.settings/
|
||||
build
|
||||
run
|
||||
bukkit/.classpath
|
||||
common/.classpath
|
||||
.classpath
|
||||
bin/
|
||||
@@ -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
@@ -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 +0,0 @@
|
||||
/main/
|
||||
@@ -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 +0,0 @@
|
||||
/main/
|
||||
@@ -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() {
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
rootProject.name = "WETS-OG"
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user