4 Commits
Author SHA1 Message Date
NotAlexNoyle 254d57fac3 Update README.md 2023-05-07 11:15:39 -04:00
NotAlexNoyle 07658afbc1 Change default blocks per tick to 100, rename plugin, ignore eclipse files 2023-05-07 11:15:03 -04:00
NotAlexNoyle 5e6f48f152 Remove fabric support 2023-05-07 08:24:47 -04:00
emilyy-dev 14221f095e Update to 0.0.3 2023-02-03 23:06:02 -03:00
20 changed files with 42 additions and 440 deletions

No files matched your search

+2 -1
View File
@@ -1,4 +1,5 @@
.gradle
.idea
.project
.settings/
build
run
+6 -5
View File
@@ -1,8 +1,10 @@
## WorldEdit Tick Spreader
## WETS-OG
Spread WorldEdit operations across multiple ticks.
A fork of [WETS](https://github.com/emilyy-dev/wets) **without Fabric support** maintained for [TrueOG](https://true-og.net/).
That's all. Download on [modrinth](https://modrinth.com/mod/wets).
The default blocks per tick is 100.
Spreads WorldEdit operations across multiple ticks.
Semi-configurable, single command `/wets` (`/worldedit-tick-spreader`), one permission (`wets`), two possible arguments:
* `/wets (sorted | not-sorted)` - whether blocks will be placed in a sorted arrangement or not. Sorted is default.
@@ -14,5 +16,4 @@ Sorted placing and block count per tick, affected by commands, are per-player.
## License
Project licensed under the MIT license (also known as Expat license).
The Fabric build of this project redistributes
[lucko/fabric-permissions-api](https://github.com/lucko/fabric-permissions-api) which is licensed under the MIT license.
The original project which supports Fabric API can be found [here](https://github.com/emilyy-dev/wets).
-1
View File
@@ -1,6 +1,5 @@
plugins {
id("com.github.johnrengelman.shadow") version "7.1.2" apply false
id("fabric-loom") version "1.0-SNAPSHOT" apply false
}
subprojects {
+1 -1
View File
@@ -9,7 +9,7 @@ repositories {
dependencies {
// this is all set up the wrong way around but shadowJar is special
implementation("dev.folia:folia-api:1.19.4-R0.1-SNAPSHOT")
implementation("io.papermc.paper:paper-api:1.19.3-R0.1-SNAPSHOT")
implementation("com.sk89q.worldedit:worldedit-bukkit:7.2.13")
shadow(project(":wets-common")) { isTransitive = false }
}
@@ -1,25 +0,0 @@
package ar.emily.wets.bukkit;
import org.bukkit.Location;
import org.bukkit.Server;
import org.bukkit.plugin.Plugin;
import java.util.function.Consumer;
final class BukkitSchedulerAdapter implements SchedulerAdapter.Instance {
@Override
public Consumer<Runnable> scheduleGlobal(final Plugin plugin, final long initialDelay, final long period) {
return task -> plugin.getServer().getScheduler().runTaskTimer(plugin, task, initialDelay, period);
}
@Override
public void runAt(final Plugin plugin, final Location pos, final Runnable task) {
final Server server = plugin.getServer();
if (server.isPrimaryThread()) {
task.run();
} else {
server.getScheduler().runTask(plugin, task);
}
}
}
@@ -1,26 +0,0 @@
package ar.emily.wets.bukkit;
import org.bukkit.Location;
import org.bukkit.Server;
import org.bukkit.plugin.Plugin;
import java.util.function.Consumer;
final class FoliaSchedulerAdapter implements SchedulerAdapter.Instance {
@Override
public Consumer<Runnable> scheduleGlobal(final Plugin plugin, final long initialDelay, final long period) {
final Server server = plugin.getServer();
return task -> server.getGlobalRegionScheduler().runAtFixedRate(plugin, t -> task.run(), initialDelay, period);
}
@Override
public void runAt(final Plugin plugin, final Location pos, final Runnable task) {
final Server server = plugin.getServer();
if (server.isOwnedByCurrentRegion(pos)) {
task.run();
} else {
server.getRegionScheduler().execute(plugin, pos, task);
}
}
}
@@ -1,37 +0,0 @@
package ar.emily.wets.bukkit;
import org.bukkit.Location;
import org.bukkit.plugin.Plugin;
import java.util.function.Consumer;
final class SchedulerAdapter {
private static final Instance INSTANCE = get();
static Consumer<Runnable> scheduleGlobal(final Plugin plugin, final long initialDelay, final long period) {
return INSTANCE.scheduleGlobal(plugin, initialDelay, period);
}
static void runAt(final Plugin plugin, final Location pos, final Runnable task) {
INSTANCE.runAt(plugin, pos, task);
}
private static Instance get() {
try {
Class.forName("io.papermc.paper.threadedregions.scheduler.GlobalRegionScheduler");
return new FoliaSchedulerAdapter();
} catch (final ClassNotFoundException ex) {
return new BukkitSchedulerAdapter();
}
}
private SchedulerAdapter() {
}
interface Instance {
Consumer<Runnable> scheduleGlobal(Plugin plugin, long initialDelay, long period);
void runAt(Plugin plugin, Location pos, Runnable task);
}
}
@@ -20,13 +20,10 @@ public final class WESpreadPlugin extends JavaPlugin implements Listener {
private static final List<String> COMPLETIONS = List.of("sorted", "not-sorted");
private int globalTick = 0;
@SuppressWarnings("ThisEscapedInObjectConstruction")
private final AbstractScheduler scheduler =
new AbstractScheduler(
() -> this.globalTick,
SchedulerAdapter.scheduleGlobal(this, 1L, 1L),
(pos, task) -> SchedulerAdapter.runAt(this, BukkitAdapter.adapt(pos), task)
getServer()::getCurrentTick,
task -> getServer().getScheduler().runTaskTimer(this, task, 1L, 1L)
);
private final WESpread plugin = new WESpread(this.scheduler);
@@ -43,7 +40,6 @@ public final class WESpreadPlugin extends JavaPlugin implements Listener {
@Override
public void onEnable() {
this.scheduler.setup();
SchedulerAdapter.scheduleGlobal(this, 0L, 1L).accept(() -> this.globalTick += 1);
getServer().getPluginManager().registerEvents(this, this);
}
+4 -5
View File
@@ -1,9 +1,8 @@
name: WorldEditTickSpreader
name: WETS-OG
version: ${version}
website: https://github.com/emilyy-dev/wets
website: https://github.com/NotAlexNoyle/WETS-OG
main: ar.emily.wets.bukkit.WESpreadPlugin
api-version: 1.13
folia-supported: true
api-version: 1.18
depend:
- WorldEdit
@@ -16,4 +15,4 @@ commands:
permissions:
wets:
default: op
default: op
+1 -1
View File
@@ -10,4 +10,4 @@ dependencies {
api("com.sk89q.worldedit:worldedit-core:7.2.13")
api("it.unimi.dsi:fastutil:8.5.9")
compileOnlyApi("org.jetbrains:annotations:23.0.0")
}
}
@@ -1,11 +1,8 @@
package ar.emily.wets.common;
import com.sk89q.worldedit.util.Location;
import java.util.ArrayDeque;
import java.util.Iterator;
import java.util.Queue;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.IntSupplier;
@@ -14,21 +11,15 @@ 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> globalScheduler;
private final BiConsumer<? super Location, ? super Runnable> regionScheduler;
private final Consumer<Runnable> underlyingScheduler;
public AbstractScheduler(
final IntSupplier currentTickGetter,
final Consumer<Runnable> globalScheduler,
final BiConsumer<? super Location, ? super Runnable> regionScheduler
) {
public AbstractScheduler(final IntSupplier currentTickGetter, final Consumer<Runnable> underlyingScheduler) {
this.currentTickGetter = currentTickGetter;
this.globalScheduler = globalScheduler;
this.regionScheduler = regionScheduler;
this.underlyingScheduler = underlyingScheduler;
}
public void setup() {
this.globalScheduler.accept(() -> {
this.underlyingScheduler.accept(() -> {
processPendingTasks();
processTasks();
});
@@ -39,11 +30,6 @@ public final class AbstractScheduler implements Scheduler {
this.pendingTasks.add(new AbstractTask(action, this.currentTickGetter.getAsInt(), initialDelay, period));
}
@Override
public void runAt(final Location pos, final Runnable action) {
this.regionScheduler.accept(pos, action);
}
public void flush() {
this.tasks.addAll(this.pendingTasks);
this.pendingTasks.clear();
@@ -1,14 +1,11 @@
package ar.emily.wets.common;
import com.sk89q.worldedit.util.Location;
import java.util.function.Consumer;
public interface Scheduler {
void flush();
void runPeriodically(Consumer<Task> action, long initialDelay, long period);
void runAt(Location pos, Runnable action);
@FunctionalInterface
interface Task {
@@ -12,38 +12,36 @@ 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.Location;
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.util.formatting.text.format.TextColor;
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.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import java.util.function.LongSupplier;
import java.util.stream.Stream;
public final class WESpread {
public static final UUID NON_PLAYER_ACTOR_ID =
public static final UUID NON_IDENTIFIABLE_ACTOR_ID =
V5UUID.create(WESpread.class.descriptorString().getBytes(StandardCharsets.UTF_8));
private static final AtomicLongFieldUpdater<WESpread> DEFAULT_BLOCKS_PER_TICK =
AtomicLongFieldUpdater.newUpdater(WESpread.class, "defaultBlocksPerTick");
// @formatter:off
private static final Component INVALID_ARGUMENT = TextComponent.of("Blocks per tick must be a valid positive number");
private static final Component INVALID_ARGUMENT = TextComponent.of("Blocks per tick must be a valid positive number", TextColor.RED);
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");
@@ -51,17 +49,16 @@ public final class WESpread {
// @formatter:on
private final Scheduler scheduler;
// Folia note: none of the ops on these collections are critical enough to be atomic
private final ConcurrentMap<UUID, Long> blocksPerTickMap = new ConcurrentHashMap<>();
private final Set<UUID> actorsWhosePlacementIsNotSorted = ConcurrentHashMap.newKeySet();
private volatile long defaultBlocksPerTick = 1;
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.remove(id);
this.blocksPerTickMap.removeLong(id);
this.actorsWhosePlacementIsNotSorted.remove(id);
}
@@ -73,7 +70,7 @@ public final class WESpread {
@Subscribe(priority = EventHandler.Priority.VERY_EARLY)
public void on(final PlatformUnreadyEvent event) {
WorldEdit.getInstance().getEventBus().unregister(this);
DEFAULT_BLOCKS_PER_TICK.set(this, Long.MAX_VALUE);
this.defaultBlocksPerTick = Long.MAX_VALUE;
this.blocksPerTickMap.clear();
this.actorsWhosePlacementIsNotSorted.clear();
this.scheduler.flush();
@@ -84,10 +81,9 @@ public final class WESpread {
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);
// use a set ID for non-identifiable actors, such as console or potentially API users
.orElse(NON_IDENTIFIABLE_ACTOR_ID);
event.setExtent(new SchedulingExtent(event.getExtent(), actorId));
}
}
@@ -98,7 +94,7 @@ public final class WESpread {
return;
}
final UUID id = source.isPlayer() ? source.getUniqueId() : NON_PLAYER_ACTOR_ID;
final UUID id = source.isPlayer() ? source.getUniqueId() : NON_IDENTIFIABLE_ACTOR_ID;
final String arg = args.iterator().next();
if ("sorted".equals(arg)) {
this.actorsWhosePlacementIsNotSorted.remove(id);
@@ -113,10 +109,10 @@ public final class WESpread {
try {
long blocksPerTick = Long.parseLong(arg);
if (blocksPerTick < 0) { blocksPerTick = Long.MAX_VALUE; }
this.blocksPerTickMap.put(id, blocksPerTick);
this.blocksPerTickMap.put(id, 100);
source.print(COMMAND_BPT_UPDATED);
} catch (final NumberFormatException exception) {
source.printError(INVALID_ARGUMENT);
source.print(INVALID_ARGUMENT);
source.print(COMMAND_USAGE);
}
}
@@ -130,8 +126,7 @@ public final class WESpread {
private SchedulingExtent(final Extent delegate, final UUID actor) {
super(delegate);
this.sorted = !WESpread.this.actorsWhosePlacementIsNotSorted.contains(actor);
this.blocksPerTick =
() -> WESpread.this.blocksPerTickMap.getOrDefault(actor, DEFAULT_BLOCKS_PER_TICK.get(WESpread.this));
this.blocksPerTick = () -> WESpread.this.blocksPerTickMap.getOrDefault(actor, WESpread.this.defaultBlocksPerTick);
}
@Override
@@ -172,17 +167,10 @@ public final class WESpread {
try {
for (long i = 0; it.hasNext() && i < SchedulingExtent.this.blocksPerTick.getAsLong(); ++i) {
final var entry = it.next();
WESpread.this.scheduler.runAt(
new Location(getExtent(), entry.getKey().toVector3()),
() -> {
try {
setDelegateBlock(entry.getKey(), entry.getValue());
} catch (final WorldEditException ex) {
throw new RuntimeException(ex);
}
}
);
setDelegateBlock(entry.getKey(), entry.getValue());
}
} catch (final WorldEditException exception) {
throw new RuntimeException(exception);
} finally {
if (!it.hasNext()) { task.cancel(); }
}
-38
View File
@@ -1,38 +0,0 @@
plugins {
id("fabric-loom")
}
repositories {
maven("https://oss.sonatype.org/content/repositories/snapshots/").mavenContent { snapshotsOnly() }
maven("https://maven.enginehub.org/repo/")
}
dependencies {
minecraft("com.mojang:minecraft:1.19.3")
mappings(loom.officialMojangMappings())
modImplementation("net.fabricmc:fabric-loader:0.14.11")
setOf(
"fabric-command-api-v2",
"fabric-lifecycle-events-v1"
)
.forEach { modImplementation(fabricApi.module(it, "0.72.0+1.19.3")) }
modImplementation("com.sk89q.worldedit:worldedit-fabric-mc1.19.3:7.2.13")
modImplementation("me.lucko:fabric-permissions-api:0.2-SNAPSHOT") { include(this) }
implementation(project(":wets-common")) { include(this) }
}
tasks {
remapJar {
archiveBaseName.set("WETS")
archiveClassifier.set("fabric")
}
assemble {
dependsOn(remapJar)
}
runClient {
javaLauncher.set(project.javaToolchains.launcherFor(project.java.toolchain))
}
}
@@ -1,125 +0,0 @@
package ar.emily.wets.fabric;
import ar.emily.wets.common.WESpread;
import com.sk89q.worldedit.WorldEdit;
import com.sk89q.worldedit.extension.platform.AbstractNonPlayerActor;
import com.sk89q.worldedit.session.SessionKey;
import com.sk89q.worldedit.util.formatting.WorldEditText;
import com.sk89q.worldedit.util.formatting.text.Component;
import com.sk89q.worldedit.util.formatting.text.serializer.gson.GsonComponentSerializer;
import me.lucko.fabric.api.permissions.v0.Permissions;
import net.minecraft.ChatFormatting;
import net.minecraft.commands.CommandSourceStack;
import org.jetbrains.annotations.NotNull;
import java.util.Locale;
import java.util.UUID;
final class FabricNonPlayerActor extends AbstractNonPlayerActor {
private static final String[] EMPTY_STRINGS = new String[0];
private final CommandSourceStack source;
FabricNonPlayerActor(final CommandSourceStack source) {
this.source = source;
}
@Override
public UUID getUniqueId() {
return WESpread.NON_PLAYER_ACTOR_ID;
}
@Override
public String getName() {
return this.source.getTextName();
}
@Override
@Deprecated
public void printRaw(final String msg) {
for (final String part : msg.split("\n")) {
this.source.sendSystemMessage(net.minecraft.network.chat.Component.literal(part));
}
}
@Override
@Deprecated
public void printDebug(final String msg) {
for (final String part : msg.split("\n")) {
this.source.sendSystemMessage(net.minecraft.network.chat.Component.literal(part).withStyle(ChatFormatting.GRAY));
}
}
@Override
@Deprecated
public void print(final String msg) {
for (final String part : msg.split("\n")) {
this.source.sendSystemMessage(
net.minecraft.network.chat.Component.literal(part).withStyle(ChatFormatting.LIGHT_PURPLE)
);
}
}
@Override
@Deprecated
public void printError(final String msg) {
for (final String part : msg.split("\n")) {
this.source.sendSystemMessage(net.minecraft.network.chat.Component.literal(part).withStyle(ChatFormatting.RED));
}
}
@Override
@SuppressWarnings("DataFlowIssue")
public void print(final Component component) {
this.source.sendSystemMessage(
net.minecraft.network.chat.Component.Serializer.fromJson(
GsonComponentSerializer.INSTANCE.serialize(WorldEditText.format(component, getLocale()))
)
);
}
@Override
public Locale getLocale() {
return WorldEdit.getInstance().getConfiguration().defaultLocale;
}
@Override
public SessionKey getSessionKey() {
return new SessionKey() {
@Override
public @NotNull String getName() {
return FabricNonPlayerActor.this.getName();
}
@Override
public boolean isActive() {
return true;
}
@Override
public boolean isPersistent() {
return true;
}
@Override
public UUID getUniqueId() {
return FabricNonPlayerActor.this.getUniqueId();
}
};
}
@Override
public String[] getGroups() {
return EMPTY_STRINGS;
}
@Override
public void checkPermission(final String permission) {
}
@Override
public boolean hasPermission(final String permission) {
return Permissions.check(this.source, permission);
}
}
@@ -1,82 +0,0 @@
package ar.emily.wets.fabric;
import ar.emily.wets.common.AbstractScheduler;
import ar.emily.wets.common.WESpread;
import com.mojang.brigadier.Command;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.tree.LiteralCommandNode;
import com.sk89q.worldedit.extension.platform.Actor;
import com.sk89q.worldedit.fabric.FabricAdapter;
import me.lucko.fabric.api.permissions.v0.Permissions;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.minecraft.commands.CommandSourceStack;
import java.util.List;
import java.util.function.Predicate;
import static com.mojang.brigadier.arguments.LongArgumentType.getLong;
import static com.mojang.brigadier.arguments.LongArgumentType.longArg;
import static net.minecraft.commands.Commands.LEVEL_GAMEMASTERS;
import static net.minecraft.commands.Commands.argument;
import static net.minecraft.commands.Commands.literal;
public final class WESpreadMod implements ModInitializer {
private static final List<String> SORTED = List.of("sorted");
private static final List<String> NOT_SORTED = List.of("not-sorted");
private static Actor adaptActor(final CommandSourceStack source) throws CommandSyntaxException {
return source.isPlayer() ?
FabricAdapter.adaptPlayer(source.getPlayerOrException()) :
new FabricNonPlayerActor(source);
}
private WESpread plugin;
@Override
public void onInitialize() {
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> command(dispatcher));
ServerLifecycleEvents.SERVER_STARTING.register(server -> {
final AbstractScheduler scheduler =
new AbstractScheduler(
server::getTickCount,
task -> ServerTickEvents.END_SERVER_TICK.register(s -> task.run()),
(pos, task) -> task.run() // already on server tick
);
scheduler.setup();
this.plugin = new WESpread(scheduler);
this.plugin.load();
});
ServerPlayConnectionEvents.DISCONNECT.register((packetListener, server) ->
this.plugin.playerLogout(packetListener.getPlayer().getUUID())
);
}
private int executeCommand(final CommandContext<? extends CommandSourceStack> ctx, final List<String> args)
throws CommandSyntaxException {
this.plugin.command(adaptActor(ctx.getSource()), args);
return Command.SINGLE_SUCCESS;
}
private void command(final CommandDispatcher<CommandSourceStack> dispatcher) {
final Predicate<CommandSourceStack> permissionRequirement = Permissions.require("wets", LEVEL_GAMEMASTERS);
final LiteralCommandNode<CommandSourceStack> wetsCommand =
dispatcher.register(
literal("worldedit-tick-spreader")
.requires(permissionRequirement)
.then(literal("sorted").executes(ctx -> executeCommand(ctx, SORTED)))
.then(literal("not-sorted").executes(ctx -> executeCommand(ctx, NOT_SORTED)))
.then(argument("blocks-per-tick", longArg()).executes(ctx ->
executeCommand(ctx, List.of(String.valueOf(getLong(ctx, "blocks-per-tick"))))
))
);
dispatcher.register(literal("wets").requires(permissionRequirement).redirect(wetsCommand));
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

-31
View File
@@ -1,31 +0,0 @@
{
"schemaVersion": 1,
"id": "worldedit-tick-spreader",
"version": "${version}",
"icon": "assets/worldedit-tick-spreader/icon.png",
"name": "WorldEdit Tick Spreader",
"authors": [
"emilyy-dev"
],
"contact": {
"sources": "https://github.com/emilyy-dev/wets",
"issues": "https://github.com/emilyy-dev/wets/issues"
},
"license": "MIT",
"environment": "*",
"entrypoints": {
"main": [
"ar.emily.wets.fabric.WESpreadMod"
]
},
"depends": {
"fabric-permissions-api-v0": "*",
"fabricloader": "*",
"java": ">=17",
"minecraft": "1.19.3",
"worldedit": "*"
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
org.gradle.jvmargs=-Xmx1G
org.gradle.caching=false
group=ar.emily
version=0.0.4-SNAPSHOT
version=0.0.3
+1 -2
View File
@@ -2,13 +2,12 @@ rootProject.name = "worldedit-spread-placing"
pluginManagement {
repositories {
maven("https://maven.fabricmc.net/")
mavenCentral()
gradlePluginPortal()
}
}
listOf("common", "bukkit", "fabric").forEach {
listOf("common", "bukkit").forEach {
include(it)
project(":$it").name = "wets-$it"
}