From 3ab6bce4038b8c7319a0685d57b84a5180222380 Mon Sep 17 00:00:00 2001 From: LT_Name <572413378@qq.com> Date: Sun, 1 Mar 2026 12:27:58 +0800 Subject: [PATCH 01/12] feat: preliminarily compatible with nkmot --- settings.gradle.kts | 2 +- worldedit-libs/nukkit-mot/build.gradle.kts | 3 + worldedit-nukkit-mot/build.gradle.kts | 126 ++++++ .../nukkitmot/FaweNukkit.java | 104 +++++ .../nukkitmot/NukkitNbtConverter.java | 159 +++++++ .../nukkitmot/NukkitPlatformAdapter.java | 16 + .../nukkitmot/NukkitQueueHandler.java | 21 + .../nukkitmot/NukkitRelighter.java | 63 +++ .../nukkitmot/NukkitTaskManager.java | 60 +++ .../nukkitmot/mapping/BiomeMapping.java | 90 ++++ .../nukkitmot/mapping/BlockMapping.java | 376 ++++++++++++++++ .../nukkitmot/mapping/ItemMapping.java | 131 ++++++ .../nukkitmot/mapping/JeBlockState.java | 125 +++++ .../nukkitmot/mapping/NukkitBlockData.java | 19 + .../worldedit/nukkitmot/NukkitAdapter.java | 148 ++++++ .../nukkitmot/NukkitBlockRegistry.java | 28 ++ .../nukkitmot/NukkitCommandSender.java | 122 +++++ .../nukkitmot/NukkitConfiguration.java | 17 + .../worldedit/nukkitmot/NukkitEntity.java | 126 ++++++ .../worldedit/nukkitmot/NukkitGetBlocks.java | 426 ++++++++++++++++++ .../nukkitmot/NukkitGetBlocks_Copy.java | 265 +++++++++++ .../nukkitmot/NukkitItemRegistry.java | 18 + .../worldedit/nukkitmot/NukkitPlayer.java | 261 +++++++++++ .../worldedit/nukkitmot/NukkitRegistries.java | 33 ++ .../nukkitmot/NukkitServerInterface.java | 241 ++++++++++ .../worldedit/nukkitmot/NukkitWorld.java | 291 ++++++++++++ .../nukkitmot/NukkitWorldEditListener.java | 142 ++++++ .../nukkitmot/WorldEditNukkitPlugin.java | 156 +++++++ .../src/main/resources/plugin.yml | 8 + 29 files changed, 3576 insertions(+), 1 deletion(-) create mode 100644 worldedit-libs/nukkit-mot/build.gradle.kts create mode 100644 worldedit-nukkit-mot/build.gradle.kts create mode 100644 worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/FaweNukkit.java create mode 100644 worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitNbtConverter.java create mode 100644 worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitPlatformAdapter.java create mode 100644 worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitQueueHandler.java create mode 100644 worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitRelighter.java create mode 100644 worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitTaskManager.java create mode 100644 worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/BiomeMapping.java create mode 100644 worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/BlockMapping.java create mode 100644 worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/ItemMapping.java create mode 100644 worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/JeBlockState.java create mode 100644 worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/NukkitBlockData.java create mode 100644 worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitAdapter.java create mode 100644 worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitBlockRegistry.java create mode 100644 worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitCommandSender.java create mode 100644 worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitConfiguration.java create mode 100644 worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitEntity.java create mode 100644 worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitGetBlocks.java create mode 100644 worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitGetBlocks_Copy.java create mode 100644 worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitItemRegistry.java create mode 100644 worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitPlayer.java create mode 100644 worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitRegistries.java create mode 100644 worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitServerInterface.java create mode 100644 worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitWorld.java create mode 100644 worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitWorldEditListener.java create mode 100644 worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/WorldEditNukkitPlugin.java create mode 100644 worldedit-nukkit-mot/src/main/resources/plugin.yml diff --git a/settings.gradle.kts b/settings.gradle.kts index 77b27b66d9..9c89936565 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -59,7 +59,7 @@ listOf("1_20_2", "1_20_4", "1_20_5", "1_21", "1_21_4", "1_21_5", "1_21_6", "1_21 include("worldedit-bukkit:adapters:adapter-$it") } -listOf("bukkit", "core", "cli").forEach { +listOf("bukkit", "core", "cli", "nukkit-mot").forEach { include("worldedit-libs:$it") include("worldedit-$it") } diff --git a/worldedit-libs/nukkit-mot/build.gradle.kts b/worldedit-libs/nukkit-mot/build.gradle.kts new file mode 100644 index 0000000000..3f6c7e06cb --- /dev/null +++ b/worldedit-libs/nukkit-mot/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("buildlogic.libs") +} diff --git a/worldedit-nukkit-mot/build.gradle.kts b/worldedit-nukkit-mot/build.gradle.kts new file mode 100644 index 0000000000..336cb049e6 --- /dev/null +++ b/worldedit-nukkit-mot/build.gradle.kts @@ -0,0 +1,126 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +plugins { + `java-library` + id("buildlogic.platform") +} + +project.description = "Nukkit-MOT" + +platform { + kind = buildlogic.WorldEditKind.Plugin + includeClasspath = true +} + +repositories { + mavenLocal() + mavenCentral() + maven { + name = "EngineHub Repository" + url = uri("https://maven.enginehub.org/repo/") + } + maven { + name = "OpenCollab Releases" + url = uri("https://repo.opencollab.dev/maven-releases/") + } + maven { + name = "OpenCollab Snapshots" + url = uri("https://repo.opencollab.dev/maven-snapshots/") + } + maven { + name = "repo-lanink-cn" + url = uri("https://repo.lanink.cn/repository/maven-public/") + } + ivy { + url = uri("https://raw.githubusercontent.com/") + patternLayout { + artifact("[organisation]/[revision]/[artifact].([ext])") + setM2compatible(true) + } + metadataSources { + artifact() + } + } +} + +val geyserMappings: Configuration by configurations.register("geyserMappings") { + isCanBeConsumed = false +} + +val mcmeta: Configuration by configurations.register("mcmeta") { + isCanBeConsumed = false +} + +dependencies { + api(project(":worldedit-core")) + api(project(":worldedit-libs:nukkit-mot")) + + compileOnly("cn.nukkit:Nukkit:MOT-SNAPSHOT") + + implementation(libs.fastutil) + implementation(libs.gson) + + api(libs.lz4Java) { isTransitive = false } + api(libs.sparsebitset) { isTransitive = false } + api(libs.parallelgzip) { isTransitive = false } + compileOnlyApi(libs.checkerqual) + + geyserMappings("GeyserMC.mappings", "items", "15398c1", ext = "json") + geyserMappings("GeyserMC.mappings", "biomes", "15398c1", ext = "json") + geyserMappings("GeyserMC.mappings-generator", "generator_blocks", "8fa6058", ext = "json") + mcmeta("misode.mcmeta", "blocks/data", "cb195b9", ext = "json") +} + +tasks.named("processResources") { + val internalVersion = project.ext["internalVersion"] + inputs.property("internalVersion", internalVersion) + filesMatching("plugin.yml") { + expand(mapOf("internalVersion" to internalVersion)) + } + from(geyserMappings) { + into("mapping") + rename("(?:generator_)?([^-]+)-(.*)\\.json", "$1.json") + } + from(mcmeta) { + rename("data-(.*)\\.json", "je_blocks.json") + } +} + +tasks.named("shadowJar") { + archiveFileName.set("${rootProject.name}-Nukkit-MOT-${project.version}.${archiveExtension.getOrElse("jar")}") + dependencies { + include(dependency(":worldedit-core")) + include(dependency(":worldedit-libs:nukkit-mot")) + relocate("org.antlr.v4", "com.sk89q.worldedit.antlr4") { + include(dependency("org.antlr:antlr4-runtime")) + } + relocate("it.unimi.dsi.fastutil", "com.sk89q.worldedit.bukkit.fastutil") { + include(dependency(libs.fastutil)) + } + relocate("net.royawesome.jlibnoise", "com.sk89q.worldedit.jlibnoise") { + include(dependency("com.sk89q.lib:jlibnoise")) + } + relocate("com.zaxxer", "com.fastasyncworldedit.core.math") { + include(dependency(libs.sparsebitset)) + } + relocate("org.anarres", "com.fastasyncworldedit.core.internal.io") { + include(dependency(libs.parallelgzip)) + } + // ZSTD does not work if relocated. https://github.com/luben/zstd-jni/issues/189 + include(dependency(libs.zstd)) + include(dependency(libs.lz4Java)) + } + minimize { + exclude(dependency(libs.lz4Java)) + } +} + +tasks.named("assemble").configure { + dependsOn("shadowJar") +} + +configure { + publications.named("maven") { + from(components["java"]) + } +} diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/FaweNukkit.java b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/FaweNukkit.java new file mode 100644 index 0000000000..2423159415 --- /dev/null +++ b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/FaweNukkit.java @@ -0,0 +1,104 @@ +package com.fastasyncworldedit.nukkitmot; + +import cn.nukkit.Player; +import cn.nukkit.Server; +import cn.nukkit.plugin.Plugin; +import com.fastasyncworldedit.core.FAWEPlatformAdapterImpl; +import com.fastasyncworldedit.core.Fawe; +import com.fastasyncworldedit.core.IFawe; +import com.fastasyncworldedit.core.queue.implementation.QueueHandler; +import com.fastasyncworldedit.core.queue.implementation.preloader.Preloader; +import com.fastasyncworldedit.core.regions.FaweMaskManager; +import com.fastasyncworldedit.core.util.TaskManager; + +import java.io.File; +import java.util.Collection; +import java.util.Collections; +import java.util.UUID; + +public class FaweNukkit implements IFawe { + + private final Plugin plugin; + private final NukkitTaskManager taskManager; + private final NukkitPlatformAdapter platformAdapter; + private NukkitQueueHandler queueHandler; + + public FaweNukkit(Plugin plugin) { + this.plugin = plugin; + this.taskManager = new NukkitTaskManager(plugin); + this.platformAdapter = new NukkitPlatformAdapter(); + try { + Fawe.set(this); + Fawe.setupInjector(); + } catch (final Exception e) { + throw new RuntimeException("Failed to initialize FAWE", e); + } + } + + @Override + public File getDirectory() { + return plugin.getDataFolder(); + } + + @Override + public TaskManager getTaskManager() { + return taskManager; + } + + @Override + public Collection getMaskManagers() { + return Collections.emptyList(); + } + + @Override + public String getPlatform() { + return "Nukkit"; + } + + @Override + public UUID getUUID(String name) { + Player player = Server.getInstance().getPlayerExact(name); + if (player != null) { + return player.getUniqueId(); + } + return null; + } + + @Override + public String getName(UUID uuid) { + Server server = Server.getInstance(); + for (Player player : server.getOnlinePlayers().values()) { + if (player.getUniqueId().equals(uuid)) { + return player.getName(); + } + } + return null; + } + + @Override + public QueueHandler getQueueHandler() { + if (queueHandler == null) { + synchronized (this) { + if (queueHandler == null) { + queueHandler = new NukkitQueueHandler(); + } + } + } + return queueHandler; + } + + @Override + public Preloader getPreloader(boolean initialise) { + return null; + } + + @Override + public FAWEPlatformAdapterImpl getPlatformAdapter() { + return platformAdapter; + } + + public Plugin getPlugin() { + return plugin; + } + +} diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitNbtConverter.java b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitNbtConverter.java new file mode 100644 index 0000000000..dc2280bdce --- /dev/null +++ b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitNbtConverter.java @@ -0,0 +1,159 @@ +package com.fastasyncworldedit.nukkitmot; + +import cn.nukkit.nbt.tag.ByteArrayTag; +import cn.nukkit.nbt.tag.ByteTag; +import cn.nukkit.nbt.tag.CompoundTag; +import cn.nukkit.nbt.tag.DoubleTag; +import cn.nukkit.nbt.tag.FloatTag; +import cn.nukkit.nbt.tag.IntArrayTag; +import cn.nukkit.nbt.tag.IntTag; +import cn.nukkit.nbt.tag.ListTag; +import cn.nukkit.nbt.tag.LongTag; +import cn.nukkit.nbt.tag.ShortTag; +import cn.nukkit.nbt.tag.StringTag; +import cn.nukkit.nbt.tag.Tag; +import com.fastasyncworldedit.core.nbt.FaweCompoundTag; +import org.enginehub.linbus.common.LinTagId; +import org.enginehub.linbus.tree.LinByteArrayTag; +import org.enginehub.linbus.tree.LinByteTag; +import org.enginehub.linbus.tree.LinCompoundTag; +import org.enginehub.linbus.tree.LinDoubleTag; +import org.enginehub.linbus.tree.LinFloatTag; +import org.enginehub.linbus.tree.LinIntArrayTag; +import org.enginehub.linbus.tree.LinIntTag; +import org.enginehub.linbus.tree.LinListTag; +import org.enginehub.linbus.tree.LinLongArrayTag; +import org.enginehub.linbus.tree.LinLongTag; +import org.enginehub.linbus.tree.LinShortTag; +import org.enginehub.linbus.tree.LinStringTag; +import org.enginehub.linbus.tree.LinTag; +import org.enginehub.linbus.tree.LinTagType; + +import javax.annotation.Nullable; +import java.util.Map; + +/** + * Converts between Nukkit NBT tags and FAWE LinBus tags. + */ +public final class NukkitNbtConverter { + + private NukkitNbtConverter() { + } + + /** + * Convert a Nukkit CompoundTag to a FaweCompoundTag. + */ + public static FaweCompoundTag toFawe(CompoundTag nukkitTag) { + return FaweCompoundTag.of(toLinCompound(nukkitTag)); + } + + /** + * Convert a FaweCompoundTag to a Nukkit CompoundTag. + */ + public static CompoundTag toNukkit(FaweCompoundTag faweTag) { + return toNukkitCompound(faweTag.linTag()); + } + + /** + * Convert a Nukkit CompoundTag to a LinCompoundTag. + */ + public static LinCompoundTag toLinCompound(CompoundTag nukkitTag) { + LinCompoundTag.Builder builder = LinCompoundTag.builder(); + for (Map.Entry entry : nukkitTag.getTags().entrySet()) { + LinTag linTag = toLinTag(entry.getValue()); + if (linTag != null) { + builder.put(entry.getKey(), linTag); + } + } + return builder.build(); + } + + /** + * Convert a LinCompoundTag to a Nukkit CompoundTag. + */ + public static CompoundTag toNukkitCompound(LinCompoundTag linTag) { + CompoundTag result = new CompoundTag(); + for (Map.Entry> entry : linTag.value().entrySet()) { + Tag nukkitTag = toNukkitTag(entry.getValue()); + if (nukkitTag != null) { + result.put(entry.getKey(), nukkitTag); + } + } + return result; + } + + @Nullable + @SuppressWarnings({"unchecked", "rawtypes"}) + private static LinTag toLinTag(Tag nukkitTag) { + return switch (nukkitTag.getId()) { + case Tag.TAG_Byte -> LinByteTag.of((byte) ((ByteTag) nukkitTag).data); + case Tag.TAG_Short -> LinShortTag.of((short) ((ShortTag) nukkitTag).data); + case Tag.TAG_Int -> LinIntTag.of(((IntTag) nukkitTag).data); + case Tag.TAG_Long -> LinLongTag.of(((LongTag) nukkitTag).data); + case Tag.TAG_Float -> LinFloatTag.of(((FloatTag) nukkitTag).data); + case Tag.TAG_Double -> LinDoubleTag.of(((DoubleTag) nukkitTag).data); + case Tag.TAG_Byte_Array -> LinByteArrayTag.of(((ByteArrayTag) nukkitTag).getData()); + case Tag.TAG_String -> LinStringTag.of(((StringTag) nukkitTag).data); + case Tag.TAG_List -> { + ListTag listTag = (ListTag) nukkitTag; + if (listTag.size() == 0) { + yield LinListTag.empty(LinTagType.endTag()); + } + byte elementType = listTag.type; + LinTagId linId = LinTagId.fromId(elementType); + LinListTag.Builder builder = LinListTag.builder(LinTagType.fromId(linId)); + for (Tag item : listTag.getAll()) { + LinTag converted = toLinTag(item); + if (converted != null) { + builder.add(converted); + } + } + yield builder.build(); + } + case Tag.TAG_Compound -> toLinCompound((CompoundTag) nukkitTag); + case Tag.TAG_Int_Array -> LinIntArrayTag.of(((IntArrayTag) nukkitTag).getData()); + default -> null; + }; + } + + @Nullable + @SuppressWarnings({"unchecked", "rawtypes"}) + private static Tag toNukkitTag(LinTag linTag) { + return switch (linTag.type().id()) { + case BYTE -> new ByteTag("", ((LinByteTag) linTag).valueAsByte()); + case SHORT -> new ShortTag("", ((LinShortTag) linTag).valueAsShort()); + case INT -> new IntTag("", ((LinIntTag) linTag).valueAsInt()); + case LONG -> new LongTag("", ((LinLongTag) linTag).valueAsLong()); + case FLOAT -> new FloatTag("", ((LinFloatTag) linTag).valueAsFloat()); + case DOUBLE -> new DoubleTag("", ((LinDoubleTag) linTag).valueAsDouble()); + case BYTE_ARRAY -> new ByteArrayTag("", ((LinByteArrayTag) linTag).value()); + case STRING -> new StringTag("", ((LinStringTag) linTag).value()); + case LIST -> { + LinListTag linList = (LinListTag) linTag; + ListTag nukkitList = new ListTag<>(); + for (LinTag item : linList.value()) { + Tag converted = toNukkitTag(item); + if (converted != null) { + nukkitList.add(converted); + } + } + yield nukkitList; + } + case COMPOUND -> toNukkitCompound((LinCompoundTag) linTag); + case INT_ARRAY -> new IntArrayTag("", ((LinIntArrayTag) linTag).value()); + case LONG_ARRAY -> { + // Nukkit doesn't have LongArrayTag; store as compound with metadata + long[] values = ((LinLongArrayTag) linTag).value(); + // Best effort: convert to int array if values fit, otherwise skip + int[] intValues = new int[values.length * 2]; + for (int i = 0; i < values.length; i++) { + intValues[i * 2] = (int) (values[i] >> 32); + intValues[i * 2 + 1] = (int) values[i]; + } + yield new IntArrayTag("", intValues); + } + default -> null; + }; + } + +} diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitPlatformAdapter.java b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitPlatformAdapter.java new file mode 100644 index 0000000000..9bf58ca20e --- /dev/null +++ b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitPlatformAdapter.java @@ -0,0 +1,16 @@ +package com.fastasyncworldedit.nukkitmot; + +import com.fastasyncworldedit.core.FAWEPlatformAdapterImpl; +import com.fastasyncworldedit.core.queue.IChunkGet; + +/** + * Nukkit platform adapter for FAWE chunk sending. + */ +public class NukkitPlatformAdapter implements FAWEPlatformAdapterImpl { + + @Override + public void sendChunk(IChunkGet chunk, int mask, boolean lighting) { + // Chunk sending is handled in NukkitGetBlocks.send() + } + +} diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitQueueHandler.java b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitQueueHandler.java new file mode 100644 index 0000000000..5a14e7be59 --- /dev/null +++ b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitQueueHandler.java @@ -0,0 +1,21 @@ +package com.fastasyncworldedit.nukkitmot; + +import com.fastasyncworldedit.core.queue.implementation.QueueHandler; + +/** + * Nukkit-specific queue handler. + * Nukkit has no AsyncCatcher or Timings system to manage, so these are no-ops. + */ +public class NukkitQueueHandler extends QueueHandler { + + @Override + public void startUnsafe(boolean parallel) { + // Nukkit has no AsyncCatcher or Timings to disable + } + + @Override + public void endUnsafe(boolean parallel) { + // Nukkit has no AsyncCatcher or Timings to re-enable + } + +} diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitRelighter.java b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitRelighter.java new file mode 100644 index 0000000000..0f157a7790 --- /dev/null +++ b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitRelighter.java @@ -0,0 +1,63 @@ +package com.fastasyncworldedit.nukkitmot; + +import com.fastasyncworldedit.core.extent.processor.lighting.Relighter; + +import java.util.concurrent.locks.ReentrantLock; + +/** + * No-op Relighter for Nukkit. Nukkit handles lighting internally, + * but we need a non-NullRelighter type to satisfy RelightProcessor's constructor check. + */ +public class NukkitRelighter implements Relighter { + + private final ReentrantLock lock = new ReentrantLock(); + + @Override + public boolean addChunk(int cx, int cz, byte[] skipReason, int bitmask) { + return false; + } + + @Override + public void addLightUpdate(int x, int y, int z) { + } + + @Override + public void fixLightingSafe(boolean sky) { + } + + @Override + public void clear() { + } + + @Override + public void removeLighting() { + } + + @Override + public void fixBlockLighting() { + } + + @Override + public void fixSkyLighting() { + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public ReentrantLock getLock() { + return lock; + } + + @Override + public boolean isFinished() { + return true; + } + + @Override + public void close() { + } + +} diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitTaskManager.java b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitTaskManager.java new file mode 100644 index 0000000000..7f21f4cc98 --- /dev/null +++ b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitTaskManager.java @@ -0,0 +1,60 @@ +package com.fastasyncworldedit.nukkitmot; + +import cn.nukkit.Server; +import cn.nukkit.plugin.Plugin; +import com.fastasyncworldedit.core.util.TaskManager; + +import javax.annotation.Nonnull; + +public class NukkitTaskManager extends TaskManager { + + private final Plugin plugin; + + public NukkitTaskManager(final Plugin plugin) { + this.plugin = plugin; + } + + @Override + public int repeat(@Nonnull final Runnable runnable, final int interval) { + return this.plugin.getServer().getScheduler() + .scheduleRepeatingTask(this.plugin, runnable, interval).getTaskId(); + } + + @Override + public int repeatAsync(@Nonnull final Runnable runnable, final int interval) { + return this.plugin.getServer().getScheduler() + .scheduleRepeatingTask(this.plugin, runnable, interval, true).getTaskId(); + } + + @Override + public void async(@Nonnull final Runnable runnable) { + this.plugin.getServer().getScheduler() + .scheduleTask(this.plugin, runnable, true); + } + + @Override + public void task(@Nonnull final Runnable runnable) { + this.plugin.getServer().getScheduler() + .scheduleTask(this.plugin, runnable); + } + + @Override + public void later(@Nonnull final Runnable runnable, final int delay) { + this.plugin.getServer().getScheduler() + .scheduleDelayedTask(this.plugin, runnable, delay); + } + + @Override + public void laterAsync(@Nonnull final Runnable runnable, final int delay) { + this.plugin.getServer().getScheduler() + .scheduleDelayedTask(this.plugin, runnable, delay, true); + } + + @Override + public void cancel(final int task) { + if (task != -1) { + Server.getInstance().getScheduler().cancelTask(task); + } + } + +} diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/BiomeMapping.java b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/BiomeMapping.java new file mode 100644 index 0000000000..3afbfd7a90 --- /dev/null +++ b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/BiomeMapping.java @@ -0,0 +1,90 @@ +package com.fastasyncworldedit.nukkitmot.mapping; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Maps JE biome identifiers to Nukkit biome IDs and vice versa. + */ +public final class BiomeMapping { + + private static final Logger LOGGER = LoggerFactory.getLogger(BiomeMapping.class); + private static final Gson GSON = new Gson(); + + private static final Map JE_TO_BE = new HashMap<>(); + private static final Map BE_TO_JE = new HashMap<>(); + + private BiomeMapping() { + } + + public static void init() { + try (InputStream stream = BiomeMapping.class.getClassLoader().getResourceAsStream("mapping/biomes.json")) { + if (stream == null) { + throw new RuntimeException("biomes.json not found"); + } + + Map mappings = GSON.fromJson( + new JsonReader(new InputStreamReader(Objects.requireNonNull(stream))), + new TypeToken>() { + }.getType() + ); + + mappings.forEach((javaId, entry) -> { + JE_TO_BE.put(javaId, entry.bedrockId()); + BE_TO_JE.put(entry.bedrockId(), javaId); + }); + + LOGGER.info("Loaded {} biome mappings", JE_TO_BE.size()); + } catch (IOException e) { + throw new RuntimeException("Failed to load biome mapping", e); + } + } + + /** + * Convert JE biome ID (e.g., "minecraft:plains") to Nukkit biome ID. + */ + public static int jeToBe(String jeBiomeId) { + Integer result = JE_TO_BE.get(jeBiomeId); + if (result == null) { + return 1; // plains as fallback + } + return result; + } + + /** + * Convert Nukkit biome ID to JE biome ID. + */ + public static String beToJe(int beBiomeId) { + String result = BE_TO_JE.get(beBiomeId); + if (result == null) { + return "minecraft:plains"; + } + return result; + } + + /** + * Get all JE biome identifiers for registry population. + */ + public static Collection getAllJeBiomes() { + return JE_TO_BE.keySet(); + } + + private record BiomeEntry( + @SerializedName("bedrock_id") + int bedrockId + ) { + } + +} diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/BlockMapping.java b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/BlockMapping.java new file mode 100644 index 0000000000..757ff7f9ed --- /dev/null +++ b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/BlockMapping.java @@ -0,0 +1,376 @@ +package com.fastasyncworldedit.nukkitmot.mapping; + +import cn.nukkit.block.Block; +import cn.nukkit.level.format.leveldb.BlockStateMapping; +import cn.nukkit.level.format.leveldb.NukkitLegacyMapper; +import cn.nukkit.level.format.leveldb.structure.BlockStateSnapshot; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.sk89q.worldedit.world.block.BlockState; +import com.sk89q.worldedit.world.block.BlockTypesCache; +import it.unimi.dsi.fastutil.ints.Int2CharOpenHashMap; +import org.cloudburstmc.nbt.NbtMap; +import org.cloudburstmc.nbt.NbtMapBuilder; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; + +/** + * Core mapping class for Java Edition block states to Nukkit block data (and vice versa). + *

+ * Uses GeyserMC's blocks.json for JE ↔ BE block state mapping and Nukkit's + * BlockStateMapping to convert BE identifier+state to legacy fullId. + */ +public final class BlockMapping { + + private static final Logger LOGGER = LoggerFactory.getLogger(BlockMapping.class); + + private static final Gson GSON = new GsonBuilder() + .registerTypeAdapterFactory(new IgnoreFailureTypeAdapterFactory()) + .create(); + + // JE hash → BE block data + private static final Map JE_HASH_TO_BE = new HashMap<>(); + // BE fullId → JE block state + private static final Map BE_FULL_ID_TO_JE = new HashMap<>(); + // JE block default properties + private static final Map> JE_BLOCK_DEFAULT_PROPERTIES = new HashMap<>(); + + // Ordinal mappings (built lazily after BlockTypesCache is initialized) + private static int[] jeOrdinalToBeFullId; + private static Int2CharOpenHashMap beFullIdToJeOrdinal; + + private static int blockStateVersion; + private static boolean initialized = false; + + private BlockMapping() { + } + + /** + * Initialize JE block default properties from je_blocks.json. + * Must be called before WorldEdit's loadMappings() to provide block data for BlockTypesCache. + */ + public static void initJeBlockDefaults() { + if (!JE_BLOCK_DEFAULT_PROPERTIES.isEmpty()) { + return; + } + if (!initJeBlockDefaultProperties()) { + throw new RuntimeException("Failed to load JE block default properties"); + } + } + + /** + * Initialize block state mappings from JSON resources. + * Must be called during plugin initialization, after {@link #initJeBlockDefaults()}. + */ + public static void init() { + if (initialized) { + return; + } + + initJeBlockDefaults(); + if (!initBlockStateMapping()) { + throw new RuntimeException("Failed to load block state mapping"); + } + + initialized = true; + } + + /** + * Build ordinal mappings after BlockTypesCache has been initialized. + * This must be called AFTER WorldEdit registries are set up. + */ + public static void buildOrdinalMappings() { + BlockState[] states = BlockTypesCache.states; + jeOrdinalToBeFullId = new int[states.length]; + beFullIdToJeOrdinal = new Int2CharOpenHashMap(states.length); + beFullIdToJeOrdinal.defaultReturnValue(Character.MAX_VALUE); + + int mapped = 0; + int unmapped = 0; + + for (int ordinal = 0; ordinal < states.length; ordinal++) { + BlockState state = states[ordinal]; + if (state == null) { + jeOrdinalToBeFullId[ordinal] = 0; // air + continue; + } + + // Build JE block state string from WorldEdit BlockState + String jeStateStr = state.getAsString(); + JeBlockState jeState = JeBlockState.fromString(jeStateStr); + jeState.completeMissingProperties(JE_BLOCK_DEFAULT_PROPERTIES.get(jeState.getIdentifier())); + + NukkitBlockData beData = JE_HASH_TO_BE.get(jeState.getHash()); + if (beData != null) { + int fullId = beData.getFullId(); + jeOrdinalToBeFullId[ordinal] = fullId; + beFullIdToJeOrdinal.putIfAbsent(fullId, (char) ordinal); + mapped++; + } else { + jeOrdinalToBeFullId[ordinal] = 0; // air as fallback + unmapped++; + } + } + + LOGGER.info("Ordinal mappings built: {} mapped, {} unmapped out of {} total states", + mapped, unmapped, states.length); + } + + /** + * Convert JE block state to BE block data. + */ + public static NukkitBlockData jeToBe(JeBlockState state) { + NukkitBlockData result = JE_HASH_TO_BE.get(state.getHash()); + if (result == null) { + return NukkitBlockData.AIR; + } + return result; + } + + /** + * Convert BE fullId to JE block state. + */ + @Nullable + public static JeBlockState beToJe(int fullId) { + return BE_FULL_ID_TO_JE.get(fullId); + } + + /** + * Hot path: convert JE ordinal to BE fullId. + */ + public static int jeOrdinalToFullId(char ordinal) { + return jeOrdinalToBeFullId[ordinal]; + } + + /** + * Hot path: convert BE fullId to JE ordinal. + * Returns Character.MAX_VALUE if no mapping exists. + */ + public static char fullIdToJeOrdinal(int fullId) { + return beFullIdToJeOrdinal.get(fullId); + } + + /** + * Get JE block default properties for a given identifier. + */ + public static Map getJeBlockDefaultProperties(String identifier) { + Map props = JE_BLOCK_DEFAULT_PROPERTIES.get(identifier); + return props != null ? props : Map.of(); + } + + /** + * Get all JE block default states as string representations. + * Must be called after {@link #initJeBlockDefaults()}. + * + * @return collection of default block state strings, e.g. "minecraft:stone" or + * "minecraft:oak_stairs[facing=north,half=bottom,shape=straight,waterlogged=false]" + */ + public static Collection getAllJeBlockDefaultStates() { + List result = new ArrayList<>(JE_BLOCK_DEFAULT_PROPERTIES.size()); + for (Map.Entry> entry : JE_BLOCK_DEFAULT_PROPERTIES.entrySet()) { + String id = entry.getKey(); + Map properties = entry.getValue(); + if (properties == null || properties.isEmpty()) { + result.add(id); + } else { + StringBuilder sb = new StringBuilder(id).append("["); + boolean first = true; + for (Map.Entry prop : properties.entrySet()) { + if (!first) { + sb.append(","); + } + sb.append(prop.getKey()).append("=").append(prop.getValue()); + first = false; + } + sb.append("]"); + result.add(sb.toString()); + } + } + return result; + } + + @SuppressWarnings("unchecked") + private static boolean initJeBlockDefaultProperties() { + try (InputStream stream = BlockMapping.class.getClassLoader().getResourceAsStream("je_blocks.json")) { + if (stream == null) { + LOGGER.error("je_blocks.json not found"); + return false; + } + + Map>> data = from(stream, new TypeToken<>() { + }); + for (var entry : data.entrySet()) { + JE_BLOCK_DEFAULT_PROPERTIES.put( + "minecraft:" + entry.getKey(), + (Map) entry.getValue().get(1) + ); + } + LOGGER.info("Loaded {} JE block default properties", JE_BLOCK_DEFAULT_PROPERTIES.size()); + } catch (IOException e) { + LOGGER.error("Failed to load je_blocks.json", e); + return false; + } + return true; + } + + private static boolean initBlockStateMapping() { + // Extract block state version from palette + List palette = NukkitLegacyMapper.loadBlockPalette(); + if (!palette.isEmpty()) { + blockStateVersion = palette.getFirst().getInt("version"); + LOGGER.info("Block state version: {}", blockStateVersion); + } + + try (InputStream stream = BlockMapping.class.getClassLoader().getResourceAsStream("mapping/blocks.json")) { + if (stream == null) { + LOGGER.error("blocks.json not found"); + return false; + } + + Map> root = from(stream, new TypeToken<>() { + }); + List mappings = root.get("mappings"); + int mapped = 0; + int failed = 0; + + for (BlockMappingEntry mapping : mappings) { + JeBlockState jeState = createJeBlockState(mapping.javaState()); + NukkitBlockData beData = createNukkitBlockData(mapping.bedrockState()); + if (beData != null) { + JE_HASH_TO_BE.put(jeState.getHash(), beData); + BE_FULL_ID_TO_JE.put(beData.getFullId(), jeState); + mapped++; + } else { + failed++; + } + } + LOGGER.info("Block state mapping loaded: {} mapped, {} failed", mapped, failed); + } catch (IOException e) { + LOGGER.error("Failed to load blocks.json", e); + return false; + } + return true; + } + + private static JeBlockState createJeBlockState(BlockMappingEntry.JavaState state) { + Map properties = state.properties() == null ? Map.of() : state.properties(); + JeBlockState jeState = JeBlockState.create(state.name(), new TreeMap<>(properties)); + jeState.completeMissingProperties(JE_BLOCK_DEFAULT_PROPERTIES.get(state.name())); + return jeState; + } + + @Nullable + private static NukkitBlockData createNukkitBlockData(BlockMappingEntry.BedrockState state) { + try { + String beName = "minecraft:" + state.bedrockId(); + NbtMapBuilder statesBuilder = NbtMap.builder(); + if (state.state() != null) { + for (Map.Entry entry : state.state().entrySet()) { + Object value = entry.getValue(); + if (value instanceof Number number) { + statesBuilder.putInt(entry.getKey(), number.intValue()); + } else if (value instanceof Boolean bool) { + statesBuilder.putByte(entry.getKey(), (byte) (bool ? 1 : 0)); + } else { + statesBuilder.putString(entry.getKey(), value.toString()); + } + } + } + + NbtMap nbtState = NbtMap.builder() + .putString("name", beName) + .putCompound("states", statesBuilder.build()) + .putInt("version", blockStateVersion) + .build(); + + BlockStateSnapshot snapshot = BlockStateMapping.get().getStateUnsafe(nbtState); + if (snapshot == null) { + return null; + } + + int legacyId = snapshot.getLegacyId(); + int legacyData = snapshot.getLegacyData(); + if (legacyId == -1) { + return null; + } + return new NukkitBlockData(legacyId, legacyData); + } catch (Exception e) { + return null; + } + } + + private static V from(InputStream inputStream, TypeToken typeToken) { + JsonReader reader = new JsonReader(new InputStreamReader(Objects.requireNonNull(inputStream))); + return GSON.fromJson(reader, typeToken.getType()); + } + + // JSON model records + + public record BlockMappingEntry( + @SerializedName("java_state") + JavaState javaState, + @SerializedName("bedrock_state") + BedrockState bedrockState + ) { + public record JavaState( + @SerializedName("Name") + String name, + @Nullable + @SerializedName("Properties") + Map properties + ) { + } + + public record BedrockState( + @SerializedName("bedrock_identifier") + String bedrockId, + @Nullable + Map state + ) { + } + } + + public static class IgnoreFailureTypeAdapterFactory implements TypeAdapterFactory { + + @Override + public TypeAdapter create(Gson gson, TypeToken typeToken) { + TypeAdapter delegate = gson.getDelegateAdapter(this, typeToken); + return new TypeAdapter<>() { + @Override + public void write(JsonWriter writer, T value) throws IOException { + delegate.write(writer, value); + } + + @Override + public T read(JsonReader reader) throws IOException { + try { + return delegate.read(reader); + } catch (Exception e) { + reader.skipValue(); + return null; + } + } + }; + } + + } + +} diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/ItemMapping.java b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/ItemMapping.java new file mode 100644 index 0000000000..7bfb8867a3 --- /dev/null +++ b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/ItemMapping.java @@ -0,0 +1,131 @@ +package com.fastasyncworldedit.nukkitmot.mapping; + +import cn.nukkit.item.Item; +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Maps JE item identifiers to Nukkit item IDs and vice versa. + */ +public final class ItemMapping { + + private static final Logger LOGGER = LoggerFactory.getLogger(ItemMapping.class); + private static final Gson GSON = new Gson(); + + private static final Map JE_TO_BE = new HashMap<>(); + private static final Map BE_TO_JE = new HashMap<>(); + // All known JE item IDs from items.json (loaded early for ItemTypesCache) + private static final Set JE_ITEM_IDS = new HashSet<>(); + + private ItemMapping() { + } + + /** + * Pre-load JE item IDs from items.json. + * Must be called before WorldEdit's loadMappings() to provide item data for ItemTypesCache. + */ + public static void initJeItemIds() { + if (!JE_ITEM_IDS.isEmpty()) { + return; + } + try (InputStream stream = ItemMapping.class.getClassLoader().getResourceAsStream("mapping/items.json")) { + if (stream == null) { + throw new RuntimeException("items.json not found"); + } + + Map mappings = GSON.fromJson( + new JsonReader(new InputStreamReader(Objects.requireNonNull(stream))), + new TypeToken>() { + }.getType() + ); + + JE_ITEM_IDS.addAll(mappings.keySet()); + LOGGER.info("Loaded {} JE item IDs", JE_ITEM_IDS.size()); + } catch (IOException e) { + throw new RuntimeException("Failed to load item IDs", e); + } + } + + /** + * Get all known JE item IDs. + * Must be called after {@link #initJeItemIds()}. + */ + public static Collection getAllJeItemIds() { + return JE_ITEM_IDS; + } + + public static void init() { + try (InputStream stream = ItemMapping.class.getClassLoader().getResourceAsStream("mapping/items.json")) { + if (stream == null) { + throw new RuntimeException("items.json not found"); + } + + Map mappings = GSON.fromJson( + new JsonReader(new InputStreamReader(Objects.requireNonNull(stream))), + new TypeToken>() { + }.getType() + ); + + mappings.forEach((javaId, entry) -> { + Item nukkitItem = Item.fromString(entry.bedrockId()); + if (nukkitItem != null) { + NukkitItemData data = new NukkitItemData(nukkitItem.getId(), entry.bedrockData()); + JE_TO_BE.put(javaId, data); + BE_TO_JE.putIfAbsent(nukkitItem.getId(), javaId); + } + }); + + LOGGER.info("Loaded {} item mappings", JE_TO_BE.size()); + } catch (IOException e) { + throw new RuntimeException("Failed to load item mapping", e); + } + } + + /** + * Convert JE item ID (e.g., "minecraft:stone") to Nukkit item data. + */ + public static NukkitItemData jeToBe(String jeItemId) { + NukkitItemData result = JE_TO_BE.get(jeItemId); + if (result == null) { + return new NukkitItemData(0, 0); + } + return result; + } + + /** + * Convert Nukkit item ID to JE item ID. + */ + public static String beToJe(int beItemId) { + String result = BE_TO_JE.get(beItemId); + if (result == null) { + return "minecraft:air"; + } + return result; + } + + public record NukkitItemData(int itemId, int metadata) { + } + + private record ItemEntry( + @SerializedName("bedrock_identifier") + String bedrockId, + @SerializedName("bedrock_data") + int bedrockData + ) { + } + +} diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/JeBlockState.java b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/JeBlockState.java new file mode 100644 index 0000000000..aea467cb4c --- /dev/null +++ b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/JeBlockState.java @@ -0,0 +1,125 @@ +package com.fastasyncworldedit.nukkitmot.mapping; + +import java.util.Map; +import java.util.TreeMap; + +/** + * Represents a Java Edition block state, consisting of an identifier and properties. + * Uses FNV-1a 32-bit hash for fast lookup in mapping tables. + */ +public class JeBlockState { + + private final String identifier; + private final TreeMap properties; + private int hash = Integer.MAX_VALUE; + + private JeBlockState(String data) { + int braceIndex = data.indexOf('{'); + if (braceIndex != -1) { + data = data.substring(0, braceIndex); + } + + String[] strings = data + .replace("[", ",") + .replace("]", ",") + .replace(" ", "") + .split(","); + + this.identifier = strings[0]; + this.properties = new TreeMap<>(); + if (strings.length > 1) { + for (int i = 1; i < strings.length; i++) { + final String tmp = strings[i]; + if (tmp.isEmpty()) { + continue; + } + final int index = tmp.indexOf("="); + properties.put(tmp.substring(0, index), tmp.substring(index + 1)); + } + } + } + + private JeBlockState(String identifier, TreeMap properties) { + this.identifier = identifier; + this.properties = properties; + } + + public static JeBlockState fromString(String data) { + return new JeBlockState(data); + } + + public static JeBlockState create(String identifier, TreeMap properties) { + return new JeBlockState(identifier, properties); + } + + public String getIdentifier() { + return identifier; + } + + public String getPropertyValue(String key) { + return properties.get(key); + } + + public TreeMap getProperties() { + return properties; + } + + /** + * Complete missing properties using the provided default properties. + */ + public void completeMissingProperties(Map defaultProperties) { + if (defaultProperties == null || properties.size() == defaultProperties.size()) { + return; + } + for (Map.Entry entry : defaultProperties.entrySet()) { + properties.putIfAbsent(entry.getKey(), entry.getValue()); + } + } + + /** + * Build the canonical string representation: "identifier;key=value;key=value;..." + * Properties are sorted by key (TreeMap guarantees this). + */ + public String toCanonicalString() { + StringBuilder builder = new StringBuilder(identifier).append(";"); + properties.forEach((k, v) -> builder.append(k).append("=").append(v).append(";")); + return builder.toString(); + } + + /** + * Get FNV-1a 32-bit hash of the canonical string representation. + */ + public int getHash() { + if (hash == Integer.MAX_VALUE) { + hash = fnv1a32(toCanonicalString().getBytes()); + } + return hash; + } + + @Override + public String toString() { + if (properties.isEmpty()) { + return identifier; + } + StringBuilder sb = new StringBuilder(identifier).append("["); + boolean first = true; + for (Map.Entry entry : properties.entrySet()) { + if (!first) { + sb.append(","); + } + sb.append(entry.getKey()).append("=").append(entry.getValue()); + first = false; + } + return sb.append("]").toString(); + } + + private static int fnv1a32(byte[] data) { + int hash = 0x811c9dc5; + for (byte b : data) { + hash ^= (b & 0xff); + hash *= 0x01000193; + } + return hash; + } + +} diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/NukkitBlockData.java b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/NukkitBlockData.java new file mode 100644 index 0000000000..e2e042a5bd --- /dev/null +++ b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/NukkitBlockData.java @@ -0,0 +1,19 @@ +package com.fastasyncworldedit.nukkitmot.mapping; + +import cn.nukkit.block.Block; + +/** + * Represents a Nukkit block as its legacy ID and metadata. + */ +public record NukkitBlockData(int blockId, int metadata) { + + public static final NukkitBlockData AIR = new NukkitBlockData(0, 0); + + /** + * Get the Nukkit full block ID: (blockId << DATA_BITS) | metadata + */ + public int getFullId() { + return (blockId << Block.DATA_BITS) | metadata; + } + +} diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitAdapter.java b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitAdapter.java new file mode 100644 index 0000000000..1256fab043 --- /dev/null +++ b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitAdapter.java @@ -0,0 +1,148 @@ +package com.sk89q.worldedit.nukkitmot; + +import cn.nukkit.Player; +import cn.nukkit.item.Item; +import cn.nukkit.level.Level; +import cn.nukkit.math.Vector3; +import com.fastasyncworldedit.nukkitmot.mapping.BlockMapping; +import com.fastasyncworldedit.nukkitmot.mapping.ItemMapping; +import com.sk89q.worldedit.blocks.BaseItemStack; +import com.sk89q.worldedit.extent.Extent; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.util.Location; +import com.sk89q.worldedit.world.block.BlockState; +import com.sk89q.worldedit.world.block.BlockTypes; +import com.sk89q.worldedit.world.item.ItemType; +import com.sk89q.worldedit.world.item.ItemTypes; + +import java.util.Collections; +import java.util.Map; +import java.util.WeakHashMap; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Static adapter utility class for converting between Nukkit and WorldEdit types. + */ +public final class NukkitAdapter { + + private static final Map worldCache = Collections.synchronizedMap(new WeakHashMap<>()); + private static final Map playerCache = Collections.synchronizedMap(new WeakHashMap<>()); + + private NukkitAdapter() { + } + + public static NukkitWorld adapt(Level level) { + return worldCache.computeIfAbsent(level, NukkitWorld::new); + } + + public static NukkitPlayer adapt(Player player) { + return playerCache.computeIfAbsent(player, NukkitPlayer::new); + } + + /** + * Remove a player from the cache. Should be called on player quit. + */ + static void uncachePlayer(Player player) { + playerCache.remove(player); + } + + /** + * Convert a WorldEdit World/Extent to a Nukkit Level. + */ + public static Level adapt(Extent world) { + checkNotNull(world); + if (world instanceof NukkitWorld nukkitWorld) { + return nukkitWorld.getLevel(); + } + throw new IllegalArgumentException("Extent is not a NukkitWorld: " + world.getClass().getName()); + } + + public static Vector3 adapt(BlockVector3 position) { + return new Vector3(position.x(), position.y(), position.z()); + } + + public static Vector3 adapt(com.sk89q.worldedit.math.Vector3 position) { + return new Vector3(position.x(), position.y(), position.z()); + } + + public static BlockVector3 adapt(Vector3 position) { + return BlockVector3.at(position.getFloorX(), position.getFloorY(), position.getFloorZ()); + } + + /** + * Convert a WorldEdit Location to a Nukkit Location. + */ + public static cn.nukkit.level.Location adapt(Location location) { + checkNotNull(location); + return new cn.nukkit.level.Location( + location.x(), location.y(), location.z(), + location.getYaw(), location.getPitch(), + adapt(location.getExtent()) + ); + } + + /** + * Convert a Nukkit Location to a WorldEdit Location. + */ + public static Location adapt(cn.nukkit.level.Location location) { + checkNotNull(location); + return new Location( + adapt(location.getLevel()), + location.x, location.y, location.z, + (float) location.yaw, (float) location.pitch + ); + } + + /** + * Convert a Nukkit Item to a WorldEdit ItemType. + */ + public static ItemType adapt(Item item) { + String jeId = ItemMapping.beToJe(item.getId()); + ItemType type = ItemTypes.get(jeId); + return type != null ? type : ItemTypes.AIR; + } + + /** + * Convert a Nukkit Item to a WorldEdit BaseItemStack (with count). + */ + public static BaseItemStack adaptItemStack(Item item) { + return new BaseItemStack(adapt(item), item.getCount()); + } + + /** + * Convert a WorldEdit ItemType to a Nukkit Item. + */ + public static Item adapt(ItemType itemType) { + ItemMapping.NukkitItemData data = ItemMapping.jeToBe(itemType.id()); + return Item.get(data.itemId(), data.metadata()); + } + + /** + * Convert a WorldEdit BaseItemStack to a Nukkit Item (with count). + */ + public static Item adaptItem(BaseItemStack itemStack) { + ItemMapping.NukkitItemData data = ItemMapping.jeToBe(itemStack.getType().id()); + return Item.get(data.itemId(), data.metadata(), itemStack.getAmount()); + } + + /** + * Convert a Nukkit block fullId to a WorldEdit BlockState. + */ + public static BlockState adaptBlockState(int fullId) { + char ordinal = BlockMapping.fullIdToJeOrdinal(fullId); + if (ordinal == Character.MAX_VALUE) { + return BlockTypes.AIR.getDefaultState(); + } + BlockState state = com.sk89q.worldedit.world.block.BlockTypesCache.states[ordinal]; + return state != null ? state : BlockTypes.AIR.getDefaultState(); + } + + /** + * Convert a WorldEdit BlockState to a Nukkit fullId. + */ + public static int adaptFullId(BlockState state) { + return BlockMapping.jeOrdinalToFullId(state.getOrdinalChar()); + } + +} diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitBlockRegistry.java b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitBlockRegistry.java new file mode 100644 index 0000000000..5a5637b554 --- /dev/null +++ b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitBlockRegistry.java @@ -0,0 +1,28 @@ +package com.sk89q.worldedit.nukkitmot; + +import com.fastasyncworldedit.nukkitmot.mapping.BlockMapping; +import com.sk89q.worldedit.world.block.BlockType; +import com.sk89q.worldedit.world.registry.BlockMaterial; +import com.sk89q.worldedit.world.registry.BundledBlockRegistry; + +import java.util.Collection; +import javax.annotation.Nullable; + +/** + * Nukkit block registry that extends the bundled registry with Nukkit-specific material data. + */ +class NukkitBlockRegistry extends BundledBlockRegistry { + + @Nullable + @Override + public BlockMaterial getMaterial(BlockType blockType) { + // Use bundled data; Nukkit-specific material is provided by NukkitBlockMaterial when needed + return super.getMaterial(blockType); + } + + @Override + public Collection values() { + return BlockMapping.getAllJeBlockDefaultStates(); + } + +} diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitCommandSender.java b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitCommandSender.java new file mode 100644 index 0000000000..0636b9b8d2 --- /dev/null +++ b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitCommandSender.java @@ -0,0 +1,122 @@ +package com.sk89q.worldedit.nukkitmot; + +import cn.nukkit.command.CommandSender; +import com.sk89q.worldedit.extension.platform.AbstractNonPlayerActor; +import com.sk89q.worldedit.session.SessionKey; +import com.sk89q.worldedit.util.Location; +import com.sk89q.worldedit.util.auth.AuthorizationException; +import com.sk89q.worldedit.util.formatting.text.Component; +import com.sk89q.worldedit.util.formatting.text.TextComponent; +import com.sk89q.worldedit.util.formatting.text.serializer.plain.PlainComponentSerializer; + +import javax.annotation.Nullable; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import java.util.UUID; + +/** + * Adapter for Nukkit CommandSender (console) to WorldEdit Actor. + */ +public class NukkitCommandSender extends AbstractNonPlayerActor { + + private static final UUID CONSOLE_UUID = UUID.nameUUIDFromBytes("CONSOLE".getBytes(StandardCharsets.UTF_8)); + + private final CommandSender sender; + + public NukkitCommandSender(CommandSender sender) { + this.sender = sender; + } + + @Override + public UUID getUniqueId() { + return CONSOLE_UUID; + } + + @Override + public String getName() { + return sender.getName(); + } + + @Deprecated + @Override + public void printRaw(String msg) { + sender.sendMessage(msg); + } + + @Deprecated + @Override + public void print(String msg) { + sender.sendMessage(msg); + } + + @Deprecated + @Override + public void printDebug(String msg) { + sender.sendMessage(msg); + } + + @Deprecated + @Override + public void printError(String msg) { + sender.sendMessage("Error: " + msg); + } + + @Override + public void print(Component component) { + sender.sendMessage(PlainComponentSerializer.INSTANCE.serialize(component)); + } + + @Override + public Locale getLocale() { + return Locale.getDefault(); + } + + @Override + public boolean hasPermission(String permission) { + return sender.hasPermission(permission); + } + + @Override + public void checkPermission(String permission) throws AuthorizationException { + if (!hasPermission(permission)) { + throw new AuthorizationException(); + } + } + + @Override + public void setPermission(String permission, boolean value) { + // Nukkit console permissions are not modifiable at runtime + } + + @Override + public String[] getGroups() { + return new String[0]; + } + + @Override + public SessionKey getSessionKey() { + return new SessionKey() { + @Override + public String getName() { + return sender.getName(); + } + + @Override + public boolean isActive() { + return true; + } + + @Override + public boolean isPersistent() { + return true; + } + + @Nullable + @Override + public UUID getUniqueId() { + return CONSOLE_UUID; + } + }; + } + +} diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitConfiguration.java b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitConfiguration.java new file mode 100644 index 0000000000..f46e3f32f2 --- /dev/null +++ b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitConfiguration.java @@ -0,0 +1,17 @@ +package com.sk89q.worldedit.nukkitmot; + +import cn.nukkit.plugin.Plugin; +import com.sk89q.worldedit.util.PropertiesConfiguration; + +import java.io.File; + +/** + * Nukkit platform configuration using properties file. + */ +public class NukkitConfiguration extends PropertiesConfiguration { + + public NukkitConfiguration(Plugin plugin) { + super(new File(plugin.getDataFolder(), "worldedit.properties").toPath()); + } + +} diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitEntity.java b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitEntity.java new file mode 100644 index 0000000000..2726c1ac26 --- /dev/null +++ b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitEntity.java @@ -0,0 +1,126 @@ +package com.sk89q.worldedit.nukkitmot; + +import cn.nukkit.utils.Identifier; +import com.fastasyncworldedit.nukkitmot.NukkitNbtConverter; +import com.sk89q.worldedit.entity.BaseEntity; +import com.sk89q.worldedit.entity.Entity; +import com.sk89q.worldedit.extent.Extent; +import com.sk89q.worldedit.util.Location; +import com.sk89q.worldedit.util.concurrency.LazyReference; +import com.sk89q.worldedit.world.NullWorld; +import com.sk89q.worldedit.world.entity.EntityType; +import com.sk89q.worldedit.world.entity.EntityTypes; +import org.enginehub.linbus.tree.LinCompoundTag; + +import javax.annotation.Nullable; +import java.lang.ref.WeakReference; + +/** + * Adapts a Nukkit entity to a WorldEdit entity. + */ +public class NukkitEntity implements Entity { + + private final WeakReference entityRef; + + public NukkitEntity(cn.nukkit.entity.Entity entity) { + this.entityRef = new WeakReference<>(entity); + } + + @Override + public Extent getExtent() { + cn.nukkit.entity.Entity entity = entityRef.get(); + if (entity != null) { + return NukkitAdapter.adapt(entity.getLevel()); + } + return NullWorld.getInstance(); + } + + @Override + public Location getLocation() { + cn.nukkit.entity.Entity entity = entityRef.get(); + if (entity != null) { + return new Location( + NukkitAdapter.adapt(entity.getLevel()), + entity.x, entity.y, entity.z, + (float) entity.yaw, (float) entity.pitch + ); + } + return new Location(NullWorld.getInstance()); + } + + @Override + public boolean setLocation(Location location) { + cn.nukkit.entity.Entity entity = entityRef.get(); + if (entity != null) { + entity.teleport(NukkitAdapter.adapt(location)); + return true; + } + return false; + } + + @Nullable + @Override + public BaseEntity getState() { + cn.nukkit.entity.Entity entity = entityRef.get(); + if (entity == null || entity instanceof cn.nukkit.Player) { + return null; + } + + Identifier identifier = entity.getIdentifier(); + if (identifier == null) { + return null; + } + + EntityType type = EntityTypes.get(identifier.toString()); + if (type == null) { + return null; + } + + cn.nukkit.nbt.tag.CompoundTag namedTag = entity.namedTag; + if (namedTag != null) { + return new BaseEntity(type, LazyReference.computed( + NukkitNbtConverter.toLinCompound(namedTag) + )); + } + return new BaseEntity(type); + } + + @Override + public boolean remove() { + cn.nukkit.entity.Entity entity = entityRef.get(); + if (entity != null) { + entity.close(); + return true; + } + return false; + } + + @Nullable + @Override + public T getFacet(Class cls) { + return null; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof NukkitEntity that)) { + return false; + } + cn.nukkit.entity.Entity self = this.entityRef.get(); + cn.nukkit.entity.Entity otherEntity = that.entityRef.get(); + if (self == null || otherEntity == null) { + return false; + } + return self.getId() == otherEntity.getId(); + } + + @Override + public int hashCode() { + cn.nukkit.entity.Entity entity = entityRef.get(); + return entity != null ? Long.hashCode(entity.getId()) : 0; + } + +} diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitGetBlocks.java b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitGetBlocks.java new file mode 100644 index 0000000000..b3997b116a --- /dev/null +++ b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitGetBlocks.java @@ -0,0 +1,426 @@ +package com.sk89q.worldedit.nukkitmot; + +import cn.nukkit.block.Block; +import cn.nukkit.blockentity.BlockEntity; +import cn.nukkit.level.Level; +import cn.nukkit.level.format.ChunkSection; +import cn.nukkit.level.format.generic.BaseFullChunk; +import cn.nukkit.nbt.tag.CompoundTag; +import com.fastasyncworldedit.core.extent.processor.heightmap.HeightMapType; +import com.fastasyncworldedit.core.nbt.FaweCompoundTag; +import com.fastasyncworldedit.core.queue.IChunk; +import com.fastasyncworldedit.core.queue.IChunkGet; +import com.fastasyncworldedit.core.queue.IChunkSet; +import com.fastasyncworldedit.core.queue.IQueueExtent; +import com.fastasyncworldedit.core.queue.implementation.blocks.CharGetBlocks; +import com.fastasyncworldedit.core.registry.state.PropertyKey; +import com.fastasyncworldedit.nukkitmot.NukkitNbtConverter; +import com.fastasyncworldedit.nukkitmot.mapping.BiomeMapping; +import com.fastasyncworldedit.nukkitmot.mapping.BlockMapping; +import com.sk89q.worldedit.entity.Entity; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.world.biome.BiomeType; +import com.sk89q.worldedit.world.biome.BiomeTypes; +import com.sk89q.worldedit.world.block.BlockState; +import com.sk89q.worldedit.world.block.BlockType; +import com.sk89q.worldedit.world.block.BlockTypesCache; + +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Future; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Nukkit chunk data access for FAWE's async editing system. + */ +public class NukkitGetBlocks extends CharGetBlocks { + + private static final int MIN_Y = -64; + private static final int MAX_Y = 319; + private static final int WATER_ID = 8; + private static final int STILL_WATER_ID = 9; + + private final Level level; + private final int chunkX; + private final int chunkZ; + private final ReentrantLock callLock = new ReentrantLock(); + private final ConcurrentHashMap copies = new ConcurrentHashMap<>(); + private boolean createCopy = false; + private int copyKey = 0; + + public NukkitGetBlocks(Level level, int chunkX, int chunkZ) { + super(MIN_Y >> 4, MAX_Y >> 4); + this.level = level; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + } + + @Override + public int getX() { + return chunkX; + } + + @Override + public int getZ() { + return chunkZ; + } + + @Override + public BiomeType getBiomeType(int x, int y, int z) { + BaseFullChunk chunk = level.getChunk(chunkX, chunkZ, true); + if (chunk == null) { + return BiomeTypes.PLAINS; + } + int biomeId = chunk.getBiomeId(x & 0xF, z & 0xF); + String jeBiome = BiomeMapping.beToJe(biomeId); + BiomeType type = BiomeTypes.get(jeBiome); + return type != null ? type : BiomeTypes.PLAINS; + } + + @Override + public BlockState getBlock(int x, int y, int z) { + BaseFullChunk chunk = level.getChunk(chunkX, chunkZ, true); + if (chunk == null) { + return BlockTypesCache.states[BlockTypesCache.ReservedIDs.AIR]; + } + int fullId = chunk.getFullBlock(x & 0xF, y, z & 0xF); + char ordinal = BlockMapping.fullIdToJeOrdinal(fullId); + if (ordinal == Character.MAX_VALUE) { + return BlockTypesCache.states[BlockTypesCache.ReservedIDs.AIR]; + } + BlockState state = BlockTypesCache.states[ordinal]; + // Check layer 1 for waterlogged + if (state.getBlockType().hasProperty(PropertyKey.WATERLOGGED)) { + int layer1Id = chunk.getBlockId(x & 0xF, y, z & 0xF, 1); + if (layer1Id == WATER_ID || layer1Id == STILL_WATER_ID) { + state = state.with(PropertyKey.WATERLOGGED, true); + } + } + return state; + } + + @Override + public char[] update(int layer, char[] data, boolean aggressive) { + BaseFullChunk chunk = level.getChunk(chunkX, chunkZ, true); + if (chunk == null) { + return data; + } + if (data == null) { + data = new char[4096]; + } + + int baseY = layer << 4; + for (int y = 0; y < 16; y++) { + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + int index = (y << 8) | (z << 4) | x; + int fullId = chunk.getFullBlock(x, baseY + y, z); + char ordinal = BlockMapping.fullIdToJeOrdinal(fullId); + if (ordinal == Character.MAX_VALUE) { + data[index] = BlockTypesCache.ReservedIDs.AIR; + } else { + // Apply waterlogged state from layer 1 + BlockState state = BlockTypesCache.states[ordinal]; + if (state != null && state.getBlockType().hasProperty(PropertyKey.WATERLOGGED)) { + int layer1Id = chunk.getBlockId(x, baseY + y, z, 1); + if (layer1Id == WATER_ID || layer1Id == STILL_WATER_ID) { + state = state.with(PropertyKey.WATERLOGGED, true); + ordinal = state.getOrdinalChar(); + } + } + data[index] = ordinal; + } + } + } + } + return data; + } + + @Override + @SuppressWarnings({"rawtypes", "unchecked"}) + public > T call(IQueueExtent owner, IChunkSet set, Runnable finalizer) { + BaseFullChunk chunk = level.getChunk(chunkX, chunkZ, true); + if (chunk == null) { + if (finalizer != null) { + finalizer.run(); + } + return (T) (Future) CompletableFuture.completedFuture(null); + } + + // Create snapshot copy for undo if requested + NukkitGetBlocks_Copy copy = null; + if (createCopy) { + copy = new NukkitGetBlocks_Copy(chunkX, chunkZ); + for (int layer = set.getMinSectionPosition(); layer <= set.getMaxSectionPosition(); layer++) { + if (!set.hasSection(layer)) { + continue; + } + // Store current blocks before modification + int baseY = layer << 4; + char[] sectionData = new char[4096]; + for (int y = 0; y < 16; y++) { + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + int index = (y << 8) | (z << 4) | x; + int fullId = chunk.getFullBlock(x, baseY + y, z); + char ordinal = BlockMapping.fullIdToJeOrdinal(fullId); + sectionData[index] = (ordinal == Character.MAX_VALUE) + ? BlockTypesCache.ReservedIDs.AIR : ordinal; + } + } + } + copy.storeSection(layer, sectionData); + } + // Store existing block entities in affected area + for (BlockEntity be : chunk.getBlockEntities().values()) { + int beY = be.getFloorY(); + int layer = beY >> 4; + if (layer >= set.getMinSectionPosition() && layer <= set.getMaxSectionPosition() + && set.hasSection(layer)) { + BlockVector3 pos = BlockVector3.at(be.getFloorX(), beY, be.getFloorZ()); + copy.storeTile(pos, NukkitNbtConverter.toFawe(be.namedTag)); + } + } + } + + // Apply block changes + for (int layer = set.getMinSectionPosition(); layer <= set.getMaxSectionPosition(); layer++) { + if (!set.hasSection(layer)) { + continue; + } + char[] setBlocks = set.loadIfPresent(layer); + if (setBlocks == null) { + continue; + } + int baseY = layer << 4; + for (int y = 0; y < 16; y++) { + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + int index = (y << 8) | (z << 4) | x; + char ordinal = setBlocks[index]; + if (ordinal == BlockTypesCache.ReservedIDs.__RESERVED__) { + continue; + } + // Check for waterlogged and handle layer 1 + BlockState state = BlockTypesCache.states[ordinal]; + boolean waterlogged = false; + if (state != null && state.getBlockType().hasProperty(PropertyKey.WATERLOGGED)) { + Object wl = state.getState(PropertyKey.WATERLOGGED); + if (wl == Boolean.TRUE) { + waterlogged = true; + state = state.with(PropertyKey.WATERLOGGED, false); + ordinal = state.getOrdinalChar(); + } + } + int fullId = BlockMapping.jeOrdinalToFullId(ordinal); + int blockId = fullId >> Block.DATA_BITS; + int meta = fullId & Block.DATA_MASK; + chunk.setFullBlockId(x, baseY + y, z, 0, + (blockId << Block.DATA_BITS) | meta); + // Set or clear layer 1 water + if (waterlogged) { + chunk.setFullBlockId(x, baseY + y, z, 1, + STILL_WATER_ID << Block.DATA_BITS); + } else if (state != null && state.getBlockType().hasProperty(PropertyKey.WATERLOGGED)) { + // Clear water from layer 1 if block supports waterlogged but isn't + chunk.setFullBlockId(x, baseY + y, z, 1, 0); + } + } + } + } + } + + // Apply biome changes + if (set.hasBiomes()) { + for (int y = set.getMinSectionPosition() << 4; y <= (set.getMaxSectionPosition() << 4) + 15; y++) { + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + BiomeType biome = set.getBiomeType(x, y, z); + if (biome != null) { + int beBiomeId = BiomeMapping.jeToBe(biome.id()); + chunk.setBiomeId(x, z, (byte) beBiomeId); + } + } + } + } + } + + // Apply block entity changes + Map setTiles = set.tiles(); + if (!setTiles.isEmpty()) { + for (Map.Entry entry : setTiles.entrySet()) { + BlockVector3 pos = entry.getKey(); + CompoundTag nbt = NukkitNbtConverter.toNukkit(entry.getValue()); + nbt.putInt("x", pos.x()); + nbt.putInt("y", pos.y()); + nbt.putInt("z", pos.z()); + + // Remove existing block entity at this position + BlockEntity existing = chunk.getTile(pos.x() & 0xF, pos.y(), pos.z() & 0xF); + if (existing != null) { + existing.close(); + } + + // Create new block entity from NBT + if (nbt.contains("id")) { + String id = nbt.getString("id").replaceFirst("BlockEntity", ""); + BlockEntity.createBlockEntity(id, chunk, nbt); + } + } + } + + // Store copy for undo + if (copy != null) { + copies.put(copyKey, copy); + } + + chunk.setChanged(true); + for (cn.nukkit.Player player : level.getChunkPlayers(chunkX, chunkZ).values()) { + level.requestChunk(chunkX, chunkZ, player); + } + + if (finalizer != null) { + finalizer.run(); + } + return (T) (Future) CompletableFuture.completedFuture(null); + } + + @Override + public int getSkyLight(int x, int y, int z) { + BaseFullChunk chunk = level.getChunk(chunkX, chunkZ, true); + if (chunk == null) { + return 15; + } + return chunk.getBlockSkyLight(x & 0xF, y, z & 0xF); + } + + @Override + public int getEmittedLight(int x, int y, int z) { + BaseFullChunk chunk = level.getChunk(chunkX, chunkZ, true); + if (chunk == null) { + return 0; + } + return chunk.getBlockLight(x & 0xF, y, z & 0xF); + } + + @Override + public int[] getHeightMap(HeightMapType type) { + return new int[256]; + } + + @Nullable + @Override + public FaweCompoundTag tile(int x, int y, int z) { + BaseFullChunk chunk = level.getChunk(chunkX, chunkZ, true); + if (chunk == null) { + return null; + } + BlockEntity blockEntity = chunk.getTile(x & 0xF, y, z & 0xF); + if (blockEntity == null) { + return null; + } + return NukkitNbtConverter.toFawe(blockEntity.namedTag); + } + + @Override + public Map tiles() { + BaseFullChunk chunk = level.getChunk(chunkX, chunkZ, true); + if (chunk == null) { + return Collections.emptyMap(); + } + Map blockEntities = chunk.getBlockEntities(); + if (blockEntities.isEmpty()) { + return Collections.emptyMap(); + } + Map result = new HashMap<>(); + for (BlockEntity be : blockEntities.values()) { + BlockVector3 pos = BlockVector3.at(be.getFloorX(), be.getFloorY(), be.getFloorZ()); + result.put(pos, NukkitNbtConverter.toFawe(be.namedTag)); + } + return result; + } + + @Override + public Collection entities() { + return Collections.emptyList(); + } + + @Override + public Set getFullEntities() { + return Collections.emptySet(); + } + + @Nullable + @Override + public FaweCompoundTag entity(UUID uuid) { + return null; + } + + @Override + public void setLightingToGet(char[][] lighting, int startSectionIndex, int endSectionIndex) { + // Lighting managed by Nukkit + } + + @Override + public void setSkyLightingToGet(char[][] lighting, int startSectionIndex, int endSectionIndex) { + // Lighting managed by Nukkit + } + + @Override + public void setHeightmapToGet(HeightMapType type, int[] data) { + // Heightmap managed by Nukkit + } + + @Override + public void removeSectionLighting(int layer, boolean sky) { + // Lighting managed by Nukkit + } + + @Override + public boolean isCreateCopy() { + return createCopy; + } + + @Override + public int setCreateCopy(boolean createCopy) { + if (!callLock.isHeldByCurrentThread()) { + throw new IllegalStateException("Not call-locked"); + } + this.createCopy = createCopy; + return ++this.copyKey; + } + + @Override + public IChunkGet getCopy(final int key) { + return copies.remove(key); + } + + @Override + public void lockCall() { + this.callLock.lock(); + } + + @Override + public void unlockCall() { + this.callLock.unlock(); + } + + @Override + public int getMaxY() { + return MAX_Y; + } + + @Override + public int getMinY() { + return MIN_Y; + } + +} diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitGetBlocks_Copy.java b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitGetBlocks_Copy.java new file mode 100644 index 0000000000..d743a032ef --- /dev/null +++ b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitGetBlocks_Copy.java @@ -0,0 +1,265 @@ +package com.sk89q.worldedit.nukkitmot; + +import com.fastasyncworldedit.core.extent.processor.heightmap.HeightMapType; +import com.fastasyncworldedit.core.nbt.FaweCompoundTag; +import com.fastasyncworldedit.core.queue.IChunk; +import com.fastasyncworldedit.core.queue.IChunkGet; +import com.fastasyncworldedit.core.queue.IChunkSet; +import com.fastasyncworldedit.core.queue.IQueueExtent; +import com.sk89q.worldedit.entity.Entity; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.world.biome.BiomeType; +import com.sk89q.worldedit.world.biome.BiomeTypes; +import com.sk89q.worldedit.world.block.BaseBlock; +import com.sk89q.worldedit.world.block.BlockState; +import com.sk89q.worldedit.world.block.BlockTypesCache; +import org.enginehub.linbus.tree.LinCompoundTag; + +import javax.annotation.Nullable; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.Future; + +/** + * Immutable snapshot copy of a chunk for undo/history. + */ +public class NukkitGetBlocks_Copy implements IChunkGet { + + private static final int MIN_Y = -64; + private static final int MAX_Y = 319; + private static final int SECTION_COUNT = (MAX_Y >> 4) - (MIN_Y >> 4) + 1; + + private final int chunkX; + private final int chunkZ; + private final char[][] blocks; + private BiomeType[][] biomes; + private final Map tiles = new HashMap<>(); + + public NukkitGetBlocks_Copy(int chunkX, int chunkZ) { + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.blocks = new char[SECTION_COUNT][]; + } + + protected void storeSection(int layer, char[] data) { + int index = layer - (MIN_Y >> 4); + blocks[index] = data; + } + + protected void storeBiome(int x, int y, int z, BiomeType biome) { + if (biomes == null) { + biomes = new BiomeType[SECTION_COUNT][]; + } + int layer = (y >> 4) - (MIN_Y >> 4); + if (biomes[layer] == null) { + biomes[layer] = new BiomeType[4096]; + } + int localY = y & 0xF; + biomes[layer][(localY << 8) | ((z & 0xF) << 4) | (x & 0xF)] = biome; + } + + protected void storeTile(BlockVector3 pos, FaweCompoundTag tag) { + tiles.put(pos, tag); + } + + private int layerIndex(int layer) { + return layer - (MIN_Y >> 4); + } + + @Override + public int getX() { + return chunkX; + } + + @Override + public int getZ() { + return chunkZ; + } + + @Override + public BaseBlock getFullBlock(int x, int y, int z) { + BlockState state = getBlock(x, y, z); + FaweCompoundTag tileTag = tile(x, y, z); + if (tileTag != null) { + LinCompoundTag linTag = tileTag.linTag(); + return state.toBaseBlock(linTag); + } + return state.toBaseBlock(); + } + + @Override + public BiomeType getBiomeType(int x, int y, int z) { + if (biomes == null) { + return BiomeTypes.PLAINS; + } + int layer = (y >> 4) - (MIN_Y >> 4); + if (layer < 0 || layer >= SECTION_COUNT || biomes[layer] == null) { + return BiomeTypes.PLAINS; + } + int localY = y & 0xF; + BiomeType type = biomes[layer][(localY << 8) | ((z & 0xF) << 4) | (x & 0xF)]; + return type != null ? type : BiomeTypes.PLAINS; + } + + @Override + public BlockState getBlock(int x, int y, int z) { + int layer = y >> 4; + int index = layerIndex(layer); + if (index < 0 || index >= SECTION_COUNT || blocks[index] == null) { + return BlockTypesCache.states[BlockTypesCache.ReservedIDs.AIR]; + } + int localY = y & 0xF; + char ordinal = blocks[index][((localY << 8) | ((z & 0xF) << 4) | (x & 0xF))]; + if (ordinal == BlockTypesCache.ReservedIDs.__RESERVED__) { + return BlockTypesCache.states[BlockTypesCache.ReservedIDs.AIR]; + } + return BlockTypesCache.states[ordinal]; + } + + @Override + public int getSkyLight(int x, int y, int z) { + return 15; + } + + @Override + public int getEmittedLight(int x, int y, int z) { + return 0; + } + + @Override + public int[] getHeightMap(HeightMapType type) { + return new int[256]; + } + + @Nullable + @Override + public FaweCompoundTag tile(int x, int y, int z) { + return tiles.get(BlockVector3.at(x, y, z)); + } + + @Override + public Map tiles() { + return Collections.unmodifiableMap(tiles); + } + + @Override + public Collection entities() { + return Collections.emptyList(); + } + + @Override + public Set getFullEntities() { + return Collections.emptySet(); + } + + @Nullable + @Override + public FaweCompoundTag entity(UUID uuid) { + return null; + } + + @Override + public boolean hasSection(int layer) { + int index = layerIndex(layer); + return index >= 0 && index < SECTION_COUNT && blocks[index] != null; + } + + @Override + public char[] load(int layer) { + int index = layerIndex(layer); + if (index < 0 || index >= SECTION_COUNT) { + return new char[4096]; + } + if (blocks[index] == null) { + return new char[4096]; + } + return blocks[index]; + } + + @Nullable + @Override + public char[] loadIfPresent(int layer) { + int index = layerIndex(layer); + if (index < 0 || index >= SECTION_COUNT) { + return null; + } + return blocks[index]; + } + + @Override + public void removeSectionLighting(int layer, boolean sky) { + } + + @Override + public boolean isCreateCopy() { + return false; + } + + @Override + public int setCreateCopy(boolean createCopy) { + return -1; + } + + @Override + public void setLightingToGet(char[][] lighting, int startSectionIndex, int endSectionIndex) { + } + + @Override + public void setSkyLightingToGet(char[][] lighting, int startSectionIndex, int endSectionIndex) { + } + + @Override + public void setHeightmapToGet(HeightMapType type, int[] data) { + } + + @Override + public int getSectionCount() { + return SECTION_COUNT; + } + + @Override + public int getMinSectionPosition() { + return MIN_Y >> 4; + } + + @Override + public int getMaxSectionPosition() { + return MAX_Y >> 4; + } + + @Override + public int getMaxY() { + return MAX_Y; + } + + @Override + public int getMinY() { + return MIN_Y; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public > T call(IQueueExtent owner, IChunkSet set, Runnable finalizer) { + throw new UnsupportedOperationException("Copy does not support call()"); + } + + @Override + public IChunkGet reset() { + return this; + } + + @Override + public boolean trim(boolean aggressive) { + return false; + } + + @Override + public boolean trim(boolean aggressive, int layer) { + return false; + } + +} diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitItemRegistry.java b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitItemRegistry.java new file mode 100644 index 0000000000..1e0c50eaa7 --- /dev/null +++ b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitItemRegistry.java @@ -0,0 +1,18 @@ +package com.sk89q.worldedit.nukkitmot; + +import com.fastasyncworldedit.nukkitmot.mapping.ItemMapping; +import com.sk89q.worldedit.world.registry.BundledItemRegistry; + +import java.util.Collection; + +/** + * Nukkit item registry that provides all known JE item IDs from the mapping data. + */ +class NukkitItemRegistry extends BundledItemRegistry { + + @Override + public Collection values() { + return ItemMapping.getAllJeItemIds(); + } + +} diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitPlayer.java b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitPlayer.java new file mode 100644 index 0000000000..48806bdb77 --- /dev/null +++ b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitPlayer.java @@ -0,0 +1,261 @@ +package com.sk89q.worldedit.nukkitmot; + +import cn.nukkit.Player; +import cn.nukkit.Server; +import com.sk89q.worldedit.blocks.BaseItemStack; +import com.sk89q.worldedit.entity.BaseEntity; +import com.sk89q.worldedit.extension.platform.AbstractPlayerActor; +import com.sk89q.worldedit.extent.inventory.BlockBag; +import com.sk89q.worldedit.internal.cui.CUIEvent; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.math.Vector3; +import com.sk89q.worldedit.session.SessionKey; +import com.sk89q.worldedit.util.HandSide; +import com.sk89q.worldedit.util.Location; +import com.sk89q.worldedit.util.formatting.WorldEditText; +import com.sk89q.worldedit.util.formatting.text.Component; +import com.sk89q.worldedit.util.formatting.text.serializer.plain.PlainComponentSerializer; +import com.sk89q.worldedit.world.World; +import com.sk89q.worldedit.world.block.BlockStateHolder; +import com.sk89q.worldedit.world.gamemode.GameMode; +import com.sk89q.worldedit.world.gamemode.GameModes; + +import javax.annotation.Nullable; +import java.io.File; +import java.util.Locale; +import java.util.UUID; + +public class NukkitPlayer extends AbstractPlayerActor { + + private final Player player; + + public NukkitPlayer(Player player) { + this.player = player; + } + + @Override + public UUID getUniqueId() { + return player.getUniqueId(); + } + + @Override + public String getName() { + return player.getName(); + } + + @Override + public String getDisplayName() { + return player.getDisplayName(); + } + + @Override + public BaseItemStack getItemInHand(HandSide handSide) { + return NukkitAdapter.adaptItemStack(player.getInventory().getItemInHand()); + } + + @Override + public void giveItem(BaseItemStack itemStack) { + player.getInventory().addItem(NukkitAdapter.adaptItem(itemStack)); + } + + @Deprecated + @Override + public void printRaw(String msg) { + for (String part : msg.split("\n")) { + player.sendMessage(part); + } + } + + @Deprecated + @Override + public void print(String msg) { + for (String part : msg.split("\n")) { + player.sendMessage("\u00A7d" + part); + } + } + + @Deprecated + @Override + public void printDebug(String msg) { + for (String part : msg.split("\n")) { + player.sendMessage("\u00A77" + part); + } + } + + @Deprecated + @Override + public void printError(String msg) { + for (String part : msg.split("\n")) { + player.sendMessage("\u00A7c" + part); + } + } + + @Override + public void print(Component component) { + String text = PlainComponentSerializer.INSTANCE.serialize( + WorldEditText.format(component, getLocale()) + ); + player.sendMessage(text); + } + + @Override + public boolean trySetPosition(Vector3 pos, float pitch, float yaw) { + cn.nukkit.level.Location loc = new cn.nukkit.level.Location( + pos.x(), pos.y(), pos.z(), yaw, pitch, player.getLevel() + ); + return player.teleport(loc); + } + + @Override + public String[] getGroups() { + return new String[0]; + } + + @Override + public BlockBag getInventoryBlockBag() { + return null; + } + + @Override + public GameMode getGameMode() { + return switch (player.getGamemode()) { + case 1 -> GameModes.CREATIVE; + case 2 -> GameModes.ADVENTURE; + case 3 -> GameModes.SPECTATOR; + default -> GameModes.SURVIVAL; + }; + } + + @Override + public void setGameMode(GameMode gameMode) { + int mode = switch (gameMode.id()) { + case "creative" -> Player.CREATIVE; + case "adventure" -> Player.ADVENTURE; + case "spectator" -> Player.SPECTATOR; + default -> Player.SURVIVAL; + }; + player.setGamemode(mode); + } + + @Override + public boolean hasPermission(String perm) { + return player.isOp() || player.hasPermission(perm); + } + + @Override + public void setPermission(String permission, boolean value) { + // Nukkit doesn't have a simple runtime permission attachment API like Bukkit + } + + @Override + public World getWorld() { + return NukkitAdapter.adapt(player.getLevel()); + } + + @Override + public void dispatchCUIEvent(CUIEvent event) { + // CUI not supported on Bedrock clients + } + + @Override + public boolean isAllowedToFly() { + return player.getAllowFlight(); + } + + @Override + public void setFlying(boolean flying) { + player.setAllowFlight(flying); + } + + @Override + public BaseEntity getState() { + throw new UnsupportedOperationException("Cannot create a state from this object"); + } + + @Override + public Location getLocation() { + return NukkitAdapter.adapt(player.getLocation()); + } + + @Override + public boolean setLocation(Location location) { + return player.teleport(NukkitAdapter.adapt(location)); + } + + @Override + public Locale getLocale() { + return Locale.getDefault(); + } + + @Override + public File openFileOpenDialog(String[] extensions) { + return null; + } + + @Override + public File openFileSaveDialog(String[] extensions) { + return null; + } + + @Nullable + @Override + public T getFacet(Class cls) { + return null; + } + + @Override + public SessionKey getSessionKey() { + return new SessionKeyImpl(player); + } + + @Override + public > void sendFakeBlock(BlockVector3 pos, B block) { + // TODO: Implement fake block sending via UpdateBlockPacket + } + + @Override + public void sendTitle(Component title, Component sub) { + String titleStr = PlainComponentSerializer.INSTANCE.serialize(title); + String subStr = PlainComponentSerializer.INSTANCE.serialize(sub); + player.sendTitle(titleStr, subStr, 0, 70, 20); + } + + public Player getPlayer() { + return player; + } + + static class SessionKeyImpl implements SessionKey { + + private final UUID uuid; + private final String name; + + SessionKeyImpl(Player player) { + this.uuid = player.getUniqueId(); + this.name = player.getName(); + } + + @Override + public UUID getUniqueId() { + return uuid; + } + + @Nullable + @Override + public String getName() { + return name; + } + + @Override + public boolean isActive() { + Player player = Server.getInstance().getPlayer(uuid).orElse(null); + return player != null; + } + + @Override + public boolean isPersistent() { + return true; + } + + } + +} diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitRegistries.java b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitRegistries.java new file mode 100644 index 0000000000..b7a3e415d2 --- /dev/null +++ b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitRegistries.java @@ -0,0 +1,33 @@ +package com.sk89q.worldedit.nukkitmot; + +import com.sk89q.worldedit.world.registry.BlockRegistry; +import com.sk89q.worldedit.world.registry.BundledRegistries; +import com.sk89q.worldedit.world.registry.ItemRegistry; + +/** + * Nukkit platform registries, extending BundledRegistries for base functionality. + */ +class NukkitRegistries extends BundledRegistries { + + private static final NukkitRegistries INSTANCE = new NukkitRegistries(); + private final BlockRegistry blockRegistry = new NukkitBlockRegistry(); + private final ItemRegistry itemRegistry = new NukkitItemRegistry(); + + NukkitRegistries() { + } + + @Override + public BlockRegistry getBlockRegistry() { + return blockRegistry; + } + + @Override + public ItemRegistry getItemRegistry() { + return itemRegistry; + } + + public static NukkitRegistries getInstance() { + return INSTANCE; + } + +} diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitServerInterface.java b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitServerInterface.java new file mode 100644 index 0000000000..14bffe3a41 --- /dev/null +++ b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitServerInterface.java @@ -0,0 +1,241 @@ +package com.sk89q.worldedit.nukkitmot; + +import cn.nukkit.Server; +import cn.nukkit.command.Command; +import cn.nukkit.command.CommandSender; +import cn.nukkit.level.Level; +import com.fastasyncworldedit.core.extent.processor.lighting.RelighterFactory; +import com.fastasyncworldedit.nukkitmot.NukkitRelighter; +import com.sk89q.worldedit.LocalConfiguration; +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.command.util.PermissionCondition; +import com.sk89q.worldedit.entity.Player; +import com.sk89q.worldedit.extension.platform.AbstractPlatform; +import com.sk89q.worldedit.extension.platform.Actor; +import com.sk89q.worldedit.extension.platform.Capability; +import com.sk89q.worldedit.extension.platform.MultiUserPlatform; +import com.sk89q.worldedit.extension.platform.Preference; +import com.sk89q.worldedit.internal.Constants; +import com.sk89q.worldedit.util.SideEffect; +import com.sk89q.worldedit.world.World; +import com.sk89q.worldedit.world.registry.Registries; +import org.enginehub.piston.CommandManager; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +public class NukkitServerInterface extends AbstractPlatform implements MultiUserPlatform { + + private final WorldEditNukkitPlugin plugin; + private final Server server; + private boolean hookingEvents; + + public NukkitServerInterface(WorldEditNukkitPlugin plugin, Server server) { + this.plugin = plugin; + this.server = server; + } + + @Override + public Registries getRegistries() { + return NukkitRegistries.getInstance(); + } + + @Override + public int getDataVersion() { + // Must match the JE block registry version (je_blocks.json from misode/mcmeta) + return Constants.DATA_VERSION_MC_1_21_10; + } + + @Override + public boolean isValidMobType(String type) { + return type != null && type.startsWith("minecraft:"); + } + + @Override + public void reload() { + plugin.loadConfiguration(); + } + + @Override + public int schedule(long delay, long period, Runnable task) { + return server.getScheduler() + .scheduleDelayedRepeatingTask(plugin, task, (int) delay, (int) period) + .getTaskId(); + } + + @Override + public List getWorlds() { + List ret = new ArrayList<>(); + for (Level level : server.getLevels().values()) { + ret.add(NukkitAdapter.adapt(level)); + } + return ret; + } + + @Nullable + @Override + public Player matchPlayer(Player player) { + if (player instanceof NukkitPlayer) { + return player; + } + cn.nukkit.Player nukkitPlayer = server.getPlayerExact(player.getName()); + return nukkitPlayer != null ? NukkitAdapter.adapt(nukkitPlayer) : null; + } + + @Nullable + @Override + public World matchWorld(World world) { + if (world instanceof NukkitWorld) { + return world; + } + Level level = server.getLevelByName(world.getName()); + return level != null ? NukkitAdapter.adapt(level) : null; + } + + @Override + public void registerCommands(CommandManager dispatcher) { + dispatcher.getAllCommands().forEach(command -> { + String[] permissionsArray = command.getCondition() + .as(PermissionCondition.class) + .map(PermissionCondition::getPermissions) + .map(s -> s.toArray(new String[0])) + .orElseGet(() -> new String[0]); + + String[] aliases = Stream.concat( + Stream.of(command.getName()), + command.getAliases().stream() + ).toArray(String[]::new); + + NukkitCommand nukkitCommand = new NukkitCommand( + command.getName(), + aliases, + permissionsArray + ); + server.getCommandMap().register("worldedit", nukkitCommand); + }); + } + + @Override + public void setGameHooksEnabled(boolean enabled) { + this.hookingEvents = enabled; + } + + boolean isHookingEvents() { + return hookingEvents; + } + + @Override + public LocalConfiguration getConfiguration() { + return plugin.getLocalConfiguration(); + } + + @Override + public String getVersion() { + return plugin.getDescription().getVersion(); + } + + @Override + public String getPlatformName() { + return "Nukkit-MOT"; + } + + @Override + public String getPlatformVersion() { + return plugin.getDescription().getVersion(); + } + + @Override + public String id() { + return "intellectualsites:nukkit-mot"; + } + + @Override + public Map getCapabilities() { + Map capabilities = new EnumMap<>(Capability.class); + capabilities.put(Capability.CONFIGURATION, Preference.NORMAL); + capabilities.put(Capability.WORLDEDIT_CUI, Preference.NORMAL); + capabilities.put(Capability.GAME_HOOKS, Preference.PREFERRED); + capabilities.put(Capability.PERMISSIONS, Preference.PREFERRED); + capabilities.put(Capability.USER_COMMANDS, Preference.PREFERRED); + capabilities.put(Capability.WORLD_EDITING, Preference.PREFERRED); + return capabilities; + } + + @Override + public Set getSupportedSideEffects() { + return Set.of(SideEffect.HEIGHTMAPS, SideEffect.LIGHTING); + } + + @Override + public Collection getConnectedUsers() { + List users = new ArrayList<>(); + for (cn.nukkit.Player player : server.getOnlinePlayers().values()) { + users.add(NukkitAdapter.adapt(player)); + } + return users; + } + + @Nonnull + @Override + public RelighterFactory getRelighterFactory() { + // Nukkit handles lighting internally; use NukkitRelighter (not NullRelighter) to pass RelightProcessor check + return (relightMode, world, queue) -> new NukkitRelighter(); + } + + @Override + public int versionMinY() { + return -64; + } + + @Override + public int versionMaxY() { + return 319; + } + + /** + * A Nukkit Command wrapper that dispatches to WorldEdit. + */ + private static class NukkitCommand extends Command { + + NukkitCommand(String name, String[] aliases, String[] permissions) { + super(name); + if (aliases.length > 1) { + setAliases(java.util.Arrays.copyOfRange(aliases, 1, aliases.length)); + } + if (permissions.length > 0) { + setPermission(String.join(";", permissions)); + } + setDescription("WorldEdit command"); + } + + @Override + public boolean execute(CommandSender sender, String label, String[] args) { + StringBuilder sb = new StringBuilder(label); + for (String arg : args) { + sb.append(" ").append(arg); + } + String commandLine = sb.toString(); + + Actor actor; + if (sender instanceof cn.nukkit.Player player) { + actor = NukkitAdapter.adapt(player); + } else { + actor = new NukkitCommandSender(sender); + } + + WorldEdit.getInstance().getEventBus().post( + new com.sk89q.worldedit.event.platform.CommandEvent(actor, commandLine) + ); + return true; + } + + } + +} diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitWorld.java b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitWorld.java new file mode 100644 index 0000000000..d5d0796ed9 --- /dev/null +++ b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitWorld.java @@ -0,0 +1,291 @@ +package com.sk89q.worldedit.nukkitmot; + +import cn.nukkit.block.Block; +import cn.nukkit.level.Level; +import com.fastasyncworldedit.core.nbt.FaweCompoundTag; +import com.fastasyncworldedit.core.queue.IChunkGet; +import com.fastasyncworldedit.core.queue.implementation.packet.ChunkPacket; +import com.fastasyncworldedit.nukkitmot.mapping.BiomeMapping; +import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.WorldEditException; +import com.sk89q.worldedit.blocks.BaseItemStack; +import com.sk89q.worldedit.entity.Entity; +import com.sk89q.worldedit.entity.Player; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.regions.Region; +import com.sk89q.worldedit.util.SideEffect; +import com.sk89q.worldedit.util.SideEffectSet; +import com.sk89q.worldedit.util.TreeGenerator; +import com.sk89q.worldedit.world.AbstractWorld; +import com.sk89q.worldedit.world.biome.BiomeType; +import com.sk89q.worldedit.world.biome.BiomeTypes; +import com.sk89q.worldedit.world.block.BaseBlock; +import com.sk89q.worldedit.world.block.BlockState; +import com.sk89q.worldedit.world.block.BlockStateHolder; +import com.sk89q.worldedit.world.block.BlockTypes; + +import javax.annotation.Nullable; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +public class NukkitWorld extends AbstractWorld { + + private final WeakReference levelRef; + private final String name; + + public NukkitWorld(Level level) { + this.levelRef = new WeakReference<>(level); + this.name = level.getName(); + } + + public Level getLevel() { + Level level = levelRef.get(); + if (level == null) { + throw new RuntimeException("World '" + name + "' has been unloaded"); + } + return level; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getNameUnsafe() { + return name; + } + + @Override + public String id() { + return getName(); + } + + @Override + public > boolean setBlock(BlockVector3 position, B block, SideEffectSet sideEffects) + throws WorldEditException { + int fullId = NukkitAdapter.adaptFullId(block.toImmutableState()); + int blockId = fullId >> Block.DATA_BITS; + int meta = fullId & Block.DATA_MASK; + + Level level = getLevel(); + Block nukkitBlock = Block.get(blockId, meta); + return level.setBlock(position.x(), position.y(), position.z(), nukkitBlock, true, true); + } + + @Override + public Set applySideEffects( + BlockVector3 position, BlockState previousType, SideEffectSet sideEffectSet + ) throws WorldEditException { + return Set.of(); + } + + @Override + public BaseBlock getFullBlock(BlockVector3 position) { + return getBlock(position).toBaseBlock(); + } + + @Override + public BlockState getBlock(BlockVector3 position) { + Level level = getLevel(); + Block block = level.getBlock(position.x(), position.y(), position.z()); + int fullId = (block.getId() << Block.DATA_BITS) | block.getDamage(); + return NukkitAdapter.adaptBlockState(fullId); + } + + @Override + public BiomeType getBiome(BlockVector3 position) { + Level level = getLevel(); + int biomeId = level.getBiomeId(position.x(), position.z()); + String jeBiome = BiomeMapping.beToJe(biomeId); + BiomeType type = BiomeTypes.get(jeBiome); + return type != null ? type : BiomeTypes.PLAINS; + } + + @Override + public boolean setBiome(BlockVector3 position, BiomeType biome) { + Level level = getLevel(); + int beBiomeId = BiomeMapping.jeToBe(biome.id()); + level.setBiomeId(position.x(), position.z(), (byte) beBiomeId); + return true; + } + + @Override + public boolean clearContainerBlockContents(BlockVector3 position) { + cn.nukkit.blockentity.BlockEntity be = getLevel().getBlockEntity(NukkitAdapter.adapt(position)); + if (be instanceof cn.nukkit.inventory.InventoryHolder holder) { + holder.getInventory().clearAll(); + return true; + } + return false; + } + + @Override + public void dropItem(com.sk89q.worldedit.math.Vector3 position, BaseItemStack item) { + cn.nukkit.item.Item nukkitItem = NukkitAdapter.adaptItem(item); + if (nukkitItem.getId() != cn.nukkit.item.Item.AIR) { + getLevel().dropItem(NukkitAdapter.adapt(position), nukkitItem); + } + } + + @Override + public void simulateBlockMine(BlockVector3 position) { + Level level = getLevel(); + level.setBlock(position.x(), position.y(), position.z(), Block.get(Block.AIR), true, true); + } + + @Override + public boolean generateTree(TreeGenerator.TreeType type, EditSession editSession, BlockVector3 position) { + Level level = getLevel(); + cn.nukkit.math.NukkitRandom random = new cn.nukkit.math.NukkitRandom(); + int x = position.x(); + int y = position.y(); + int z = position.z(); + cn.nukkit.math.Vector3 pos = new cn.nukkit.math.Vector3(x, y, z); + + // ObjectTree subclasses use placeObject(ChunkManager, x, y, z, NukkitRandom) + cn.nukkit.level.generator.object.tree.ObjectTree objectTree = switch (type) { + case TREE, BIG_TREE -> new cn.nukkit.level.generator.object.tree.ObjectOakTree(); + case REDWOOD, TALL_REDWOOD -> new cn.nukkit.level.generator.object.tree.ObjectSpruceTree(); + case MEGA_REDWOOD -> new cn.nukkit.level.generator.object.tree.ObjectBigSpruceTree(0.45f, 2); + case BIRCH -> new cn.nukkit.level.generator.object.tree.ObjectBirchTree(); + case TALL_BIRCH -> new cn.nukkit.level.generator.object.tree.ObjectTallBirchTree(); + case SMALL_JUNGLE, SHORT_JUNGLE -> new cn.nukkit.level.generator.object.tree.ObjectJungleTree(); + case CRIMSON_FUNGUS -> new cn.nukkit.level.generator.object.tree.ObjectCrimsonTree(); + case WARPED_FUNGUS -> new cn.nukkit.level.generator.object.tree.ObjectWarpedTree(); + default -> null; + }; + if (objectTree != null) { + objectTree.placeObject(level, x, y, z, random); + return true; + } + + // TreeGenerator subclasses use generate(ChunkManager, NukkitRandom, Vector3) + cn.nukkit.level.generator.object.tree.TreeGenerator treeGen = switch (type) { + case JUNGLE -> new cn.nukkit.level.generator.object.tree.ObjectJungleBigTree( + 10, 20, + Block.get(Block.WOOD, cn.nukkit.block.BlockWood.JUNGLE), + Block.get(Block.LEAVES, cn.nukkit.block.BlockWood.JUNGLE) + ); + case SWAMP -> new cn.nukkit.level.generator.object.tree.ObjectSwampTree(); + case ACACIA -> new cn.nukkit.level.generator.object.tree.ObjectSavannaTree(); + case DARK_OAK -> new cn.nukkit.level.generator.object.tree.ObjectDarkOakTree(); + case MANGROVE, TALL_MANGROVE -> new cn.nukkit.level.generator.object.tree.ObjectMangroveTree(); + case CHERRY -> new cn.nukkit.level.generator.object.tree.ObjectCherryTree(); + case PALE_OAK, PALE_OAK_CREAKING -> new cn.nukkit.level.generator.object.tree.ObjectPaleOakTree(); + default -> null; + }; + if (treeGen != null) { + return treeGen.generate(level, random, pos); + } + + return false; + } + + @Override + public BlockVector3 getSpawnPosition() { + Level level = getLevel(); + cn.nukkit.math.Vector3 spawn = level.getSpawnLocation(); + return BlockVector3.at(spawn.getFloorX(), spawn.getFloorY(), spawn.getFloorZ()); + } + + @Override + public void refreshChunk(int chunkX, int chunkZ) { + Level level = getLevel(); + for (cn.nukkit.Player player : level.getChunkPlayers(chunkX, chunkZ).values()) { + level.requestChunk(chunkX, chunkZ, player); + } + } + + @Override + public IChunkGet get(int chunkX, int chunkZ) { + return new NukkitGetBlocks(getLevel(), chunkX, chunkZ); + } + + @Override + public void sendFakeChunk(@Nullable Player player, ChunkPacket packet) { + // TODO: Implement fake chunk sending + } + + @Override + public List getEntities(Region region) { + List entities = new ArrayList<>(); + for (cn.nukkit.entity.Entity entity : getLevel().getEntities()) { + if (region.contains(BlockVector3.at(entity.getFloorX(), entity.getFloorY(), entity.getFloorZ()))) { + entities.add(new NukkitEntity(entity)); + } + } + return entities; + } + + @Override + public List getEntities() { + List entities = new ArrayList<>(); + for (cn.nukkit.entity.Entity entity : getLevel().getEntities()) { + entities.add(new NukkitEntity(entity)); + } + return entities; + } + + @Override + public int getMinY() { + return getLevel().getMinBlockY(); + } + + @Override + public int getMaxY() { + return getLevel().getMaxBlockY(); + } + + @Override + public boolean equals(Object other) { + if (other == null) { + return false; + } + if (other == this) { + return true; + } + if (other instanceof NukkitWorld otherWorld) { + return name.equals(otherWorld.name); + } else if (other instanceof com.sk89q.worldedit.world.World otherWorld) { + return name.equals(otherWorld.getName()); + } + return false; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean tile(int x, int y, int z, FaweCompoundTag tile) throws WorldEditException { + Level level = getLevel(); + cn.nukkit.level.format.generic.BaseFullChunk chunk = level.getChunk(x >> 4, z >> 4, true); + if (chunk == null) { + return false; + } + cn.nukkit.nbt.tag.CompoundTag nbt = com.fastasyncworldedit.nukkitmot.NukkitNbtConverter.toNukkit(tile); + nbt.putInt("x", x); + nbt.putInt("y", y); + nbt.putInt("z", z); + + cn.nukkit.blockentity.BlockEntity existing = chunk.getTile(x & 0xF, y, z & 0xF); + if (existing != null) { + existing.close(); + } + if (nbt.contains("id")) { + String id = nbt.getString("id").replaceFirst("BlockEntity", ""); + cn.nukkit.blockentity.BlockEntity.createBlockEntity(id, chunk, nbt); + } + return true; + } + + @Override + public void flush() { + // No-op: Nukkit handles chunk flushing internally + } + +} diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitWorldEditListener.java b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitWorldEditListener.java new file mode 100644 index 0000000000..e096ce7742 --- /dev/null +++ b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitWorldEditListener.java @@ -0,0 +1,142 @@ +package com.sk89q.worldedit.nukkitmot; + +import cn.nukkit.Player; +import cn.nukkit.block.Block; +import cn.nukkit.event.EventHandler; +import cn.nukkit.event.EventPriority; +import cn.nukkit.event.Listener; +import cn.nukkit.event.block.BlockBreakEvent; +import cn.nukkit.event.player.PlayerInteractEvent; +import cn.nukkit.event.player.PlayerQuitEvent; +import cn.nukkit.math.BlockFace; +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.event.platform.SessionIdleEvent; +import com.sk89q.worldedit.math.Vector3; +import com.sk89q.worldedit.util.Direction; +import com.sk89q.worldedit.util.Location; + +import java.util.Collections; +import java.util.Set; +import java.util.WeakHashMap; + +/** + * Nukkit event listener for WorldEdit interactions. + */ +public class NukkitWorldEditListener implements Listener { + + private final WorldEditNukkitPlugin plugin; + /** + * Tracks players whose LEFT_CLICK_BLOCK was already handled by {@link #onPlayerInteract}, + * so that {@link #onBlockBreak} can skip duplicate processing for the same click. + */ + private final Set handledLeftClick = Collections.newSetFromMap(new WeakHashMap<>()); + + public NukkitWorldEditListener(WorldEditNukkitPlugin plugin) { + this.plugin = plugin; + } + + @EventHandler(priority = EventPriority.NORMAL) + public void onPlayerInteract(PlayerInteractEvent event) { + Player nukkitPlayer = event.getPlayer(); + NukkitPlayer player = NukkitAdapter.adapt(nukkitPlayer); + WorldEdit we = WorldEdit.getInstance(); + + PlayerInteractEvent.Action action = event.getAction(); + switch (action) { + case LEFT_CLICK_BLOCK -> { + Block block = event.getBlock(); + Location loc = new Location( + player.getWorld(), + Vector3.at(block.getFloorX(), block.getFloorY(), block.getFloorZ()) + ); + Direction direction = adaptFace(event.getFace()); + boolean handled = we.handleBlockLeftClick(player, loc, direction); + if (handled) { + handledLeftClick.add(nukkitPlayer); + event.setCancelled(true); + } + } + case LEFT_CLICK_AIR -> { + if (we.handleArmSwing(player)) { + event.setCancelled(true); + } + } + case RIGHT_CLICK_BLOCK -> { + Block block = event.getBlock(); + Location loc = new Location( + player.getWorld(), + Vector3.at(block.getFloorX(), block.getFloorY(), block.getFloorZ()) + ); + Direction direction = adaptFace(event.getFace()); + boolean handled = we.handleBlockRightClick(player, loc, direction); + if (handled) { + event.setCancelled(true); + } + } + case RIGHT_CLICK_AIR -> { + if (we.handleRightClick(player)) { + event.setCancelled(true); + } + } + default -> { + // PHYSICAL and other actions are not handled + } + } + } + + /** + * Handle block break events for left-click tool interaction. + *

+ * When server authoritative block breaking is enabled (e.g. creative mode), + * {@code PlayerInteractEvent(LEFT_CLICK_BLOCK)} is not fired — only {@code BlockBreakEvent} is. + * This handler delegates to {@code handleBlockLeftClick} to follow the standard FAWE async path. + */ + @EventHandler(priority = EventPriority.LOW) + public void onBlockBreak(BlockBreakEvent event) { + Player nukkitPlayer = event.getPlayer(); + + // Skip if already handled by PlayerInteractEvent(LEFT_CLICK_BLOCK) + if (handledLeftClick.remove(nukkitPlayer)) { + event.setCancelled(true); + return; + } + + NukkitPlayer player = NukkitAdapter.adapt(nukkitPlayer); + WorldEdit we = WorldEdit.getInstance(); + + Block block = event.getBlock(); + Location loc = new Location( + player.getWorld(), + Vector3.at(block.getFloorX(), block.getFloorY(), block.getFloorZ()) + ); + Direction direction = adaptFace(event.getFace()); + boolean handled = we.handleBlockLeftClick(player, loc, direction); + if (handled) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.NORMAL) + public void onPlayerQuit(PlayerQuitEvent event) { + Player nukkitPlayer = event.getPlayer(); + WorldEdit.getInstance().getEventBus().post( + new SessionIdleEvent(new NukkitPlayer.SessionKeyImpl(nukkitPlayer)) + ); + NukkitAdapter.uncachePlayer(nukkitPlayer); + } + + private static Direction adaptFace(BlockFace face) { + if (face == null) { + return Direction.UP; + } + return switch (face) { + case DOWN -> Direction.DOWN; + case UP -> Direction.UP; + case NORTH -> Direction.NORTH; + case SOUTH -> Direction.SOUTH; + case WEST -> Direction.WEST; + case EAST -> Direction.EAST; + }; + } + +} diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/WorldEditNukkitPlugin.java b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/WorldEditNukkitPlugin.java new file mode 100644 index 0000000000..abd25b109a --- /dev/null +++ b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/WorldEditNukkitPlugin.java @@ -0,0 +1,156 @@ +package com.sk89q.worldedit.nukkitmot; + +import cn.nukkit.plugin.PluginBase; +import com.fastasyncworldedit.core.Fawe; +import com.fastasyncworldedit.nukkitmot.FaweNukkit; +import com.fastasyncworldedit.nukkitmot.mapping.BiomeMapping; +import com.fastasyncworldedit.nukkitmot.mapping.BlockMapping; +import com.fastasyncworldedit.nukkitmot.mapping.ItemMapping; +import com.sk89q.worldedit.LocalConfiguration; +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.event.platform.PlatformReadyEvent; +import com.sk89q.worldedit.event.platform.PlatformUnreadyEvent; +import com.sk89q.worldedit.event.platform.PlatformsRegisteredEvent; +import com.sk89q.worldedit.world.biome.BiomeType; +import com.sk89q.worldedit.world.biome.BiomeTypes; +import com.sk89q.worldedit.world.gamemode.GameModes; +import com.sk89q.worldedit.world.weather.WeatherTypes; + +/** + * FastAsyncWorldEdit plugin entry point for Nukkit-MOT. + */ +public class WorldEditNukkitPlugin extends PluginBase { + + private static WorldEditNukkitPlugin instance; + private NukkitConfiguration configuration; + private NukkitServerInterface platform; + + @Override + public void onLoad() { + instance = this; + + // Create data folder + getDataFolder().mkdirs(); + + // Load configuration before registering platform + loadConfiguration(); + + // Register platform with WorldEdit + WorldEdit worldEdit = WorldEdit.getInstance(); + platform = new NukkitServerInterface(this, getServer()); + worldEdit.getPlatformManager().register(platform); + } + + @Override + public void onEnable() { + // Check SQLite JDBC driver (soft dependency) + if (!checkSQLiteDriver()) { + return; + } + + // Initialize FAWE + new FaweNukkit(this); + + WorldEdit worldEdit = WorldEdit.getInstance(); + + // Signal that all platforms are registered + worldEdit.getEventBus().post(new PlatformsRegisteredEvent()); + + // Pre-load JE block/item IDs (needed by BlockTypesCache/ItemTypesCache during loadMappings) + BlockMapping.initJeBlockDefaults(); + ItemMapping.initJeItemIds(); + + // Load bundled mappings (block data, item data, legacy mapper) + worldEdit.loadMappings(); + + // Initialize Nukkit-specific mappings (JE <-> BE block state mapping) + BlockMapping.init(); + BiomeMapping.init(); + ItemMapping.init(); + + // Initialize registries + initializeRegistries(); + + // Register event listeners + getServer().getPluginManager().registerEvents( + new NukkitWorldEditListener(this), this + ); + + // Build ordinal mappings (must be after BlockTypesCache is initialized) + BlockMapping.buildOrdinalMappings(); + + // Signal platform is ready + worldEdit.getEventBus().post(new PlatformReadyEvent(platform)); + + getLogger().info("FastAsyncWorldEdit for Nukkit-MOT enabled."); + } + + @Override + public void onDisable() { + WorldEdit worldEdit = WorldEdit.getInstance(); + + // FAWE cleanup + Fawe.instance().onDisable(); + + // Unload sessions + worldEdit.getSessionManager().unload(); + + // Signal platform is going down + worldEdit.getEventBus().post(new PlatformUnreadyEvent(platform)); + + // Unregister platform + worldEdit.getPlatformManager().unregister(platform); + + // Cancel scheduled tasks + getServer().getScheduler().cancelTask(this); + + getLogger().info("FastAsyncWorldEdit for Nukkit-MOT disabled."); + } + + private boolean checkSQLiteDriver() { + try { + Class.forName("org.sqlite.JDBC"); + return true; + } catch (ClassNotFoundException e) { + getLogger().error("==========================================="); + getLogger().error("SQLite JDBC driver not found!"); + getLogger().error("Please download DbLib.jar and put it"); + getLogger().error("in the server's 'plugins' folder."); + getLogger().error("Download: https://cloudburstmc.org/resources/dblib.12/"); + getLogger().error("==========================================="); + getServer().getPluginManager().disablePlugin(this); + return false; + } + } + + private void initializeRegistries() { + // Initialize biome registry with known biome types + for (String biomeId : BiomeMapping.getAllJeBiomes()) { + if (BiomeType.REGISTRY.get(biomeId) == null) { + BiomeTypes.register(new BiomeType(biomeId)); + } + } + + // Initialize game modes and weather types + GameModes.get(""); + WeatherTypes.get(""); + } + + public void loadConfiguration() { + configuration = new NukkitConfiguration(this); + configuration.load(); + } + + public LocalConfiguration getLocalConfiguration() { + return configuration; + } + + NukkitServerInterface getInternalPlatform() { + return platform; + } + + public static WorldEditNukkitPlugin getInstance() { + return instance; + } + +} diff --git a/worldedit-nukkit-mot/src/main/resources/plugin.yml b/worldedit-nukkit-mot/src/main/resources/plugin.yml new file mode 100644 index 0000000000..eb02f3fe92 --- /dev/null +++ b/worldedit-nukkit-mot/src/main/resources/plugin.yml @@ -0,0 +1,8 @@ +name: FastAsyncWorldEdit +main: com.sk89q.worldedit.nukkitmot.WorldEditNukkitPlugin +version: "${internalVersion}" +api: ["1.0.0"] +load: STARTUP +softdepend: ["DbLib"] +website: https://github.com/IntellectualSites/FastAsyncWorldEdit +description: Blazingly fast world manipulation for builders, large networks and developers. From d460836eb57c5acce891d471832ac7a236a4329f Mon Sep 17 00:00:00 2001 From: LT_Name <572413378@qq.com> Date: Sun, 1 Mar 2026 13:05:00 +0800 Subject: [PATCH 02/12] feat(nukkit-mot): implement entity support and use nukkit plugin logger --- .../nukkitmot/mapping/BiomeMapping.java | 6 +- .../nukkitmot/mapping/BlockMapping.java | 23 ++-- .../nukkitmot/mapping/ItemMapping.java | 8 +- .../worldedit/nukkitmot/NukkitGetBlocks.java | 101 ++++++++++++++++-- .../nukkitmot/NukkitGetBlocks_Copy.java | 71 ++++++++---- 5 files changed, 153 insertions(+), 56 deletions(-) diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/BiomeMapping.java b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/BiomeMapping.java index 3afbfd7a90..8d849cc41b 100644 --- a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/BiomeMapping.java +++ b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/BiomeMapping.java @@ -4,8 +4,7 @@ import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.sk89q.worldedit.nukkitmot.WorldEditNukkitPlugin; import java.io.IOException; import java.io.InputStream; @@ -20,7 +19,6 @@ */ public final class BiomeMapping { - private static final Logger LOGGER = LoggerFactory.getLogger(BiomeMapping.class); private static final Gson GSON = new Gson(); private static final Map JE_TO_BE = new HashMap<>(); @@ -46,7 +44,7 @@ public static void init() { BE_TO_JE.put(entry.bedrockId(), javaId); }); - LOGGER.info("Loaded {} biome mappings", JE_TO_BE.size()); + WorldEditNukkitPlugin.getInstance().getLogger().info("Loaded " + JE_TO_BE.size() + " biome mappings"); } catch (IOException e) { throw new RuntimeException("Failed to load biome mapping", e); } diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/BlockMapping.java b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/BlockMapping.java index 757ff7f9ed..5bca67e773 100644 --- a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/BlockMapping.java +++ b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/BlockMapping.java @@ -1,6 +1,5 @@ package com.fastasyncworldedit.nukkitmot.mapping; -import cn.nukkit.block.Block; import cn.nukkit.level.format.leveldb.BlockStateMapping; import cn.nukkit.level.format.leveldb.NukkitLegacyMapper; import cn.nukkit.level.format.leveldb.structure.BlockStateSnapshot; @@ -12,14 +11,13 @@ import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; +import com.sk89q.worldedit.nukkitmot.WorldEditNukkitPlugin; import com.sk89q.worldedit.world.block.BlockState; import com.sk89q.worldedit.world.block.BlockTypesCache; import it.unimi.dsi.fastutil.ints.Int2CharOpenHashMap; import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMapBuilder; import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; @@ -40,8 +38,6 @@ */ public final class BlockMapping { - private static final Logger LOGGER = LoggerFactory.getLogger(BlockMapping.class); - private static final Gson GSON = new GsonBuilder() .registerTypeAdapterFactory(new IgnoreFailureTypeAdapterFactory()) .create(); @@ -130,8 +126,7 @@ public static void buildOrdinalMappings() { } } - LOGGER.info("Ordinal mappings built: {} mapped, {} unmapped out of {} total states", - mapped, unmapped, states.length); + WorldEditNukkitPlugin.getInstance().getLogger().info("Ordinal mappings built: " + mapped + " mapped, " + unmapped + " unmapped out of " + states.length + " total states"); } /** @@ -211,7 +206,7 @@ public static Collection getAllJeBlockDefaultStates() { private static boolean initJeBlockDefaultProperties() { try (InputStream stream = BlockMapping.class.getClassLoader().getResourceAsStream("je_blocks.json")) { if (stream == null) { - LOGGER.error("je_blocks.json not found"); + WorldEditNukkitPlugin.getInstance().getLogger().error("je_blocks.json not found"); return false; } @@ -223,9 +218,9 @@ private static boolean initJeBlockDefaultProperties() { (Map) entry.getValue().get(1) ); } - LOGGER.info("Loaded {} JE block default properties", JE_BLOCK_DEFAULT_PROPERTIES.size()); + WorldEditNukkitPlugin.getInstance().getLogger().info("Loaded " + JE_BLOCK_DEFAULT_PROPERTIES.size() + " JE block default properties"); } catch (IOException e) { - LOGGER.error("Failed to load je_blocks.json", e); + WorldEditNukkitPlugin.getInstance().getLogger().error("Failed to load je_blocks.json: ", e); return false; } return true; @@ -236,12 +231,12 @@ private static boolean initBlockStateMapping() { List palette = NukkitLegacyMapper.loadBlockPalette(); if (!palette.isEmpty()) { blockStateVersion = palette.getFirst().getInt("version"); - LOGGER.info("Block state version: {}", blockStateVersion); + WorldEditNukkitPlugin.getInstance().getLogger().info("Block state version: " + blockStateVersion); } try (InputStream stream = BlockMapping.class.getClassLoader().getResourceAsStream("mapping/blocks.json")) { if (stream == null) { - LOGGER.error("blocks.json not found"); + WorldEditNukkitPlugin.getInstance().getLogger().error("blocks.json not found"); return false; } @@ -262,9 +257,9 @@ private static boolean initBlockStateMapping() { failed++; } } - LOGGER.info("Block state mapping loaded: {} mapped, {} failed", mapped, failed); + WorldEditNukkitPlugin.getInstance().getLogger().info("Block state mapping loaded: " + mapped + " mapped, " + failed + " failed"); } catch (IOException e) { - LOGGER.error("Failed to load blocks.json", e); + WorldEditNukkitPlugin.getInstance().getLogger().error("Failed to load blocks.json: ", e); return false; } return true; diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/ItemMapping.java b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/ItemMapping.java index 7bfb8867a3..ee8bff24fc 100644 --- a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/ItemMapping.java +++ b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/ItemMapping.java @@ -5,8 +5,7 @@ import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.sk89q.worldedit.nukkitmot.WorldEditNukkitPlugin; import java.io.IOException; import java.io.InputStream; @@ -23,7 +22,6 @@ */ public final class ItemMapping { - private static final Logger LOGGER = LoggerFactory.getLogger(ItemMapping.class); private static final Gson GSON = new Gson(); private static final Map JE_TO_BE = new HashMap<>(); @@ -54,7 +52,7 @@ public static void initJeItemIds() { ); JE_ITEM_IDS.addAll(mappings.keySet()); - LOGGER.info("Loaded {} JE item IDs", JE_ITEM_IDS.size()); + WorldEditNukkitPlugin.getInstance().getLogger().info("Loaded " + JE_ITEM_IDS.size() + " JE item IDs"); } catch (IOException e) { throw new RuntimeException("Failed to load item IDs", e); } @@ -89,7 +87,7 @@ public static void init() { } }); - LOGGER.info("Loaded {} item mappings", JE_TO_BE.size()); + WorldEditNukkitPlugin.getInstance().getLogger().info("Loaded " + JE_TO_BE.size() + " item mappings"); } catch (IOException e) { throw new RuntimeException("Failed to load item mapping", e); } diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitGetBlocks.java b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitGetBlocks.java index b3997b116a..39db9b97c5 100644 --- a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitGetBlocks.java +++ b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitGetBlocks.java @@ -3,7 +3,6 @@ import cn.nukkit.block.Block; import cn.nukkit.blockentity.BlockEntity; import cn.nukkit.level.Level; -import cn.nukkit.level.format.ChunkSection; import cn.nukkit.level.format.generic.BaseFullChunk; import cn.nukkit.nbt.tag.CompoundTag; import com.fastasyncworldedit.core.extent.processor.heightmap.HeightMapType; @@ -22,14 +21,18 @@ import com.sk89q.worldedit.world.biome.BiomeType; import com.sk89q.worldedit.world.biome.BiomeTypes; import com.sk89q.worldedit.world.block.BlockState; -import com.sk89q.worldedit.world.block.BlockType; import com.sk89q.worldedit.world.block.BlockTypesCache; +import org.enginehub.linbus.tree.LinCompoundTag; +import org.enginehub.linbus.tree.LinStringTag; +import org.enginehub.linbus.tree.LinTagType; import javax.annotation.Nullable; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -43,24 +46,26 @@ */ public class NukkitGetBlocks extends CharGetBlocks { - private static final int MIN_Y = -64; - private static final int MAX_Y = 319; private static final int WATER_ID = 8; private static final int STILL_WATER_ID = 9; private final Level level; private final int chunkX; private final int chunkZ; + private final int minY; + private final int maxY; private final ReentrantLock callLock = new ReentrantLock(); private final ConcurrentHashMap copies = new ConcurrentHashMap<>(); private boolean createCopy = false; private int copyKey = 0; public NukkitGetBlocks(Level level, int chunkX, int chunkZ) { - super(MIN_Y >> 4, MAX_Y >> 4); + super(level.getMinBlockY() >> 4, level.getMaxBlockY() >> 4); this.level = level; this.chunkX = chunkX; this.chunkZ = chunkZ; + this.minY = level.getMinBlockY(); + this.maxY = level.getMaxBlockY(); } @Override @@ -158,7 +163,7 @@ public > T call(IQueueExtent owner, IChunk // Create snapshot copy for undo if requested NukkitGetBlocks_Copy copy = null; if (createCopy) { - copy = new NukkitGetBlocks_Copy(chunkX, chunkZ); + copy = new NukkitGetBlocks_Copy(chunkX, chunkZ, minY, maxY); for (int layer = set.getMinSectionPosition(); layer <= set.getMaxSectionPosition(); layer++) { if (!set.hasSection(layer)) { continue; @@ -277,6 +282,49 @@ public > T call(IQueueExtent owner, IChunk } } + // Apply entity removals + Set entityRemoves = set.getEntityRemoves(); + if (entityRemoves != null && !entityRemoves.isEmpty()) { + Map chunkEntities = level.getChunkEntities(chunkX, chunkZ); + Set entitiesRemoved = new HashSet<>(); + for (cn.nukkit.entity.Entity entity : chunkEntities.values()) { + if (entity instanceof cn.nukkit.Player) { + continue; + } + if (entityRemoves.contains(entity.getUniqueId())) { + if (copy != null) { + copy.storeEntity(entity); + } + entity.close(); + entitiesRemoved.add(entity.getUniqueId()); + } + } + set.getEntityRemoves().clear(); + set.getEntityRemoves().addAll(entitiesRemoved); + } + + // Apply entity creations + Collection setEntities = set.entities(); + if (setEntities != null && !setEntities.isEmpty()) { + for (FaweCompoundTag nativeTag : setEntities) { + LinCompoundTag linTag = nativeTag.linTag(); + LinStringTag idTag = linTag.findTag("Id", LinTagType.stringTag()); + if (idTag == null) { + idTag = linTag.findTag("id", LinTagType.stringTag()); + } + if (idTag == null) { + continue; + } + CompoundTag nukkitNbt = NukkitNbtConverter.toNukkit(nativeTag); + cn.nukkit.entity.Entity created = cn.nukkit.entity.Entity.createEntity( + idTag.value(), chunk, nukkitNbt + ); + if (created != null) { + created.spawnToAll(); + } + } + } + // Store copy for undo if (copy != null) { copies.put(copyKey, copy); @@ -350,17 +398,50 @@ public Map tiles() { @Override public Collection entities() { - return Collections.emptyList(); + Map chunkEntities = level.getChunkEntities(chunkX, chunkZ); + if (chunkEntities.isEmpty()) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + for (cn.nukkit.entity.Entity entity : chunkEntities.values()) { + if (entity instanceof cn.nukkit.Player) { + continue; + } + entity.saveNBT(); + result.add(NukkitNbtConverter.toFawe(entity.namedTag)); + } + return result; } @Override public Set getFullEntities() { - return Collections.emptySet(); + Map chunkEntities = level.getChunkEntities(chunkX, chunkZ); + if (chunkEntities.isEmpty()) { + return Collections.emptySet(); + } + Set result = new HashSet<>(); + for (cn.nukkit.entity.Entity entity : chunkEntities.values()) { + if (entity instanceof cn.nukkit.Player) { + continue; + } + result.add(new NukkitEntity(entity)); + } + return result; } @Nullable @Override public FaweCompoundTag entity(UUID uuid) { + Map chunkEntities = level.getChunkEntities(chunkX, chunkZ); + for (cn.nukkit.entity.Entity entity : chunkEntities.values()) { + if (entity instanceof cn.nukkit.Player) { + continue; + } + if (uuid.equals(entity.getUniqueId())) { + entity.saveNBT(); + return NukkitNbtConverter.toFawe(entity.namedTag); + } + } return null; } @@ -415,12 +496,12 @@ public void unlockCall() { @Override public int getMaxY() { - return MAX_Y; + return maxY; } @Override public int getMinY() { - return MIN_Y; + return minY; } } diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitGetBlocks_Copy.java b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitGetBlocks_Copy.java index d743a032ef..513cf4f742 100644 --- a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitGetBlocks_Copy.java +++ b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitGetBlocks_Copy.java @@ -6,6 +6,7 @@ import com.fastasyncworldedit.core.queue.IChunkGet; import com.fastasyncworldedit.core.queue.IChunkSet; import com.fastasyncworldedit.core.queue.IQueueExtent; +import com.fastasyncworldedit.nukkitmot.NukkitNbtConverter; import com.sk89q.worldedit.entity.Entity; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.world.biome.BiomeType; @@ -14,11 +15,14 @@ import com.sk89q.worldedit.world.block.BlockState; import com.sk89q.worldedit.world.block.BlockTypesCache; import org.enginehub.linbus.tree.LinCompoundTag; +import org.enginehub.linbus.tree.LinStringTag; +import org.enginehub.linbus.tree.LinTagType; import javax.annotation.Nullable; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -29,32 +33,37 @@ */ public class NukkitGetBlocks_Copy implements IChunkGet { - private static final int MIN_Y = -64; - private static final int MAX_Y = 319; - private static final int SECTION_COUNT = (MAX_Y >> 4) - (MIN_Y >> 4) + 1; - private final int chunkX; private final int chunkZ; + private final int minY; + private final int maxY; + private final int minSectionPosition; + private final int sectionCount; private final char[][] blocks; private BiomeType[][] biomes; private final Map tiles = new HashMap<>(); + private final Set entities = new HashSet<>(); - public NukkitGetBlocks_Copy(int chunkX, int chunkZ) { + public NukkitGetBlocks_Copy(int chunkX, int chunkZ, int minY, int maxY) { this.chunkX = chunkX; this.chunkZ = chunkZ; - this.blocks = new char[SECTION_COUNT][]; + this.minY = minY; + this.maxY = maxY; + this.minSectionPosition = minY >> 4; + this.sectionCount = (maxY >> 4) - minSectionPosition + 1; + this.blocks = new char[sectionCount][]; } protected void storeSection(int layer, char[] data) { - int index = layer - (MIN_Y >> 4); + int index = layer - minSectionPosition; blocks[index] = data; } protected void storeBiome(int x, int y, int z, BiomeType biome) { if (biomes == null) { - biomes = new BiomeType[SECTION_COUNT][]; + biomes = new BiomeType[sectionCount][]; } - int layer = (y >> 4) - (MIN_Y >> 4); + int layer = (y >> 4) - minSectionPosition; if (biomes[layer] == null) { biomes[layer] = new BiomeType[4096]; } @@ -66,8 +75,13 @@ protected void storeTile(BlockVector3 pos, FaweCompoundTag tag) { tiles.put(pos, tag); } + protected void storeEntity(cn.nukkit.entity.Entity entity) { + entity.saveNBT(); + entities.add(NukkitNbtConverter.toFawe(entity.namedTag)); + } + private int layerIndex(int layer) { - return layer - (MIN_Y >> 4); + return layer - minSectionPosition; } @Override @@ -96,8 +110,8 @@ public BiomeType getBiomeType(int x, int y, int z) { if (biomes == null) { return BiomeTypes.PLAINS; } - int layer = (y >> 4) - (MIN_Y >> 4); - if (layer < 0 || layer >= SECTION_COUNT || biomes[layer] == null) { + int layer = (y >> 4) - minSectionPosition; + if (layer < 0 || layer >= sectionCount || biomes[layer] == null) { return BiomeTypes.PLAINS; } int localY = y & 0xF; @@ -109,7 +123,7 @@ public BiomeType getBiomeType(int x, int y, int z) { public BlockState getBlock(int x, int y, int z) { int layer = y >> 4; int index = layerIndex(layer); - if (index < 0 || index >= SECTION_COUNT || blocks[index] == null) { + if (index < 0 || index >= sectionCount || blocks[index] == null) { return BlockTypesCache.states[BlockTypesCache.ReservedIDs.AIR]; } int localY = y & 0xF; @@ -148,30 +162,41 @@ public Map tiles() { @Override public Collection entities() { - return Collections.emptyList(); + return this.entities; } @Override public Set getFullEntities() { - return Collections.emptySet(); + throw new UnsupportedOperationException("Cannot get full entities from GET copy."); } @Nullable @Override public FaweCompoundTag entity(UUID uuid) { + for (FaweCompoundTag tag : entities) { + LinStringTag uuidTag = tag.linTag().findTag("uuid", LinTagType.stringTag()); + if (uuidTag != null) { + try { + if (uuid.equals(UUID.fromString(uuidTag.value()))) { + return tag; + } + } catch (IllegalArgumentException ignored) { + } + } + } return null; } @Override public boolean hasSection(int layer) { int index = layerIndex(layer); - return index >= 0 && index < SECTION_COUNT && blocks[index] != null; + return index >= 0 && index < sectionCount && blocks[index] != null; } @Override public char[] load(int layer) { int index = layerIndex(layer); - if (index < 0 || index >= SECTION_COUNT) { + if (index < 0 || index >= sectionCount) { return new char[4096]; } if (blocks[index] == null) { @@ -184,7 +209,7 @@ public char[] load(int layer) { @Override public char[] loadIfPresent(int layer) { int index = layerIndex(layer); - if (index < 0 || index >= SECTION_COUNT) { + if (index < 0 || index >= sectionCount) { return null; } return blocks[index]; @@ -218,27 +243,27 @@ public void setHeightmapToGet(HeightMapType type, int[] data) { @Override public int getSectionCount() { - return SECTION_COUNT; + return sectionCount; } @Override public int getMinSectionPosition() { - return MIN_Y >> 4; + return minSectionPosition; } @Override public int getMaxSectionPosition() { - return MAX_Y >> 4; + return maxY >> 4; } @Override public int getMaxY() { - return MAX_Y; + return maxY; } @Override public int getMinY() { - return MIN_Y; + return minY; } @SuppressWarnings({"rawtypes", "unchecked"}) From 9b964521ac7f893bbc0219484ec53e93e8e45e32 Mon Sep 17 00:00:00 2001 From: LT_Name <572413378@qq.com> Date: Sun, 1 Mar 2026 14:27:23 +0800 Subject: [PATCH 03/12] feat(nukkit-mot): implement BlockBag, Preloader and various player/world features --- .../nukkitmot/FaweNukkit.java | 7 +- .../nukkitmot/NukkitPlayerBlockBag.java | 126 ++++++++++++++++++ .../worldedit/nukkitmot/NukkitEntity.java | 2 +- .../worldedit/nukkitmot/NukkitPlayer.java | 50 ++++--- .../worldedit/nukkitmot/NukkitWorld.java | 70 +++++++++- 5 files changed, 235 insertions(+), 20 deletions(-) create mode 100644 worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitPlayerBlockBag.java diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/FaweNukkit.java b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/FaweNukkit.java index 2423159415..59112bd742 100644 --- a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/FaweNukkit.java +++ b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/FaweNukkit.java @@ -7,6 +7,7 @@ import com.fastasyncworldedit.core.Fawe; import com.fastasyncworldedit.core.IFawe; import com.fastasyncworldedit.core.queue.implementation.QueueHandler; +import com.fastasyncworldedit.core.queue.implementation.preloader.AsyncPreloader; import com.fastasyncworldedit.core.queue.implementation.preloader.Preloader; import com.fastasyncworldedit.core.regions.FaweMaskManager; import com.fastasyncworldedit.core.util.TaskManager; @@ -22,6 +23,7 @@ public class FaweNukkit implements IFawe { private final NukkitTaskManager taskManager; private final NukkitPlatformAdapter platformAdapter; private NukkitQueueHandler queueHandler; + private Preloader preloader; public FaweNukkit(Plugin plugin) { this.plugin = plugin; @@ -89,7 +91,10 @@ public QueueHandler getQueueHandler() { @Override public Preloader getPreloader(boolean initialise) { - return null; + if (preloader == null && initialise) { + preloader = new AsyncPreloader(); + } + return preloader; } @Override diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitPlayerBlockBag.java b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitPlayerBlockBag.java new file mode 100644 index 0000000000..07ce67e413 --- /dev/null +++ b/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitPlayerBlockBag.java @@ -0,0 +1,126 @@ +package com.fastasyncworldedit.nukkitmot; + +import cn.nukkit.Player; +import cn.nukkit.item.Item; +import com.fastasyncworldedit.nukkitmot.mapping.ItemMapping; +import com.sk89q.worldedit.extent.inventory.BlockBag; +import com.sk89q.worldedit.extent.inventory.BlockBagException; +import com.sk89q.worldedit.extent.inventory.OutOfBlocksException; +import com.sk89q.worldedit.extent.inventory.OutOfSpaceException; +import com.sk89q.worldedit.util.Location; +import com.sk89q.worldedit.world.block.BlockState; +import com.sk89q.worldedit.world.block.BlockType; + +import java.util.HashMap; +import java.util.Map; + +public class NukkitPlayerBlockBag extends BlockBag { + + private final Player player; + private Map items; + + public NukkitPlayerBlockBag(Player player) { + this.player = player; + } + + private void loadInventory() { + if (items == null) { + items = new HashMap<>(player.getInventory().getContents()); + } + } + + @Override + public void fetchBlock(BlockState blockState) throws BlockBagException { + if (blockState.getBlockType().getMaterial().isAir()) { + throw new IllegalArgumentException("Can't fetch air block"); + } + + loadInventory(); + + BlockType type = blockState.getBlockType(); + if (!type.hasItemType()) { + throw new OutOfBlocksException(); + } + ItemMapping.NukkitItemData beData = ItemMapping.jeToBe(type.getItemType().id()); + if (beData.itemId() == 0) { + throw new OutOfBlocksException(); + } + + for (Map.Entry entry : items.entrySet()) { + Item item = entry.getValue(); + if (item.getId() == beData.itemId() + && item.getDamage() == beData.metadata() + && item.getCount() > 0) { + if (item.getCount() == 1) { + items.put(entry.getKey(), Item.get(Item.AIR)); + } else { + item.setCount(item.getCount() - 1); + } + return; + } + } + throw new OutOfBlocksException(); + } + + @Override + public void storeBlock(BlockState blockState, int amount) throws BlockBagException { + if (blockState.getBlockType().getMaterial().isAir()) { + throw new IllegalArgumentException("Can't store air block"); + } + if (!blockState.getBlockType().hasItemType()) { + throw new IllegalArgumentException("This block cannot be stored"); + } + + loadInventory(); + + BlockType type = blockState.getBlockType(); + ItemMapping.NukkitItemData beData = ItemMapping.jeToBe(type.getItemType().id()); + + // Merge into existing stacks first + for (Map.Entry entry : items.entrySet()) { + if (amount <= 0) { + return; + } + Item item = entry.getValue(); + if (item.getId() == beData.itemId() + && item.getDamage() == beData.metadata() + && item.getCount() < item.getMaxStackSize()) { + int space = item.getMaxStackSize() - item.getCount(); + int add = Math.min(space, amount); + item.setCount(item.getCount() + add); + amount -= add; + } + } + + // Place into empty slots + for (int slot = 0; slot < 36 && amount > 0; slot++) { + Item item = items.get(slot); + if (item == null || item.isNull()) { + int stackSize = Math.min(amount, 64); + items.put(slot, Item.get(beData.itemId(), beData.metadata(), stackSize)); + amount -= stackSize; + } + } + + if (amount > 0) { + throw new OutOfSpaceException(blockState.getBlockType()); + } + } + + @Override + public void flushChanges() { + if (items != null) { + player.getInventory().setContents(items); + items = null; + } + } + + @Override + public void addSourcePosition(Location pos) { + } + + @Override + public void addSingleSourcePosition(Location pos) { + } + +} diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitEntity.java b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitEntity.java index 2726c1ac26..4d08bcc049 100644 --- a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitEntity.java +++ b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitEntity.java @@ -10,7 +10,6 @@ import com.sk89q.worldedit.world.NullWorld; import com.sk89q.worldedit.world.entity.EntityType; import com.sk89q.worldedit.world.entity.EntityTypes; -import org.enginehub.linbus.tree.LinCompoundTag; import javax.annotation.Nullable; import java.lang.ref.WeakReference; @@ -76,6 +75,7 @@ public BaseEntity getState() { return null; } + entity.saveNBT(); cn.nukkit.nbt.tag.CompoundTag namedTag = entity.namedTag; if (namedTag != null) { return new BaseEntity(type, LazyReference.computed( diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitPlayer.java b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitPlayer.java index 48806bdb77..b92f94cee3 100644 --- a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitPlayer.java +++ b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitPlayer.java @@ -1,7 +1,11 @@ package com.sk89q.worldedit.nukkitmot; +import cn.nukkit.AdventureSettings; import cn.nukkit.Player; import cn.nukkit.Server; +import cn.nukkit.level.Level; +import cn.nukkit.network.protocol.UpdateBlockPacket; +import com.fastasyncworldedit.nukkitmot.NukkitPlayerBlockBag; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.extension.platform.AbstractPlayerActor; @@ -21,7 +25,6 @@ import com.sk89q.worldedit.world.gamemode.GameModes; import javax.annotation.Nullable; -import java.io.File; import java.util.Locale; import java.util.UUID; @@ -113,7 +116,7 @@ public String[] getGroups() { @Override public BlockBag getInventoryBlockBag() { - return null; + return new NukkitPlayerBlockBag(player); } @Override @@ -164,7 +167,9 @@ public boolean isAllowedToFly() { @Override public void setFlying(boolean flying) { - player.setAllowFlight(flying); + player.getAdventureSettings().set(AdventureSettings.Type.ALLOW_FLIGHT, true); + player.getAdventureSettings().set(AdventureSettings.Type.FLYING, flying); + player.getAdventureSettings().update(); } @Override @@ -184,17 +189,12 @@ public boolean setLocation(Location location) { @Override public Locale getLocale() { - return Locale.getDefault(); - } - - @Override - public File openFileOpenDialog(String[] extensions) { - return null; - } - - @Override - public File openFileSaveDialog(String[] extensions) { - return null; + try { + String code = player.getLanguageCode().name(); // e.g. "zh_CN" + return Locale.forLanguageTag(code.replace('_', '-')); + } catch (Exception e) { + return Locale.getDefault(); + } } @Nullable @@ -210,7 +210,27 @@ public SessionKey getSessionKey() { @Override public > void sendFakeBlock(BlockVector3 pos, B block) { - // TODO: Implement fake block sending via UpdateBlockPacket + Level level = player.getLevel(); + if (block == null) { + // Restore real block + cn.nukkit.math.Vector3 vec = NukkitAdapter.adapt(pos); + level.sendBlocks(new Player[]{player}, new cn.nukkit.math.Vector3[]{vec}, + UpdateBlockPacket.FLAG_ALL); + } else { + int fullId = NukkitAdapter.adaptFullId(block.toImmutableState()); + int blockId = fullId >> cn.nukkit.block.Block.DATA_BITS; + int meta = fullId & cn.nukkit.block.Block.DATA_MASK; + + UpdateBlockPacket pk = new UpdateBlockPacket(); + pk.x = pos.x(); + pk.y = pos.y(); + pk.z = pos.z(); + pk.flags = UpdateBlockPacket.FLAG_ALL; + pk.blockRuntimeId = cn.nukkit.level.GlobalBlockPalette.getOrCreateRuntimeId( + player.getGameVersion(), blockId, meta + ); + player.dataPacket(pk); + } } @Override diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitWorld.java b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitWorld.java index d5d0796ed9..cb4b201348 100644 --- a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitWorld.java +++ b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitWorld.java @@ -16,13 +16,15 @@ import com.sk89q.worldedit.util.SideEffect; import com.sk89q.worldedit.util.SideEffectSet; import com.sk89q.worldedit.util.TreeGenerator; +import com.sk89q.worldedit.util.concurrency.LazyReference; import com.sk89q.worldedit.world.AbstractWorld; import com.sk89q.worldedit.world.biome.BiomeType; import com.sk89q.worldedit.world.biome.BiomeTypes; import com.sk89q.worldedit.world.block.BaseBlock; import com.sk89q.worldedit.world.block.BlockState; import com.sk89q.worldedit.world.block.BlockStateHolder; -import com.sk89q.worldedit.world.block.BlockTypes; +import com.sk89q.worldedit.world.weather.WeatherType; +import com.sk89q.worldedit.world.weather.WeatherTypes; import javax.annotation.Nullable; import java.lang.ref.WeakReference; @@ -84,7 +86,15 @@ public Set applySideEffects( @Override public BaseBlock getFullBlock(BlockVector3 position) { - return getBlock(position).toBaseBlock(); + BlockState state = getBlock(position); + Level level = getLevel(); + cn.nukkit.blockentity.BlockEntity be = level.getBlockEntity(NukkitAdapter.adapt(position)); + if (be != null && be.namedTag != null) { + return state.toBaseBlock(LazyReference.computed( + com.fastasyncworldedit.nukkitmot.NukkitNbtConverter.toLinCompound(be.namedTag) + )); + } + return state.toBaseBlock(); } @Override @@ -133,7 +143,7 @@ public void dropItem(com.sk89q.worldedit.math.Vector3 position, BaseItemStack it @Override public void simulateBlockMine(BlockVector3 position) { Level level = getLevel(); - level.setBlock(position.x(), position.y(), position.z(), Block.get(Block.AIR), true, true); + level.useBreakOn(NukkitAdapter.adapt(position)); } @Override @@ -283,6 +293,60 @@ public boolean tile(int x, int y, int z, FaweCompoundTag tile) throws WorldEditE return true; } + @Override + public WeatherType getWeather() { + Level level = getLevel(); + if (level.isThundering()) { + return WeatherTypes.THUNDER_STORM; + } else if (level.isRaining()) { + return WeatherTypes.RAIN; + } + return WeatherTypes.CLEAR; + } + + @Override + public long getRemainingWeatherDuration() { + Level level = getLevel(); + if (level.isThundering()) { + return level.getThunderTime(); + } else if (level.isRaining()) { + return level.getRainTime(); + } + return 0; + } + + @Override + public void setWeather(WeatherType weatherType) { + Level level = getLevel(); + if (weatherType == WeatherTypes.THUNDER_STORM) { + level.setRaining(true); + level.setThundering(true); + } else if (weatherType == WeatherTypes.RAIN) { + level.setRaining(true); + level.setThundering(false); + } else { + level.setRaining(false); + level.setThundering(false); + } + } + + @Override + public void setWeather(WeatherType weatherType, long duration) { + setWeather(weatherType); + Level level = getLevel(); + if (weatherType == WeatherTypes.THUNDER_STORM) { + level.setRainTime((int) duration); + level.setThunderTime((int) duration); + } else if (weatherType == WeatherTypes.RAIN) { + level.setRainTime((int) duration); + } + } + + @Override + public void checkLoadedChunk(BlockVector3 pt) { + getLevel().loadChunk(pt.x() >> 4, pt.z() >> 4, false); + } + @Override public void flush() { // No-op: Nukkit handles chunk flushing internally From cc047ddb395b0b759c01608b02ae0ffba8ca8730 Mon Sep 17 00:00:00 2001 From: LT_Name <572413378@qq.com> Date: Sun, 1 Mar 2026 14:40:27 +0800 Subject: [PATCH 04/12] fix(nukkit-mot): implement missing platform features and fix event hook guard --- .../sk89q/worldedit/nukkitmot/NukkitPlayer.java | 14 +++++++++++++- .../nukkitmot/NukkitWorldEditListener.java | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitPlayer.java b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitPlayer.java index b92f94cee3..5931abb9f5 100644 --- a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitPlayer.java +++ b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitPlayer.java @@ -5,6 +5,7 @@ import cn.nukkit.Server; import cn.nukkit.level.Level; import cn.nukkit.network.protocol.UpdateBlockPacket; +import cn.nukkit.permission.PermissionAttachment; import com.fastasyncworldedit.nukkitmot.NukkitPlayerBlockBag; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.entity.BaseEntity; @@ -31,6 +32,7 @@ public class NukkitPlayer extends AbstractPlayerActor { private final Player player; + private PermissionAttachment permAttachment; public NukkitPlayer(Player player) { this.player = player; @@ -147,7 +149,17 @@ public boolean hasPermission(String perm) { @Override public void setPermission(String permission, boolean value) { - // Nukkit doesn't have a simple runtime permission attachment API like Bukkit + if (permAttachment == null) { + permAttachment = player.addAttachment(WorldEditNukkitPlugin.getInstance()); + } + permAttachment.setPermission(permission, value); + } + + void removePermissionAttachment() { + if (permAttachment != null) { + permAttachment.remove(); + permAttachment = null; + } } @Override diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitWorldEditListener.java b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitWorldEditListener.java index e096ce7742..530a4f0f39 100644 --- a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitWorldEditListener.java +++ b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitWorldEditListener.java @@ -37,6 +37,10 @@ public NukkitWorldEditListener(WorldEditNukkitPlugin plugin) { @EventHandler(priority = EventPriority.NORMAL) public void onPlayerInteract(PlayerInteractEvent event) { + if (!plugin.getInternalPlatform().isHookingEvents()) { + return; + } + Player nukkitPlayer = event.getPlayer(); NukkitPlayer player = NukkitAdapter.adapt(nukkitPlayer); WorldEdit we = WorldEdit.getInstance(); @@ -93,6 +97,10 @@ public void onPlayerInteract(PlayerInteractEvent event) { */ @EventHandler(priority = EventPriority.LOW) public void onBlockBreak(BlockBreakEvent event) { + if (!plugin.getInternalPlatform().isHookingEvents()) { + return; + } + Player nukkitPlayer = event.getPlayer(); // Skip if already handled by PlayerInteractEvent(LEFT_CLICK_BLOCK) @@ -118,7 +126,13 @@ public void onBlockBreak(BlockBreakEvent event) { @EventHandler(priority = EventPriority.NORMAL) public void onPlayerQuit(PlayerQuitEvent event) { + if (!plugin.getInternalPlatform().isHookingEvents()) { + return; + } + Player nukkitPlayer = event.getPlayer(); + NukkitPlayer wePlayer = NukkitAdapter.adapt(nukkitPlayer); + wePlayer.removePermissionAttachment(); WorldEdit.getInstance().getEventBus().post( new SessionIdleEvent(new NukkitPlayer.SessionKeyImpl(nukkitPlayer)) ); From 230e4ff91a19a088a7a5ab9e0cc49cffb4909621 Mon Sep 17 00:00:00 2001 From: LT_Name <572413378@qq.com> Date: Sun, 1 Mar 2026 15:11:43 +0800 Subject: [PATCH 05/12] i18n: add Simplified Chinese translation Co-Authored-By: Claude Opus 4.6 --- .../src/main/resources/lang/zh/strings.json | 672 ++++++++++++++++++ 1 file changed, 672 insertions(+) create mode 100644 worldedit-core/src/main/resources/lang/zh/strings.json diff --git a/worldedit-core/src/main/resources/lang/zh/strings.json b/worldedit-core/src/main/resources/lang/zh/strings.json new file mode 100644 index 0000000000..e2104ade4c --- /dev/null +++ b/worldedit-core/src/main/resources/lang/zh/strings.json @@ -0,0 +1,672 @@ +{ + "prefix": "&8(&4&lFAWE&8)&7 {0}", + "fawe.worldedit.history.find.element": "&2{0} {1} &7前 &3{2}分钟 &6{3} &c/{4}", + "fawe.worldedit.history.find.element.more": " - 变更: {0}\n - 范围: {1} -> {2}\n - 附加: {3}\n - 磁盘大小: {4}", + "fawe.worldedit.history.find.hover": "更改了 {0} 个方块,点击查看更多信息", + "fawe.worldedit.history.distr.summary_null": "无法找到输入的编辑摘要。", + "fawe.info.lighting.propagate.selection": "已在 {0} 个区块中传播光照。", + "fawe.info.updated.lighting.selection": "已在 {0} 个区块中更新光照。(数据包发送可能需要一点时间)", + "fawe.info.set.region": "选区已设置为你当前允许的区域", + "fawe.info.worldedit.toggle.tips.on": "已禁用 FAWE 提示。", + "fawe.info.worldedit.toggle.tips.off": "已启用 FAWE 提示。", + "fawe.info.worldedit.bypassed": "当前已绕过 FAWE 限制。", + "fawe.info.worldedit.restricted": "你的 FAWE 编辑现在受到限制。", + "fawe.info.worldedit.oom.admin": "可选方案:\n - //fast\n - 进行较小的编辑\n - 分配更多内存\n - 禁用 `max-memory-percent`", + "fawe.info.temporarily-not-working": "暂时无法使用", + "fawe.info.light-blocks": "光源方块比光源更可靠,请使用方块。此命令已弃用,将在未来版本中移除。", + "fawe.info.update-available.build": "FastAsyncWorldEdit 有可用更新。你落后了 {0} 个版本。\n你当前运行的是版本 {1},最新版本是 {2}。\n在 {3} 更新", + "fawe.info.update-available.release": "FastAsyncWorldEdit 有新版本可用: {0}。你当前使用的是 {1}。从 {2} 或 {3} 下载。", + "fawe.web.generating.link": "正在上传 {0},请稍候...", + "fawe.web.generating.link.failed": "生成下载链接失败!", + "fawe.web.download.link": "{0}", + "fawe.web.image.load.timeout": "图片加载超时,最长时间: {0}秒。请尝试使用较小分辨率的图片。", + "fawe.web.image.load.size.too-large": "图片尺寸过大!最大允许尺寸(宽 x 高): {0} 像素。", + "fawe.worldedit.general.texture.disabled": "纹理已重置", + "fawe.worldedit.general.texture.set": "纹理已设置为 {1}", + "fawe.worldedit.general.source.mask.disabled": "全局源遮罩已禁用", + "fawe.worldedit.general.source.mask": "全局源遮罩已设置", + "fawe.worldedit.general.transform.disabled": "全局变换已禁用", + "fawe.worldedit.general.transform": "全局变换已设置", + "fawe.worldedit.copy.command.copy": "已复制 {0} 个方块。", + "fawe.worldedit.cut.command.cut.lazy": "粘贴时将移除 {0} 个方块", + "fawe.worldedit.paste.command.paste": "剪贴板已粘贴到 {0}", + "fawe.worldedit.history.command.undo.disabled": "撤销已禁用,使用: //fast", + "fawe.worldedit.selection.selection.count": "统计了 {0} 个方块。", + "fawe.worldedit.anvil.world.is.loaded": "执行时世界不应处于使用状态。卸载世界,或使用 -f 参数覆盖(请先保存)", + "fawe.worldedit.brush.brush.reset": "已重置你的笔刷。(SHIFT + 点击)", + "fawe.worldedit.brush.brush.none": "你没有持有笔刷!", + "fawe.worldedit.brush.brush.scroll.action.set": "滚动操作已设置为 {0}", + "fawe.worldedit.brush.brush.scroll.action.unset": "已移除滚动操作", + "fawe.worldedit.brush.brush.visual.mode.set": "可视模式已设置为 {0}", + "fawe.worldedit.brush.brush.target.mode.set": "目标模式已设置为 {0}", + "fawe.worldedit.brush.brush.target.offset.set": "目标偏移已设置为 {0}", + "fawe.worldedit.brush.brush.equipped": "已装备笔刷 {0}", + "fawe.worldedit.brush.brush.try.other": "还有其他更合适的笔刷,例如:\n - //br height [radius=5] [#clipboard|file=null] [rotation=0] [yscale=1.00]", + "fawe.worldedit.brush.brush.copy": "左键点击对象底部进行复制,右键点击进行粘贴。如有必要请增大笔刷半径。", + "fawe.worldedit.brush.brush.height.invalid": "无效的高度图文件 ({0})", + "fawe.worldedit.brush.brush.spline": "点击添加一个点,在同一位置点击以完成", + "fawe.worldedit.brush.brush.line.primary": "已添加点 {0},点击另一个位置创建线条", + "fawe.worldedit.brush.brush.catenary.direction": "已添加点 {0},点击你想创建曲线的方向", + "fawe.worldedit.brush.brush.line.secondary": "已创建曲线", + "fawe.worldedit.brush.spline.primary.2": "已添加位置,在同一位置点击以连接!", + "fawe.worldedit.brush.brush.spline.secondary.error": "设置的位置不够!", + "fawe.worldedit.brush.spline.secondary": "已创建曲线", + "fawe.worldedit.brush.brush.source.mask.disabled": "笔刷源遮罩已禁用", + "fawe.worldedit.brush.brush.source.mask": "笔刷源遮罩已设置", + "fawe.worldedit.brush.brush.transform.disabled": "笔刷变换已禁用", + "fawe.worldedit.brush.brush.transform": "笔刷变换已设置", + "fawe.worldedit.rollback.rollingback.index": "正在撤销 {0} ...", + "fawe.worldedit.rollback.rollback.element": "已撤销 {0}。", + "fawe.worldedit.tool.tool.inspect": "检查工具已绑定到 {0}。", + "fawe.worldedit.tool.tool.inspect.info": "{0} 在 {3} 前将 {1} 更改为 {2}", + "fawe.worldedit.tool.tool.inspect.info.footer": "总计: {0} 次更改", + "fawe.worldedit.tool.tool.range.error": "最大范围: {0}。", + "fawe.worldedit.tool.tool.lrbuild.info": "左键设置为 {0};右键设置为 {1}。", + "fawe.worldedit.utility.nothing.confirmed": "你没有待确认的操作。", + "fawe.worldedit.schematic.schematic.move.exists": "{0} 已存在", + "fawe.worldedit.schematic.schematic.move.success": "{0} -> {1}", + "fawe.worldedit.schematic.schematic.move.failed": "{0} 未移动: {1}", + "fawe.worldedit.schematic.schematic.loaded": "已加载 {0}。使用 //paste 粘贴", + "fawe.worldedit.schematic.schematic.saved": "已保存 {0}。", + "fawe.worldedit.schematic.schematic.none": "未找到文件。", + "fawe.worldedit.schematic.schematic.load-failure": "文件无法读取或不存在: {0}。如果你指定了格式,可能指定的格式不正确。Sponge schematic v2 和 v3 都使用 .schem 文件扩展名。要让 FAWE 自动选择格式,请不要指定格式。如果你使用的是 litematica 原理图,则不受支持!", + "fawe.worldedit.clipboard.clipboard.uri.not.found": "你未加载 {0}", + "fawe.worldedit.clipboard.clipboard.cleared": "剪贴板已清除", + "fawe.worldedit.clipboard.clipboard.invalid.format": "未知的剪贴板格式: {0}", + "fawe.worldedit.visitor.visitor.block": "影响了 {0} 个方块", + "fawe.worldedit.selector.selector.fuzzy.pos1": "区域已从 {0} {1} 设置并扩展。", + "fawe.worldedit.selector.selector.fuzzy.pos2": "已添加 {0} {1} 的扩展。", + "fawe.progress.progress.message": "{1}/{0} ({2}%) @{3}个/秒 剩余{4}秒", + "fawe.progress.progress.finished": "[ 完成! ]", + "fawe.error.command.syntax": "用法: {0}", + "fawe.error.no-perm": "你缺少权限节点: {0}", + "fawe.error.block.not.allowed": "你不被允许使用: {0}", + "fawe.error.setting.disable": "缺少设置: {0}", + "fawe.error.brush.not.found": "可用笔刷: {0}", + "fawe.error.brush.incompatible": "笔刷与此版本不兼容", + "fawe.error.no.region": "你没有当前允许的区域", + "fawe.error.player.not.found": "未找到玩家: {0}", + "fawe.error.worldedit.some.fails": "有 {0} 个方块未被放置,因为它们在你允许的区域之外。", + "fawe.error.worldedit.some.fails.blockbag": "缺少方块: {0}", + "fawe.error.mask.angle": "无法将角度与方块步长组合", + "fawe.error.invalid-flag": "标志 {0} 在此处不适用", + "fawe.error.lighting": "处理光照时出错。你可能需要重新加载区块才能看到编辑效果。", + "fawe.error.parser.invalid-data": "无效数据: {0}", + "fawe.error.unsupported": "不支持!", + "fawe.error.invalid-block-type": "不匹配有效的方块类型: {0}", + "fawe.error.invalid-block-state-property": "无法解析属性 `{1}` 的值 `{0}`,方块状态: `{2}`", + "fawe.error.nbt.forbidden": "你不被允许使用 NBT。缺少权限: {0}", + "fawe.error.invalid-arguments": "无效的参数数量。预期: {0}", + "fawe.error.unrecognised-tag": "无法识别的标签: {0} {1}", + "fawe.error.unknown-block-tag": "未知的方块标签: {0}", + "fawe.error.block-tag-no-blocks": "方块标签 '{0}' 没有方块。", + "fawe.error.no-block-found": "未找到 '{0}' 对应的方块。", + "fawe.error.invalid-states": "无效状态: {0}", + "fawe.error.no-session": "没有可用的会话,因此没有可用的剪贴板。", + "fawe.error.empty-clipboard": "要使用 '{0}',请先将内容复制到剪贴板", + "fawe.error.selection-expand": "选区无法扩展。", + "fawe.error.selection-contract": "选区无法收缩。", + "fawe.error.selection-shift": "选区无法移动。", + "fawe.error.invalid-user": "必须提供用户。", + "fawe.error.radius-too-small": "半径必须 >=0", + "fawe.error.time-too-less": "时间必须 >=0", + "fawe.error.invalid-image": "无效图片: {0}", + "fawe.error.image-dimensions": "给定的图片尺寸过大,最大允许尺寸(宽 x 高): {0} 像素。", + "fawe.error.file-not-found": "未找到文件: {0}", + "fawe.error.file-is-invalid-directory": "文件是目录: {0}", + "fawe.error.stacktrace": "===============---=============", + "fawe.error.no-failure": "这不应该导致任何失败", + "fawe.error.invalid-bracketing": "无效的括号,你是否缺少 '{0}'。", + "fawe.error.too-simple": "复杂度必须在 0-100 范围内", + "fawe.error.outside-range": "参数 {0} 超出范围 {1}-{2}。", + "fawe.error.outside-range-lower": "参数 {0} 不能小于 {1}", + "fawe.error.outside-range-upper": "参数 {0} 不能大于 {1}", + "fawe.error.argument-size-mismatch": "参数 {0} 不能大于参数 {1}", + "fawe.error.input-parser-exception": "无效的空字符串代替布尔值。", + "fawe.error.invalid-boolean": "无效的布尔值 {0}", + "fawe.error.schematic.not.found": "未找到原理图 {0}。", + "fawe.error.parse.invalid-dangling-character": "无效的悬挂字符 {0}。", + "fawe.error.parse.unknown-mask": "未知遮罩: {0},参见: {1}", + "fawe.error.parse.unknown-pattern": "未知图案: {0},参见: {1}", + "fawe.error.parse.unknown-transform": "未知变换: {0},参见: {1}", + "fawe.error.parse.no-clipboard": "要使用 {0},请先将内容复制到剪贴板", + "fawe.error.parse.no-clipboard-source": "在给定的源中未找到剪贴板: {0}", + "fawe.error.clipboard.invalid": "====== 无效的剪贴板 ======", + "fawe.error.clipboard.invalid.info": "文件: {0} (长度: {1})", + "fawe.error.clipboard.load.failure": "从磁盘加载剪贴板时发生意外错误!", + "fawe.error.clipboard.on.disk.version.mismatch": "剪贴板版本不匹配: 预期 {0} 但得到 {1}。建议删除剪贴板文件夹并重启服务器。\n你的剪贴板文件夹位于 {2}。", + "fawe.error.limit.disallowed-block": "你的限制不允许使用方块 '{0}'", + "fawe.error.limit.disallowed-property": "你的限制不允许使用属性 '{0}'", + "fawe.error.region-mask-invalid": "无效的区域遮罩: {0}", + "fawe.error.occurred-continuing": "编辑期间发生可忽略的错误: {0}", + "fawe.error.limit.max-brush-radius": "限制中的最大笔刷半径: {0}", + "fawe.error.limit.max-radius": "限制中的最大半径: {0}", + "fawe.error.no-valid-on-hotbar": "快捷栏上没有有效的方块类型", + "fawe.error.no-process-non-synchronous-edit": "未找到处理器持有者,但编辑是非同步的", + "fawe.cancel.count": "已取消 {0} 个编辑。", + "fawe.cancel.reason.confirm": "使用 //confirm 执行 {0}", + "fawe.cancel.reason.confirm.region": "你的选区很大 ({0} -> {1},包含 {3} 个方块)。使用 //confirm 执行 {2}", + "fawe.cancel.reason.confirm.radius": "你的半径很大 ({0} > {1})。使用 //confirm 执行 {2}", + "fawe.cancel.reason.confirm.limit": "你超出了此操作的限制 ({0} > {1})。使用 //confirm 执行 {2}", + "fawe.cancel.reason": "你的 WorldEdit 操作已被取消: {0}。", + "fawe.cancel.reason.manual": "手动取消", + "fawe.cancel.reason.low.memory": "内存不足", + "fawe.cancel.reason.max.changes": "更改的方块过多", + "fawe.cancel.reason.max.checks": "检查的方块过多", + "fawe.cancel.reason.max.fails": "失败次数过多", + "fawe.cancel.reason.max.tiles": "方块实体过多", + "fawe.cancel.reason.max.entities": "实体过多", + "fawe.cancel.reason.max.iterations": "达到最大迭代次数", + "fawe.cancel.reason.outside.level": "超出世界范围", + "fawe.cancel.reason.outside.region": "超出允许区域(使用 /wea 绕过,或在 config.yml 中禁用 `region-restrictions`)", + "fawe.cancel.reason.outside.safe.region": "超出 +/- 30,000,000 方块的安全编辑区域。", + "fawe.cancel.reason.no.region": "没有允许的区域(使用 /wea 绕过,或在 config.yml 中禁用 `region-restrictions`)", + "fawe.cancel.reason.no.region.reason": "没有允许的区域: {0}", + "fawe.cancel.reason.no.region.plot.noworldeditflag": "地皮标志 NoWorldeditFlag 已设置", + "fawe.cancel.reason.no.region.plot.owner.offline": "区域所有者离线", + "fawe.cancel.reason.no.region.plot.owner.only": "只有区域所有者可以编辑", + "fawe.cancel.reason.no.region.not.added": "未被添加到区域", + "fawe.cancel.reason.player-only": "此操作需要玩家,不能从控制台执行,也不能没有操作者。", + "fawe.cancel.reason.actor-required": "此操作需要一个操作者。", + "fawe.cancel.reason.world.limit": "此操作无法在 y={0} 处执行,因为超出了世界限制。", + "fawe.cancel.worldedit.failed.load.chunk": "跳过加载区块: {0};{1}。尝试增加 chunk-wait。", + "fawe.navigation.no.block": "视线内没有方块!(或太远)", + "fawe.selection.sel.max": "最多 {0} 个点。", + "fawe.selection.sel.fuzzy": "模糊选择器: 左键点击选择所有相邻方块,右键点击添加。要选择空气空腔,请使用 //pos1。", + "fawe.selection.sel.fuzzy-instruction": "选择所有相连的方块(魔术棒)", + "fawe.selection.sel.convex.polyhedral": "凸多面体选择器: 左键=第一个顶点,右键添加更多。", + "fawe.selection.sel.polyhedral": "选择一个空心多面体", + "fawe.selection.sel.list": "要查看选区类型列表,请使用: //sel list", + "fawe.tips.tip.sel.list": "提示: 使用 //sel list 查看不同的选区模式", + "fawe.tips.tip.select.connected": "提示: 使用 //sel fuzzy 选择所有相连的方块", + "fawe.tips.tip.set.pos1": "提示: 使用 //set pos1 将 pos1 用作图案", + "fawe.tips.tip.farwand": "提示: 使用 //farwand 选择远处的点", + "fawe.tips.tip.discord": "需要 FAWE 使用帮助?https://discord.gg/intellectualsites", + "fawe.tips.tip.lazycut": "提示: 使用 //lazycut 更安全", + "fawe.tips.tip.fast": "提示: 使用 //fast 设置快速模式且不记录撤销", + "fawe.tips.tip.cancel": "提示: 你可以使用 //cancel 取消正在进行的编辑", + "fawe.tips.tip.mask": "提示: 使用 /gmask 设置全局目标遮罩", + "fawe.tips.tip.mask.angle": "提示: 使用 //replace /[-20][-3] bedrock 替换 3-20 方块的上坡", + "fawe.tips.tip.set.linear": "提示: 使用 //set #l3d[wood,bedrock] 线性设置方块", + "fawe.tips.tip.surface.spread": "提示: 使用 //set #surfacespread[5][0][5][#existing] 扩展平面", + "fawe.tips.tip.set.hand": "提示: 使用 //set hand 使用你当前手持的方块", + "fawe.tips.tip.replace.regex": "提示: 使用正则替换: //replace .*_log ", + "fawe.tips.tip.replace.regex.2": "提示: 使用正则替换: //replace .*stairs[facing=(north|south)] ", + "fawe.tips.tip.replace.regex.3": "提示: 使用运算符替换: //replace water[level>2] sand", + "fawe.tips.tip.replace.regex.4": "提示: 使用运算符替换: //replace true *[waterlogged=false]", + "fawe.tips.tip.replace.regex.5": "提示: 使用运算符替换: //replace true *[level-=1]", + "fawe.tips.tip.replace.id": "提示: 仅替换方块ID: //replace woodenstair #id[cobblestair]", + "fawe.tips.tip.replace.light": "提示: 使用 //replace #brightness[1][15] 0 移除光源", + "fawe.tips.tip.tab.complete": "提示: 替换命令支持 Tab 补全", + "fawe.tips.tip.flip": "提示: 使用 //flip 镜像", + "fawe.tips.tip.deform": "提示: 使用 //deform 变形", + "fawe.tips.tip.transform": "提示: 使用 //gtransform 设置变换", + "fawe.tips.tip.copypaste": "提示: 使用 //br copypaste 点击粘贴", + "fawe.tips.tip.source.mask": "提示: 使用 /gsmask 设置源遮罩", + "fawe.tips.tip.replace.marker": "提示: 使用 //replace wool #fullcopy 将方块替换为完整剪贴板", + "fawe.tips.tip.paste": "提示: 使用 //paste 放置", + "fawe.tips.tip.lazycopy": "提示: lazycopy 更快", + "fawe.tips.tip.download": "提示: 试试 //download", + "fawe.tips.tip.rotate": "提示: 使用 //rotate 旋转", + "fawe.tips.tip.copy.pattern": "提示: 要用作图案请尝试 #copy", + "fawe.tips.tip.regen.0": "提示: 使用 /regen [biome] 配合生物群系", + "fawe.tips.tip.regen.1": "提示: 使用 /regen [biome] [seed] 配合种子", + "fawe.tips.tip.biome.pattern": "提示: #biome[forest] 图案可用于任何命令", + "fawe.tips.tip.biome.mask": "提示: 使用 `$jungle` 遮罩限制到特定生物群系", + "fawe.regen.time": "正在重新生成区域,这可能需要一些时间!", + "worldedit.expand.description.vert": "将选区垂直扩展到世界限制。", + "worldedit.expand.expanded": "区域扩展了 {0} 个方块", + "worldedit.expand.expanded.vert": "区域扩展了 {0} 个方块(从顶到底)。", + "worldedit.biomeinfo.lineofsight": "视线所及位置的生物群系: {0}", + "worldedit.biomeinfo.position": "你所在位置的生物群系: {0}", + "worldedit.biomeinfo.selection": "你选区中的生物群系: {0}", + "worldedit.biomeinfo.not-locatable": "命令发送者必须在世界中才能使用 -p 标志。", + "worldedit.error.disabled": "此功能已禁用(参见 WorldEdit 配置)。", + "worldedit.error.no-match": "没有匹配 '{0}' 的结果。", + "worldedit.error.unknown": "发生未知错误: {0}", + "worldedit.error.parser.player-only": "输入 '{0}' 需要一个玩家!", + "worldedit.error.parser.bad-state-format": "{0} 中的状态格式错误", + "worldedit.error.parser.unknown-property": "方块 '{1}' 的未知属性 '{0}'", + "worldedit.error.parser.duplicate-property": "重复属性: {0}", + "worldedit.error.parser.unknown-value": "属性 '{1}' 的未知值 '{0}'", + "worldedit.error.parser.invalid-colon": "无效的冒号。", + "worldedit.error.parser.hanging-lbracket": "无效格式。'{0}' 处有悬挂的括号。", + "worldedit.error.parser.missing-rbracket": "状态缺少尾部 ']'", + "worldedit.error.incomplete-region": "请先创建一个区域选区。", + "worldedit.error.not-a-block": "此物品不是方块。", + "worldedit.error.unknown-entity": "实体名称 '{0}' 未被识别。", + "worldedit.error.unknown-mob": "生物名称 '{0}' 未被识别。", + "worldedit.error.parser.clipboard.missing-offset": "使用 @ 指定了偏移但未给出偏移值。使用 '#copy@[x,y,z]'。", + "worldedit.error.parser.clipboard.missing-coordinates": "剪贴板偏移需要 x,y,z 坐标。", + "worldedit.error.unknown-item": "物品名称 '{0}' 未被识别。", + "worldedit.error.parser.invalid-expression": "无效的表达式: {0}", + "worldedit.error.parser.negate-nothing": "无法对空内容取反!", + "worldedit.error.invalid-page": "无效的页码", + "worldedit.error.missing-extent": "未知的 Extent", + "worldedit.error.missing-session": "未知的 LocalSession", + "worldedit.error.missing-world": "你需要提供一个世界(尝试 //world)", + "worldedit.error.missing-actor": "未知的操作者", + "worldedit.error.missing-player": "未知的玩家", + "worldedit.error.no-file-selected": "未选择文件。", + "worldedit.error.file-resolution.outside-root": "路径在允许的根目录之外", + "worldedit.error.file-resolution.resolve-failed": "解析路径失败", + "worldedit.error.invalid-filename.invalid-characters": "无效字符或缺少扩展名", + "worldedit.error.invalid-number.matches": "需要数字;给定的是字符串 \"{0}\"。", + "worldedit.error.invalid-number": "需要数字;给定的是字符串。", + "worldedit.error.unknown-block": "方块名称 '{0}' 未被识别。", + "worldedit.error.disallowed-block": "方块 '{0}' 不被允许(参见 WorldEdit 配置)。", + "worldedit.error.max-changes": "操作中达到最大方块更改数 ({0})。", + "worldedit.error.max-brush-radius": "最大笔刷半径(在 worldedit-config.yml 中): {0}", + "worldedit.error.max-radius": "最大半径(在 worldedit-config.yml 中): {0}", + "worldedit.error.unknown-direction": "未知方向: {0}", + "worldedit.error.empty-clipboard": "你的剪贴板为空。请先使用 //copy。", + "worldedit.error.invalid-filename": "文件名 '{0}' 无效: {1}", + "worldedit.error.file-resolution": "文件 '{0}' 解析错误: {1}", + "worldedit.tool.error.cannot-bind": "无法将工具绑定到 {0}: {1}", + "worldedit.error.file-aborted": "文件选择已中止。", + "worldedit.error.world-unloaded": "世界已被卸载。", + "worldedit.error.named-world-unloaded": "世界 '{0}' 已被卸载。", + "worldedit.error.blocks-cant-be-used": "无法使用方块", + "worldedit.error.unknown-tag": "标签名称 '{0}' 未被识别。", + "worldedit.error.empty-tag": "标签名称 '{0}' 没有内容。", + "worldedit.error.unknown-biome": "生物群系名称 '{0}' 未被识别。", + "worldedit.brush.radius-too-large": "最大允许笔刷半径: {0}", + "worldedit.brush.apply.description": "应用笔刷,将功能应用到每个方块", + "worldedit.brush.apply.radius": "笔刷大小", + "worldedit.brush.apply.shape": "区域形状", + "worldedit.brush.apply.type": "要使用的笔刷类型", + "worldedit.brush.apply.item.warning": "此笔刷模拟物品使用。其效果可能不适用于所有平台,可能无法撤销,并可能与其他模组/插件产生奇怪的交互。使用风险自负。", + "worldedit.brush.paint.description": "绘画笔刷,将功能应用到表面", + "worldedit.brush.paint.size": "笔刷大小", + "worldedit.brush.paint.shape": "区域形状", + "worldedit.brush.paint.density": "笔刷密度", + "worldedit.brush.paint.type": "要使用的笔刷类型", + "worldedit.brush.paint.item.warning": "此笔刷模拟物品使用。其效果可能不适用于所有平台,可能无法撤销,并可能与其他模组/插件产生奇怪的交互。使用风险自负。", + "worldedit.brush.sphere.equip": "球体笔刷已装备 ({0})。", + "worldedit.brush.cylinder.equip": "圆柱笔刷已装备 ({0} x {1})。", + "worldedit.brush.clipboard.equip": "剪贴板笔刷已装备。", + "worldedit.brush.smooth.equip": "平滑笔刷已装备 ({0} x {1}次,使用 {2})。", + "worldedit.brush.smooth.nofilter": "任何方块", + "worldedit.brush.smooth.filter": "过滤器", + "worldedit.brush.snowsmooth.equip": "雪地平滑笔刷已装备 ({0} x {1}次,使用 {2}),{3} 个雪方块。", + "worldedit.brush.snowsmooth.nofilter": "任何方块", + "worldedit.brush.snowsmooth.filter": "过滤器", + "worldedit.brush.extinguish.equip": "灭火器已装备 ({0})。", + "worldedit.brush.gravity.equip": "重力笔刷已装备 ({0})。", + "worldedit.brush.butcher.equip": "屠杀笔刷已装备 ({0})。", + "worldedit.brush.operation.equip": "笔刷设置为 {0}。", + "worldedit.brush.morph.equip": "变形笔刷已装备: {0}。", + "worldedit.brush.biome.column-supported-types": "此笔刷形状不支持整列刷涂,请尝试圆柱形状。", + "worldedit.brush.none.equip": "笔刷已从当前物品解绑。", + "worldedit.brush.none.equipped": "你的当前物品没有绑定笔刷。尝试 /brush sphere 获取基本笔刷。", + "worldedit.setbiome.changed": "已在 {0} 列中更改生物群系。你可能需要重新加入游戏(或关闭并重新打开世界)才能看到变化。", + "worldedit.setbiome.warning": "你可能需要重新加入游戏(或关闭并重新打开世界)才能看到变化。", + "worldedit.setbiome.not-locatable": "命令发送者必须在世界中才能使用 -p 标志。", + "worldedit.drawsel.disabled": "服务端 CUI 已禁用。", + "worldedit.drawsel.enabled": "服务端 CUI 已启用。仅支持长方体区域,最大尺寸为 {0}x{1}x{2}。", + "worldedit.drawsel.disabled.already": "服务端 CUI 已经是禁用状态。", + "worldedit.drawsel.enabled.already": "服务端 CUI 已经是启用状态。", + "worldedit.limit.too-high": "你允许的最大限制是 {0}。", + "worldedit.limit.set": "方块更改限制设置为 {0}。", + "worldedit.limit.return-to-default": "(使用 //limit 恢复默认值。)", + "worldedit.timeout.too-high": "你允许的最大超时时间是 {0}毫秒。", + "worldedit.timeout.set": "超时时间设置为 {0}毫秒。", + "worldedit.timeout.return-to-default": " (使用 //timeout 恢复默认值。)", + "worldedit.fast.disabled": "快速模式已禁用。", + "worldedit.fast.enabled": "快速模式已启用。更改将不会写入历史记录(//undo 被禁用)。受影响区块的光照可能不正确,且/或你可能需要重新加入才能看到更改。", + "worldedit.fast.disabled.already": "快速模式已经是禁用状态。", + "worldedit.fast.enabled.already": "快速模式已经是启用状态。", + "worldedit.perf.sideeffect.set": "副作用 \"{0}\" 设置为 {1}", + "worldedit.perf.sideeffect.get": "副作用 \"{0}\" 当前为 {1}", + "worldedit.perf.sideeffect.already-set": "副作用 \"{0}\" 已经是 {1}", + "worldedit.perf.sideeffect.set-all": "所有副作用设置为 {0}", + "worldedit.reorder.current": "当前重排序模式为 {0}", + "worldedit.reorder.set": "重排序模式已设置为 {0}", + "worldedit.gmask.disabled": "全局遮罩已禁用。", + "worldedit.gmask.set": "全局遮罩已设置。", + "worldedit.toggleplace.pos1": "现在放置在位置 #1。", + "worldedit.toggleplace.player": "现在放置在你站立的方块处。", + "worldedit.toggleplace.not-locatable": "在此上下文中无法切换放置位置。", + "worldedit.searchitem.too-short": "请输入更长的搜索字符串(长度 > 2)。", + "worldedit.searchitem.either-b-or-i": "不能同时使用 'b' 和 'i' 标志。", + "worldedit.searchitem.searching": "(请稍候...正在搜索物品。)", + "worldedit.watchdog.no-hook": "此平台没有看门狗钩子。", + "worldedit.watchdog.active.already": "看门狗钩子已经处于激活状态。", + "worldedit.watchdog.inactive.already": "看门狗钩子已经处于非激活状态。", + "worldedit.watchdog.active": "看门狗钩子已激活。", + "worldedit.watchdog.inactive": "看门狗钩子已停用。", + "worldedit.world.remove": "已移除世界覆盖。", + "worldedit.world.set": "世界覆盖已设置为 {0}。(使用 //world 恢复默认)", + "worldedit.undo.undone": "已撤销 {0} 个可用编辑。", + "worldedit.undo.none": "没有可以撤销的内容。", + "worldedit.redo.redone": "已重做 {0} 个可用编辑。", + "worldedit.redo.none": "没有可以重做的内容。", + "worldedit.clearhistory.cleared": "历史记录已清除。", + "worldedit.raytrace.noblock": "视线内没有方块!", + "worldedit.raytrace.require-player": "射线追踪命令需要一个玩家!", + "worldedit.restore.not-configured": "快照/备份还原未配置。", + "worldedit.restore.not-available": "该快照不存在或不可用。", + "worldedit.restore.failed": "加载快照失败: {0}", + "worldedit.restore.loaded": "快照 '{0}' 已加载;正在还原...", + "worldedit.restore.restored": "已还原;{0} 个缺失区块和 {1} 个其他错误。", + "worldedit.restore.none-for-specific-world": "未找到世界 '{0}' 的快照。", + "worldedit.restore.none-for-world": "未找到此世界的快照。", + "worldedit.restore.none-found": "未找到快照。", + "worldedit.restore.none-found-console": "未找到快照。详情请查看控制台。", + "worldedit.restore.chunk-not-present": "区块不存在于快照中。", + "worldedit.restore.chunk-load-failed": "无法加载任何区块。(存档损坏?)", + "worldedit.restore.block-place-failed": "错误导致无法还原任何方块。", + "worldedit.restore.block-place-error": "最后一个错误: {0}", + "worldedit.snapshot.use.newest": "现在使用最新的快照。", + "worldedit.snapshot.use": "快照设置为: {0}", + "worldedit.snapshot.none-before": "找不到 {0} 之前的快照。", + "worldedit.snapshot.none-after": "找不到 {0} 之后的快照。", + "worldedit.snapshot.index-above-0": "无效索引,必须大于或等于 1。", + "worldedit.snapshot.index-oob": "无效索引,必须在 1 到 {0} 之间。", + "worldedit.schematic.unknown-format": "未知的原理图格式: {0}。", + "worldedit.schematic.load.does-not-exist": "原理图 {0} 不存在!", + "worldedit.schematic.load.loading": "(请稍候...正在加载原理图。)", + "worldedit.schematic.load.unsupported-version": "不支持此原理图。版本: {0}。如果你使用的是 litematica 原理图,则不受支持!", + "worldedit.schematic.save.already-exists": "该原理图已存在。使用 -f 标志覆盖。", + "worldedit.schematic.save.failed-directory": "无法创建原理图文件夹!", + "worldedit.schematic.save.saving": "(请稍候...正在保存原理图。)", + "worldedit.schematic.save.still-saving": "(请稍候...仍在保存原理图。)", + "worldedit.schematic.share.unsupported-format": "原理图分享目标 \"{0}\" 不支持 \"{1}\" 格式。", + "worldedit.schematic.share.response.arkitektonika.download": "下载: {0}", + "worldedit.schematic.share.response.arkitektonika.delete": "删除: {0}", + "worldedit.schematic.share.response.arkitektonika.click-here": "[点击这里]", + "worldedit.schematic.delete.empty": "未找到原理图 {0}!", + "worldedit.schematic.delete.does-not-exist": "原理图 {0} 不存在!", + "worldedit.schematic.delete.failed": "删除 {0} 失败!是否为只读?", + "worldedit.schematic.delete.deleted": "{0} 已被删除。", + "worldedit.schematic.formats.title": "可用的剪贴板格式(名称: 查找名称)", + "worldedit.schematic.load.symbol": "[加载]", + "worldedit.schematic.plus.symbol": "[+]", + "worldedit.schematic.minus.symbol": "[-]", + "worldedit.schematic.x.symbol": "[X]", + "worldedit.schematic.0.symbol": "[O]", + "worldedit.schematic.dash.symbol": " - ", + "worldedit.schematic.click-to-load": "点击加载", + "worldedit.schematic.load": "加载", + "worldedit.schematic.list": "列表", + "worldedit.schematic.available": "可用原理图", + "worldedit.schematic.unload": "卸载", + "worldedit.schematic.delete": "删除", + "worldedit.schematic.visualize": "可视化", + "worldedit.schematic.clipboard": "添加到(多重)剪贴板", + "worldedit.schematic.unknown-filename": "未知文件名: {0}", + "worldedit.schematic.file-not-exist": "文件无法读取或不存在: {0}", + "worldedit.schematic.already-exists": "该原理图已存在!", + "worldedit.schematic.failed-to-save": "保存原理图失败", + "worldedit.schematic.directory-does-not-exist": "目录 '{0}' 不存在!", + "worldedit.schematic.file-perm-fail": "创建 '{0}' 失败!请检查文件权限。", + "worldedit.schematic.sorting-old-new": "不能同时按最旧和最新排序。", + "worldedit.schematic.unsupported-minecraft-version": "此版本的 WorldEdit 不支持你的 Minecraft 版本。在解决此问题之前,原理图将无法使用。", + "worldedit.pos.already-set": "位置已设置。", + "worldedit.pos.console-require-coords": "在控制台中必须提供坐标。", + "worldedit.hpos.no-block": "视线内没有方块!", + "worldedit.hpos.already-set": "位置已设置。", + "worldedit.chunk.selected-multiple": "已选区块: ({0}, {1}, {2}) - ({3}, {4}, {5})", + "worldedit.chunk.selected": "已选区块: {0}, {1}, {2}", + "worldedit.wand.invalid": "魔杖物品配置错误或已禁用。", + "worldedit.wand.selwand.info": "左键: 选择位置 #1;右键: 选择位置 #2", + "worldedit.wand.selwand.now.tool": "选择魔杖现在是普通工具。你可以使用 {0} 禁用它,使用 {1} 重新绑定到任何物品,或使用 {2} 获取新魔杖。", + "worldedit.wand.navwand.info": "左键: 跳转到位置;右键: 穿过墙壁", + "worldedit.contract.contracted": "区域收缩了 {0} 个方块。", + "worldedit.shift.shifted": "区域已移动。", + "worldedit.outset.outset": "区域已外扩。", + "worldedit.inset.inset": "区域已内缩。", + "worldedit.trim.trim": "区域已裁剪。", + "worldedit.trim.no-blocks": "没有方块匹配裁剪遮罩。", + "worldedit.size.offset": "{0}: {1} @ {2} ({3} 个方块)", + "worldedit.size.type": "类型: {0}", + "worldedit.size.size": "大小: {0}", + "worldedit.size.distance": "长方体距离: {0}", + "worldedit.size.blocks": "方块数量: {0}", + "worldedit.count.counted": "统计: {0}", + "worldedit.distr.no-blocks": "没有统计到方块。", + "worldedit.distr.no-previous": "没有之前的分布。", + "worldedit.distr.total": "方块总数: {0}", + "worldedit.select.cleared": "选区已清除。", + "worldedit.select.cuboid.message": "长方体: 左键选择点 1,右键选择点 2", + "worldedit.select.cuboid.description": "选择长方体的两个角", + "worldedit.select.extend.message": "长方体: 左键设置起始点,右键扩展", + "worldedit.select.extend.description": "快速长方体选择模式", + "worldedit.select.poly.message": "二维多边形选择器: 左键/右键添加一个点。", + "worldedit.select.poly.limit-message": "最多 {0} 个点。", + "worldedit.select.poly.description": "选择带高度的二维多边形", + "worldedit.select.ellipsoid.message": "椭球选择器: 左键=中心,右键扩展", + "worldedit.select.ellipsoid.description": "选择一个椭球", + "worldedit.select.sphere.message": "球体选择器: 左键=中心,右键设置半径", + "worldedit.select.sphere.description": "选择一个球体", + "worldedit.select.cyl.message": "圆柱选择器: 左键=中心,右键扩展", + "worldedit.select.cyl.description": "选择一个圆柱", + "worldedit.select.convex.message": "凸多面体选择器: 左键=第一个顶点,右键添加更多。", + "worldedit.select.convex.limit-message": "最多 {0} 个点。", + "worldedit.select.convex.description": "选择一个凸多面体", + "worldedit.select.default-set": "你的默认区域选择器现在是 {0}。", + "worldedit.chunkinfo.chunk": "区块: {0}, {1}", + "worldedit.chunkinfo.old-filename": "旧格式: {0}", + "worldedit.chunkinfo.mcregion-filename": "McRegion: region/{0}", + "worldedit.listchunks.listfor": "列出以下区块: {0}", + "worldedit.drain.drained": "已排干 {0} 个方块。", + "worldedit.fill.created": "已填充 {0} 个方块。", + "worldedit.fillr.created": "已递归填充 {0} 个方块。", + "worldedit.fixlava.fixed": "已修复 {0} 个方块。", + "worldedit.fixwater.fixed": "已修复 {0} 个方块。", + "worldedit.removeabove.removed": "已移除 {0} 个方块。", + "worldedit.removebelow.removed": "已移除 {0} 个方块。", + "worldedit.removenear.removed": "已移除 {0} 个方块。", + "worldedit.replacenear.replaced": "已替换 {0} 个方块。", + "worldedit.snow.created": "已覆盖 {0} 个表面。", + "worldedit.thaw.removed": "已融化 {0} 个方块。", + "worldedit.green.changed": "已绿化 {0} 个方块。", + "worldedit.extinguish.removed": "已熄灭 {0} 处火焰。", + "worldedit.butcher.killed": "在半径 {1} 内已杀死 {0} 个生物。", + "worldedit.butcher.explain-all": "使用 -1 移除已加载区块中的所有生物", + "worldedit.remove.removed": "已标记移除 {0} 个实体。", + "worldedit.remove.explain-all": "使用 -1 移除已加载区块中的所有实体", + "worldedit.calc.invalid": "'{0}' 无法被解析为有效的表达式", + "worldedit.calc.invalid.with-error": "'{0}' 无法被解析为有效的表达式: '{1}'", + "worldedit.paste.pasted": "剪贴板已粘贴到 {0}", + "worldedit.paste.selected": "已选择剪贴板粘贴区域。", + "worldedit.rotate.no-interpolation": "注意: 尚不支持插值,因此建议使用 90 的倍数角度。", + "worldedit.rotate.rotated": "剪贴板副本已旋转。", + "worldedit.flip.flipped": "剪贴板副本已翻转。", + "worldedit.clearclipboard.cleared": "剪贴板已清除。", + "worldedit.set.done": "操作完成 ({0})。", + "worldedit.set.done.verbose": "操作完成 ({0})。", + "worldedit.line.changed": "已更改 {0} 个方块。", + "worldedit.line.invalid-type": "//line 仅适用于长方体选区或凸多面体选区", + "worldedit.line.cuboid-only": "//line 仅适用于长方体选区", + "worldedit.curve.changed": "已更改 {0} 个方块。", + "worldedit.curve.invalid-type": "//curve 仅适用于凸多面体选区", + "worldedit.curve.convex-only": "//curve 仅适用于凸多面体选区", + "worldedit.replace.replaced": "已替换 {0} 个方块。", + "worldedit.stack.changed": "{0} 个方块已更改。使用 //undo 撤销", + "worldedit.stack.intersecting-region": "使用方块单位时,堆叠偏移不能与区域碰撞", + "worldedit.regen.regenerated": "区域已重新生成。", + "worldedit.regen.failed": "无法重新生成区块。详情请查看控制台。", + "worldedit.walls.changed": "已更改 {0} 个方块。", + "worldedit.faces.changed": "已更改 {0} 个方块。", + "worldedit.overlay.overlaid": "已覆盖 {0} 个方块。", + "worldedit.naturalize.naturalized": "已将 {0} 个方块自然化。", + "worldedit.center.changed": "中心已设置。({0} 个方块已更改)", + "worldedit.smooth.changed": "地形高度图已平滑。{0} 个方块已更改。", + "worldedit.snowsmooth.changed": "雪地高度图已平滑。{0} 个方块已更改。", + "worldedit.move.moved": "已移动 {0} 个方块。", + "worldedit.deform.deformed": "已变形 {0} 个方块。", + "worldedit.hollow.changed": "已更改 {0} 个方块。", + "worldedit.forest.created": "已创建 {0} 棵树。", + "worldedit.flora.created": "已创建 {0} 个植物。", + "worldedit.unstuck.moved": "你自由了!", + "worldedit.ascend.obstructed": "上方没有找到空闲位置。", + "worldedit.ascend.moved": "上升了 {0} 层。", + "worldedit.descend.obstructed": "下方没有找到空闲位置。", + "worldedit.descend.moved": "下降了 {0} 层。", + "worldedit.ceil.obstructed": "上方没有找到空闲位置。", + "worldedit.ceil.moved": "嗖!", + "worldedit.thru.obstructed": "前方没有找到空闲位置。", + "worldedit.thru.moved": "嗖!", + "worldedit.jumpto.moved": "噗!", + "worldedit.jumpto.none": "视线内没有方块(或太远)!", + "worldedit.up.obstructed": "你会撞到上方的东西。", + "worldedit.up.moved": "嗖!", + "worldedit.cone.invalid-radius": "你必须指定 1 或 2 个半径值。", + "worldedit.cone.created": "已创建 {0} 个方块。", + "worldedit.cyl.invalid-radius": "你必须指定 1 或 2 个半径值。", + "worldedit.cyl.created": "已创建 {0} 个方块。", + "worldedit.hcyl.thickness-too-large": "厚度不能大于 x 或 z 半径。", + "worldedit.sphere.invalid-radius": "你必须指定 1 或 3 个半径值。", + "worldedit.sphere.created": "已创建 {0} 个方块。", + "worldedit.blob.created": "已创建 {0} 个方块。", + "worldedit.feature.created": "地物已创建,放置了 {0} 个方块。", + "worldedit.generate.feature.failed": "此地物无法放置在这里。请确保该区域满足要求。", + "worldedit.forestgen.created": "已创建 {0} 棵树。", + "worldedit.pumpkins.created": "已创建 {0} 个南瓜地。", + "worldedit.feature.failed": "生成地物失败。此位置是否适合?", + "worldedit.pyramid.created": "已创建 {0} 个方块。", + "worldedit.generate.created": "已创建 {0} 个方块。", + "worldedit.generatebiome.changed": "影响了 {0} 个生物群系。", + "worldedit.structure.created": "结构已创建,放置了 {0} 个方块。", + "worldedit.generate.structure.failed": "生成结构失败。此位置是否适合?", + "worldedit.reload.config": "配置已重新加载!", + "worldedit.report.written": "FAWE 报告已写入 {0}", + "worldedit.report.error": "写入报告失败: {0}", + "worldedit.report.callback": "FAWE 报告: {0}.report", + "worldedit.timezone.invalid": "无效的时区", + "worldedit.timezone.set": "此会话的时区设置为: {0}", + "worldedit.timezone.current": "该时区的当前时间是: {0}", + "worldedit.version.version": "FAWE 版本:\n - 日期 {0}\n - 提交 {1}\n - 构建 {2}\n - 平台 {3}", + "worldedit.trace.no-tracing-extents": "追踪: 没有使用任何 Extent。", + "worldedit.trace.action-failed": "追踪: 操作 {0} 在 {1} 处被 Extent {2} 丢弃", + "worldedit.trace.active.already": "追踪模式已经处于激活状态。", + "worldedit.trace.inactive.already": "追踪模式已经处于非激活状态。", + "worldedit.trace.active": "追踪模式已激活。", + "worldedit.trace.inactive": "追踪模式已停用。", + "worldedit.command.time-elapsed": "耗时 {0}秒(历史: 更改了 {1};{2} 方块/秒)。", + "worldedit.command.permissions": "你没有权限执行此操作。你是否在正确的模式?", + "worldedit.command.player-only": "此命令必须由玩家使用。", + "worldedit.command.error.report": "&c请报告此错误: [查看控制台]", + "worldedit.command.deprecation": "此命令已弃用。", + "worldedit.command.deprecation-message": "请使用 '{0}' 代替。", + "worldedit.pastebin.uploading": "(请稍候...正在将输出发送到粘贴服务...)", + "worldedit.session.cant-find-session": "无法找到 {0} 的会话", + "worldedit.platform.no-file-dialog": "你的环境不支持文件对话框。", + "worldedit.tool.max-block-changes": "已达到最大方块更改限制。", + "worldedit.tool.no-block": "视线内没有方块!", + "worldedit.tool.repl.equip": "方块替换工具已绑定到 {0}。", + "worldedit.tool.repl.switched": "替换工具已切换为: {0}", + "worldedit.tool.data-cycler.equip": "方块数据循环工具已绑定到 {0}。", + "worldedit.tool.data-cycler.block-not-permitted": "你没有权限循环该方块的数据值。", + "worldedit.tool.data-cycler.cant-cycle": "该方块的数据无法循环!", + "worldedit.tool.data-cycler.new-value": "{0} 的值现在是 {1}。", + "worldedit.tool.data-cycler.cycling": "现在循环 {0}。", + "worldedit.tool.deltree.equip": "浮空树移除工具已绑定到 {0}。", + "worldedit.tool.deltree.not-tree": "那不是一棵树。", + "worldedit.tool.deltree.not-floating": "那不是一棵浮空树。", + "worldedit.tool.tree.equip": "树木工具已绑定到 {0}。", + "worldedit.tool.tree.obstructed": "树无法放在那里。", + "worldedit.tool.structure.equip": "结构放置工具已绑定到 {0}。", + "worldedit.tool.feature.equip": "地物放置工具已绑定到 {0}。", + "worldedit.tool.info.equip": "信息工具已绑定到 {0}。", + "worldedit.tool.inspect.equip": "检查工具已绑定到 {0}。", + "worldedit.tool.info.blockstate.hover": "方块状态(点击复制)", + "worldedit.tool.info.internalid.hover": "内部 ID", + "worldedit.tool.info.legacy.hover": "旧版 id:data", + "worldedit.tool.info.light.hover": "方块光照/上方光照", + "worldedit.tool.none.equip": "工具已从当前物品解绑。", + "worldedit.tool.selwand.equip": "选择魔杖已绑定到 {0}。", + "worldedit.tool.navwand.equip": "导航魔杖已绑定到 {0}。", + "worldedit.tool.floodfill.equip": "方块填充工具已绑定到 {0}。", + "worldedit.tool.farwand.equip": "远距离魔杖工具已绑定到 {0}。", + "worldedit.tool.lrbuild.equip": "远程建造工具已绑定到 {0}。", + "worldedit.tool.lrbuild.set": "左键设置为 {0};右键设置为 {1}。", + "worldedit.tool.stack.equip": "堆叠工具已绑定到 {0}。", + "worldedit.tool.unbind-instruction": "持有该物品时运行 {0} 以解绑。", + "worldedit.tool.superpickaxe.mode.single": "模式现在是单点。使用镐子左键点击。// 禁用。", + "worldedit.tool.superpickaxe.mode.area": "模式现在是区域。使用镐子左键点击。// 禁用。", + "worldedit.tool.superpickaxe.mode.recursive": "模式现在是递归。使用镐子左键点击。// 禁用。", + "worldedit.tool.superpickaxe.max-range": "最大范围是 {0}。", + "worldedit.tool.superpickaxe.enabled.already": "超级镐已经处于启用状态。", + "worldedit.tool.superpickaxe.disabled.already": "超级镐已经处于禁用状态。", + "worldedit.tool.superpickaxe.enabled": "超级镐已启用。", + "worldedit.tool.superpickaxe.disabled": "超级镐已禁用。", + "worldedit.tool.mask.set": "笔刷遮罩已设置。", + "worldedit.tool.mask.disabled": "笔刷遮罩已禁用。", + "worldedit.tool.material.set": "笔刷材质已设置。", + "worldedit.tool.range.set": "笔刷范围已设置。", + "worldedit.tool.size.set": "笔刷大小已设置。", + "worldedit.tool.tracemask.set": "追踪遮罩已设置。", + "worldedit.tool.tracemask.disabled": "追踪遮罩已禁用。", + "worldedit.execute.script-permissions": "你没有权限使用该脚本。", + "worldedit.executelast.no-script": "请先使用 /cs 指定脚本名称。", + "worldedit.script.read-error": "脚本读取错误: {0}", + "worldedit.script.unsupported": "当前仅支持 .js 脚本", + "worldedit.script.file-not-found": "脚本不存在: {0}", + "worldedit.script.no-script-engine": "未找到已安装的脚本引擎。\n请参见 https://worldedit.enginehub.org/en/latest/usage/other/craftscripts/", + "worldedit.script.failed": "执行失败: {0}", + "worldedit.script.failed-console": "执行失败(查看控制台): {0}", + "worldedit.operation.affected.biome": "影响了 {0} 个生物群系", + "worldedit.operation.affected.block": "影响了 {0} 个方块", + "worldedit.operation.affected.column": "影响了 {0} 列", + "worldedit.operation.affected.entity": "影响了 {0} 个实体", + "worldedit.operation.deform.expression": "使用 {0} 进行了变形", + "worldedit.error.parser.invalid-nbt": "输入中的 NBT 数据无效: '{0}'。错误: {1}", + "worldedit.selection.convex.info.vertices": "顶点: {0}", + "worldedit.selection.convex.info.triangles": "三角面: {0}", + "worldedit.selection.convex.explain.primary": "已用顶点 {0} 开始新选区。", + "worldedit.selection.convex.explain.secondary": "已将顶点 {0} 添加到选区。", + "worldedit.selection.cuboid.info.pos1": "位置 1: {0}", + "worldedit.selection.cuboid.info.pos2": "位置 2: {0}", + "worldedit.selection.cuboid.explain.primary": "第一个位置设置为 {0}。", + "worldedit.selection.cuboid.explain.primary-area": "第一个位置设置为 {0} ({1})。", + "worldedit.selection.cuboid.explain.secondary": "第二个位置设置为 {0}。", + "worldedit.selection.cuboid.explain.secondary-area": "第二个位置设置为 {0} ({1})。", + "worldedit.selection.extend.explain.primary": "选区起始于 {0} ({1})。", + "worldedit.selection.extend.explain.secondary": "选区扩展至包含 {0} ({1})。", + "worldedit.selection.ellipsoid.info.center": "中心: {0}", + "worldedit.selection.ellipsoid.info.radius": "X/Y/Z 半径: {0}", + "worldedit.selection.ellipsoid.explain.primary": "中心位置设置为 {0}。", + "worldedit.selection.ellipsoid.explain.primary-area": "中心位置设置为 {0} ({1})。", + "worldedit.selection.ellipsoid.explain.secondary": "半径设置为 {0}。", + "worldedit.selection.ellipsoid.explain.secondary-area": "半径设置为 {0} ({1})。", + "worldedit.selection.cylinder.info.center": "中心: {0}", + "worldedit.selection.cylinder.info.radius": "半径: {0}", + "worldedit.selection.cylinder.explain.primary": "在 {0} 开始新的圆柱选区。", + "worldedit.selection.cylinder.explain.secondary": "半径设置为 {0}/{1} 个方块。({2})", + "worldedit.selection.cylinder.explain.secondary-missing": "在设置半径之前,必须先选择中心点。", + "worldedit.selection.polygon2d.info": "点数: {0}", + "worldedit.selection.polygon2d.explain.primary": "在 {0} 开始新的多边形。", + "worldedit.selection.polygon2d.explain.secondary": "在 {1} 添加了第 {0} 个点。", + "worldedit.selection.sphere.explain.secondary": "半径设置为 {0}。", + "worldedit.selection.sphere.explain.secondary-defined": "半径设置为 {0} ({1})。", + "worldedit.sideeffect.history": "历史记录", + "worldedit.sideeffect.history.description": "记录更改的历史", + "worldedit.sideeffect.heightmaps": "高度图", + "worldedit.sideeffect.heightmaps.description": "更新高度图", + "worldedit.sideeffect.lighting": "光照", + "worldedit.sideeffect.lighting.description": "更新方块光照", + "worldedit.sideeffect.neighbors": "相邻方块", + "worldedit.sideeffect.neighbors.description": "更新编辑中方块的形状", + "worldedit.sideeffect.update": "更新", + "worldedit.sideeffect.update.description": "通知被更改的方块", + "worldedit.sideeffect.validation": "验证", + "worldedit.sideeffect.validation.description": "验证并修复不一致的世界状态,例如断开连接的方块", + "worldedit.sideeffect.entity_ai": "实体 AI", + "worldedit.sideeffect.entity_ai.description": "更新方块变更的实体 AI 路径", + "worldedit.sideeffect.events": "模组/插件事件", + "worldedit.sideeffect.events.description": "在适用时通知其他模组/插件这些更改", + "worldedit.sideeffect.state.on": "开启", + "worldedit.sideeffect.state.delayed": "延迟", + "worldedit.sideeffect.state.off": "关闭", + "worldedit.sideeffect.box.current": "当前", + "worldedit.sideeffect.box.change-to": "点击设置为 {0}", + "worldedit.help.command-not-found": "未找到命令 '{0}'。", + "worldedit.help.no-subcommands": "'{0}' 没有子命令。(也许 '{1}' 是一个参数?)", + "worldedit.help.subcommand-not-found": "在 '{1}' 下未找到子命令 '{0}'。", + "worldedit.cli.stopping": "正在停止!", + "worldedit.cli.unknown-command": "未知命令!", + "worldedit.version.bukkit.unsupported-adapter": "此 FastAsyncWorldEdit 版本不完全支持你的 Bukkit 版本。方块实体(如箱子)将为空,方块属性(如旋转)将缺失,其他功能也可能无法使用。请更新 FastAsyncWorldEdit 以恢复此功能:\n{0}", + "worldedit.bukkit.no-edit-without-adapter": "在不支持的版本上编辑已被禁用。" +} From 45ee27d8abaf98bdc298d142fb2434dd2e12b757 Mon Sep 17 00:00:00 2001 From: LT_Name <572413378@qq.com> Date: Sun, 1 Mar 2026 15:11:50 +0800 Subject: [PATCH 06/12] fix(nukkit-mot): replace deprecated getState(PropertyKey) call Co-Authored-By: Claude Opus 4.6 --- .../com/sk89q/worldedit/nukkitmot/NukkitGetBlocks.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitGetBlocks.java b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitGetBlocks.java index 39db9b97c5..dfbe7d44f3 100644 --- a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitGetBlocks.java +++ b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitGetBlocks.java @@ -13,6 +13,7 @@ import com.fastasyncworldedit.core.queue.IQueueExtent; import com.fastasyncworldedit.core.queue.implementation.blocks.CharGetBlocks; import com.fastasyncworldedit.core.registry.state.PropertyKey; +import com.sk89q.worldedit.registry.state.Property; import com.fastasyncworldedit.nukkitmot.NukkitNbtConverter; import com.fastasyncworldedit.nukkitmot.mapping.BiomeMapping; import com.fastasyncworldedit.nukkitmot.mapping.BlockMapping; @@ -218,10 +219,10 @@ public > T call(IQueueExtent owner, IChunk BlockState state = BlockTypesCache.states[ordinal]; boolean waterlogged = false; if (state != null && state.getBlockType().hasProperty(PropertyKey.WATERLOGGED)) { - Object wl = state.getState(PropertyKey.WATERLOGGED); - if (wl == Boolean.TRUE) { + Property waterloggedProp = state.getBlockType().getProperty(PropertyKey.WATERLOGGED); + if (waterloggedProp != null && state.getState(waterloggedProp) == Boolean.TRUE) { waterlogged = true; - state = state.with(PropertyKey.WATERLOGGED, false); + state = state.with(waterloggedProp, false); ordinal = state.getOrdinalChar(); } } From 8a5658cd2cde8ecc6a31e2e4f53350c4c64a55be Mon Sep 17 00:00:00 2001 From: LT_Name <572413378@qq.com> Date: Sun, 1 Mar 2026 15:20:41 +0800 Subject: [PATCH 07/12] refactor(nukkit-mot): replace unicode escapes with literal section signs in NukkitPlayer Co-Authored-By: Claude Opus 4.6 --- .../java/com/sk89q/worldedit/nukkitmot/NukkitPlayer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitPlayer.java b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitPlayer.java index 5931abb9f5..48bf912043 100644 --- a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitPlayer.java +++ b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitPlayer.java @@ -75,7 +75,7 @@ public void printRaw(String msg) { @Override public void print(String msg) { for (String part : msg.split("\n")) { - player.sendMessage("\u00A7d" + part); + player.sendMessage("§d" + part); } } @@ -83,7 +83,7 @@ public void print(String msg) { @Override public void printDebug(String msg) { for (String part : msg.split("\n")) { - player.sendMessage("\u00A77" + part); + player.sendMessage("§7" + part); } } @@ -91,7 +91,7 @@ public void printDebug(String msg) { @Override public void printError(String msg) { for (String part : msg.split("\n")) { - player.sendMessage("\u00A7c" + part); + player.sendMessage("§c" + part); } } From 5bf602ce4e9a919672566e4bce5465f70f599f92 Mon Sep 17 00:00:00 2001 From: LT_Name <572413378@qq.com> Date: Sun, 1 Mar 2026 18:41:48 +0800 Subject: [PATCH 08/12] fix(nukkit-mot): replace WeakHashMap with Guava Cache for click handling --- .../nukkitmot/NukkitWorldEditListener.java | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitWorldEditListener.java b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitWorldEditListener.java index 530a4f0f39..f410313389 100644 --- a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitWorldEditListener.java +++ b/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitWorldEditListener.java @@ -9,15 +9,15 @@ import cn.nukkit.event.player.PlayerInteractEvent; import cn.nukkit.event.player.PlayerQuitEvent; import cn.nukkit.math.BlockFace; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.event.platform.SessionIdleEvent; import com.sk89q.worldedit.math.Vector3; import com.sk89q.worldedit.util.Direction; import com.sk89q.worldedit.util.Location; -import java.util.Collections; -import java.util.Set; -import java.util.WeakHashMap; +import java.util.concurrent.TimeUnit; /** * Nukkit event listener for WorldEdit interactions. @@ -27,9 +27,13 @@ public class NukkitWorldEditListener implements Listener { private final WorldEditNukkitPlugin plugin; /** * Tracks players whose LEFT_CLICK_BLOCK was already handled by {@link #onPlayerInteract}, - * so that {@link #onBlockBreak} can skip duplicate processing for the same click. + * so that duplicate {@code PlayerInteractEvent} and {@link #onBlockBreak} can be skipped. + * Entries expire after 1 seconds in case {@code BlockBreakEvent} is never fired. */ - private final Set handledLeftClick = Collections.newSetFromMap(new WeakHashMap<>()); + private final Cache handledLeftClick = CacheBuilder.newBuilder() + .expireAfterWrite(1, TimeUnit.SECONDS) + .weakKeys() + .build(); public NukkitWorldEditListener(WorldEditNukkitPlugin plugin) { this.plugin = plugin; @@ -45,18 +49,20 @@ public void onPlayerInteract(PlayerInteractEvent event) { NukkitPlayer player = NukkitAdapter.adapt(nukkitPlayer); WorldEdit we = WorldEdit.getInstance(); - PlayerInteractEvent.Action action = event.getAction(); - switch (action) { + switch (event.getAction()) { case LEFT_CLICK_BLOCK -> { + if (handledLeftClick.getIfPresent(nukkitPlayer) != null) { + event.setCancelled(true); + return; + } Block block = event.getBlock(); Location loc = new Location( player.getWorld(), Vector3.at(block.getFloorX(), block.getFloorY(), block.getFloorZ()) ); Direction direction = adaptFace(event.getFace()); - boolean handled = we.handleBlockLeftClick(player, loc, direction); - if (handled) { - handledLeftClick.add(nukkitPlayer); + if (we.handleBlockLeftClick(player, loc, direction)) { + handledLeftClick.put(nukkitPlayer, Boolean.TRUE); event.setCancelled(true); } } @@ -72,8 +78,7 @@ public void onPlayerInteract(PlayerInteractEvent event) { Vector3.at(block.getFloorX(), block.getFloorY(), block.getFloorZ()) ); Direction direction = adaptFace(event.getFace()); - boolean handled = we.handleBlockRightClick(player, loc, direction); - if (handled) { + if (we.handleBlockRightClick(player, loc, direction)) { event.setCancelled(true); } } @@ -104,22 +109,20 @@ public void onBlockBreak(BlockBreakEvent event) { Player nukkitPlayer = event.getPlayer(); // Skip if already handled by PlayerInteractEvent(LEFT_CLICK_BLOCK) - if (handledLeftClick.remove(nukkitPlayer)) { + if (handledLeftClick.getIfPresent(nukkitPlayer) != null) { event.setCancelled(true); return; } NukkitPlayer player = NukkitAdapter.adapt(nukkitPlayer); - WorldEdit we = WorldEdit.getInstance(); - Block block = event.getBlock(); Location loc = new Location( player.getWorld(), Vector3.at(block.getFloorX(), block.getFloorY(), block.getFloorZ()) ); Direction direction = adaptFace(event.getFace()); - boolean handled = we.handleBlockLeftClick(player, loc, direction); - if (handled) { + if (WorldEdit.getInstance().handleBlockLeftClick(player, loc, direction)) { + handledLeftClick.put(nukkitPlayer, Boolean.TRUE); event.setCancelled(true); } } From 7f3e11fbaf9ceed1d5aa2eb300fe4040d5fd0f12 Mon Sep 17 00:00:00 2001 From: LT_Name <572413378@qq.com> Date: Tue, 3 Mar 2026 20:17:01 +0800 Subject: [PATCH 09/12] feat: adapt to upstream nukkit --- settings.gradle.kts | 7 +- .../{nukkit-mot => nukkit}/build.gradle.kts | 0 .../build.gradle.kts | 31 ++++++-- .../nukkit}/FaweNukkit.java | 2 +- .../nukkit}/NukkitNbtConverter.java | 2 +- .../nukkit}/NukkitPlatformAdapter.java | 2 +- .../nukkit}/NukkitPlayerBlockBag.java | 8 ++- .../nukkit}/NukkitQueueHandler.java | 2 +- .../nukkit}/NukkitRelighter.java | 2 +- .../nukkit}/NukkitTaskManager.java | 2 +- .../nukkit/adapter/NukkitImplAdapter.java | 70 +++++++++++++++++++ .../nukkit/adapter/NukkitImplLoader.java | 64 +++++++++++++++++ .../nukkit}/mapping/BiomeMapping.java | 4 +- .../nukkit}/mapping/BlockMapping.java | 11 ++- .../nukkit}/mapping/ItemMapping.java | 30 +++++--- .../nukkit}/mapping/JeBlockState.java | 2 +- .../nukkit}/mapping/NukkitBlockData.java | 6 +- .../worldedit/nukkit}/NukkitAdapter.java | 8 +-- .../nukkit}/NukkitBlockRegistry.java | 6 +- .../nukkit}/NukkitCommandSender.java | 4 +- .../nukkit}/NukkitConfiguration.java | 2 +- .../sk89q/worldedit/nukkit}/NukkitEntity.java | 10 +-- .../worldedit/nukkit}/NukkitGetBlocks.java | 55 +++++++++++---- .../nukkit}/NukkitGetBlocks_Copy.java | 4 +- .../worldedit/nukkit}/NukkitItemRegistry.java | 4 +- .../sk89q/worldedit/nukkit}/NukkitPlayer.java | 20 +++--- .../worldedit/nukkit}/NukkitRegistries.java | 2 +- .../nukkit}/NukkitServerInterface.java | 9 +-- .../sk89q/worldedit/nukkit}/NukkitWorld.java | 27 ++++--- .../nukkit}/NukkitWorldEditListener.java | 2 +- .../nukkit}/WorldEditNukkitPlugin.java | 41 ++++++----- .../src/main/resources/plugin.yml | 2 +- 32 files changed, 334 insertions(+), 107 deletions(-) rename worldedit-libs/{nukkit-mot => nukkit}/build.gradle.kts (100%) rename {worldedit-nukkit-mot => worldedit-nukkit}/build.gradle.kts (79%) rename {worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot => worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit}/FaweNukkit.java (98%) rename {worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot => worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit}/NukkitNbtConverter.java (99%) rename {worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot => worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit}/NukkitPlatformAdapter.java (90%) rename {worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot => worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit}/NukkitPlayerBlockBag.java (94%) rename {worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot => worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit}/NukkitQueueHandler.java (92%) rename {worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot => worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit}/NukkitRelighter.java (96%) rename {worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot => worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit}/NukkitTaskManager.java (97%) create mode 100644 worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/adapter/NukkitImplAdapter.java create mode 100644 worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/adapter/NukkitImplLoader.java rename {worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot => worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit}/mapping/BiomeMapping.java (95%) rename {worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot => worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit}/mapping/BlockMapping.java (97%) rename {worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot => worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit}/mapping/ItemMapping.java (81%) rename {worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot => worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit}/mapping/JeBlockState.java (98%) rename {worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot => worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit}/mapping/NukkitBlockData.java (63%) rename {worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot => worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit}/NukkitAdapter.java (95%) rename {worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot => worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit}/NukkitBlockRegistry.java (88%) rename {worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot => worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit}/NukkitCommandSender.java (95%) rename {worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot => worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit}/NukkitConfiguration.java (90%) rename {worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot => worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit}/NukkitEntity.java (92%) rename {worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot => worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit}/NukkitGetBlocks.java (87%) rename {worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot => worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit}/NukkitGetBlocks_Copy.java (98%) rename {worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot => worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit}/NukkitItemRegistry.java (77%) rename {worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot => worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit}/NukkitPlayer.java (92%) rename {worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot => worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit}/NukkitRegistries.java (95%) rename {worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot => worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit}/NukkitServerInterface.java (96%) rename {worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot => worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit}/NukkitWorld.java (90%) rename {worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot => worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit}/NukkitWorldEditListener.java (99%) rename {worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot => worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit}/WorldEditNukkitPlugin.java (78%) rename {worldedit-nukkit-mot => worldedit-nukkit}/src/main/resources/plugin.yml (81%) diff --git a/settings.gradle.kts b/settings.gradle.kts index 9c89936565..9e2b379049 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -59,10 +59,15 @@ listOf("1_20_2", "1_20_4", "1_20_5", "1_21", "1_21_4", "1_21_5", "1_21_6", "1_21 include("worldedit-bukkit:adapters:adapter-$it") } -listOf("bukkit", "core", "cli", "nukkit-mot").forEach { +listOf("bukkit", "core", "cli", "nukkit").forEach { include("worldedit-libs:$it") include("worldedit-$it") } + +listOf("mot", "nkx").forEach { + include("worldedit-nukkit:nk-adapters:adapter-$it") +} + include("worldedit-libs:core:ap") diff --git a/worldedit-libs/nukkit-mot/build.gradle.kts b/worldedit-libs/nukkit/build.gradle.kts similarity index 100% rename from worldedit-libs/nukkit-mot/build.gradle.kts rename to worldedit-libs/nukkit/build.gradle.kts diff --git a/worldedit-nukkit-mot/build.gradle.kts b/worldedit-nukkit/build.gradle.kts similarity index 79% rename from worldedit-nukkit-mot/build.gradle.kts rename to worldedit-nukkit/build.gradle.kts index 336cb049e6..1112c21e5c 100644 --- a/worldedit-nukkit-mot/build.gradle.kts +++ b/worldedit-nukkit/build.gradle.kts @@ -5,7 +5,7 @@ plugins { id("buildlogic.platform") } -project.description = "Nukkit-MOT" +project.description = "Nukkit" platform { kind = buildlogic.WorldEditKind.Plugin @@ -43,6 +43,11 @@ repositories { } } +val adapters: Configuration by configurations.register("adapters") { + description = "Nukkit platform adapters to include in the shadow JAR" + isCanBeConsumed = false +} + val geyserMappings: Configuration by configurations.register("geyserMappings") { isCanBeConsumed = false } @@ -53,7 +58,7 @@ val mcmeta: Configuration by configurations.register("mcmeta") { dependencies { api(project(":worldedit-core")) - api(project(":worldedit-libs:nukkit-mot")) + api(project(":worldedit-libs:nukkit")) compileOnly("cn.nukkit:Nukkit:MOT-SNAPSHOT") @@ -71,6 +76,12 @@ dependencies { mcmeta("misode.mcmeta", "blocks/data", "cb195b9", ext = "json") } +project.project(":worldedit-nukkit:nk-adapters").subprojects.forEach { + dependencies { + "adapters"(project(it.path)) + } +} + tasks.named("processResources") { val internalVersion = project.ext["internalVersion"] inputs.property("internalVersion", internalVersion) @@ -87,10 +98,14 @@ tasks.named("processResources") { } tasks.named("shadowJar") { - archiveFileName.set("${rootProject.name}-Nukkit-MOT-${project.version}.${archiveExtension.getOrElse("jar")}") + archiveFileName.set("${rootProject.name}-Nukkit-${project.version}.${archiveExtension.getOrElse("jar")}") + configurations = listOf( + project.configurations.getByName("runtimeClasspath"), + adapters + ) dependencies { include(dependency(":worldedit-core")) - include(dependency(":worldedit-libs:nukkit-mot")) + include(dependency(":worldedit-libs:nukkit")) relocate("org.antlr.v4", "com.sk89q.worldedit.antlr4") { include(dependency("org.antlr:antlr4-runtime")) } @@ -110,6 +125,14 @@ tasks.named("shadowJar") { include(dependency(libs.zstd)) include(dependency(libs.lz4Java)) } + project.project(":worldedit-nukkit:nk-adapters").subprojects.forEach { + dependencies { + include(dependency("${it.group}:${it.name}")) + } + minimize { + exclude(dependency("${it.group}:${it.name}")) + } + } minimize { exclude(dependency(libs.lz4Java)) } diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/FaweNukkit.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/FaweNukkit.java similarity index 98% rename from worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/FaweNukkit.java rename to worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/FaweNukkit.java index 59112bd742..99a07111a6 100644 --- a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/FaweNukkit.java +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/FaweNukkit.java @@ -1,4 +1,4 @@ -package com.fastasyncworldedit.nukkitmot; +package com.fastasyncworldedit.nukkit; import cn.nukkit.Player; import cn.nukkit.Server; diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitNbtConverter.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitNbtConverter.java similarity index 99% rename from worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitNbtConverter.java rename to worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitNbtConverter.java index dc2280bdce..0675e6cebc 100644 --- a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitNbtConverter.java +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitNbtConverter.java @@ -1,4 +1,4 @@ -package com.fastasyncworldedit.nukkitmot; +package com.fastasyncworldedit.nukkit; import cn.nukkit.nbt.tag.ByteArrayTag; import cn.nukkit.nbt.tag.ByteTag; diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitPlatformAdapter.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitPlatformAdapter.java similarity index 90% rename from worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitPlatformAdapter.java rename to worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitPlatformAdapter.java index 9bf58ca20e..4b85155b61 100644 --- a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitPlatformAdapter.java +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitPlatformAdapter.java @@ -1,4 +1,4 @@ -package com.fastasyncworldedit.nukkitmot; +package com.fastasyncworldedit.nukkit; import com.fastasyncworldedit.core.FAWEPlatformAdapterImpl; import com.fastasyncworldedit.core.queue.IChunkGet; diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitPlayerBlockBag.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitPlayerBlockBag.java similarity index 94% rename from worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitPlayerBlockBag.java rename to worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitPlayerBlockBag.java index 07ce67e413..53d0538909 100644 --- a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitPlayerBlockBag.java +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitPlayerBlockBag.java @@ -1,8 +1,8 @@ -package com.fastasyncworldedit.nukkitmot; +package com.fastasyncworldedit.nukkit; import cn.nukkit.Player; import cn.nukkit.item.Item; -import com.fastasyncworldedit.nukkitmot.mapping.ItemMapping; +import com.fastasyncworldedit.nukkit.mapping.ItemMapping; import com.sk89q.worldedit.extent.inventory.BlockBag; import com.sk89q.worldedit.extent.inventory.BlockBagException; import com.sk89q.worldedit.extent.inventory.OutOfBlocksException; @@ -75,6 +75,10 @@ public void storeBlock(BlockState blockState, int amount) throws BlockBagExcepti BlockType type = blockState.getBlockType(); ItemMapping.NukkitItemData beData = ItemMapping.jeToBe(type.getItemType().id()); + if (beData.itemId() == 0) { + // Unmapped JE item — discard silently to avoid storing air + return; + } // Merge into existing stacks first for (Map.Entry entry : items.entrySet()) { diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitQueueHandler.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitQueueHandler.java similarity index 92% rename from worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitQueueHandler.java rename to worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitQueueHandler.java index 5a14e7be59..b405e2492f 100644 --- a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitQueueHandler.java +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitQueueHandler.java @@ -1,4 +1,4 @@ -package com.fastasyncworldedit.nukkitmot; +package com.fastasyncworldedit.nukkit; import com.fastasyncworldedit.core.queue.implementation.QueueHandler; diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitRelighter.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitRelighter.java similarity index 96% rename from worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitRelighter.java rename to worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitRelighter.java index 0f157a7790..9c64d2144e 100644 --- a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitRelighter.java +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitRelighter.java @@ -1,4 +1,4 @@ -package com.fastasyncworldedit.nukkitmot; +package com.fastasyncworldedit.nukkit; import com.fastasyncworldedit.core.extent.processor.lighting.Relighter; diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitTaskManager.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitTaskManager.java similarity index 97% rename from worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitTaskManager.java rename to worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitTaskManager.java index 7f21f4cc98..cd66451501 100644 --- a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/NukkitTaskManager.java +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitTaskManager.java @@ -1,4 +1,4 @@ -package com.fastasyncworldedit.nukkitmot; +package com.fastasyncworldedit.nukkit; import cn.nukkit.Server; import cn.nukkit.plugin.Plugin; diff --git a/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/adapter/NukkitImplAdapter.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/adapter/NukkitImplAdapter.java new file mode 100644 index 0000000000..32a8552add --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/adapter/NukkitImplAdapter.java @@ -0,0 +1,70 @@ +package com.fastasyncworldedit.nukkit.adapter; + +import cn.nukkit.Player; +import cn.nukkit.entity.Entity; +import cn.nukkit.level.format.leveldb.structure.BlockStateSnapshot; +import org.cloudburstmc.nbt.NbtMap; + +import javax.annotation.Nullable; +import java.util.List; + +/** + * Adapter interface abstracting API differences between Nukkit-MOT and NKX. + *

+ * Each platform provides its own implementation compiled against its specific API, + * loaded at runtime by {@link NukkitImplLoader}. + */ +public interface NukkitImplAdapter { + + /** + * Platform name for display: "Nukkit-MOT" or "NKX". + */ + String getPlatformName(); + + /** + * Runtime value of {@code Block.DATA_BITS} (MOT=13, NKX=6). + */ + int getBlockDataBits(); + + /** + * Runtime value of {@code Block.DATA_MASK}: {@code (1 << getBlockDataBits()) - 1}. + */ + default int getBlockDataMask() { + return (1 << getBlockDataBits()) - 1; + } + + /** + * Get the player's language code string, or {@code null} if unsupported (NKX). + */ + @Nullable + String getPlayerLanguageCode(Player player); + + /** + * Get the runtime block ID for sending fake blocks to a player. + * + * @param player target player + * @param blockId legacy block ID + * @param meta legacy block meta + * @return block runtime ID + */ + int getBlockRuntimeId(Player player, int blockId, int meta); + + /** + * Get the entity type identifier string (e.g. "minecraft:creeper"). + * Returns {@code null} if unavailable. + */ + @Nullable + String getEntityIdentifier(Entity entity); + + /** + * Load the block palette from Nukkit's legacy mapper. + */ + List loadBlockPalette(); + + /** + * Get {@link BlockStateSnapshot} from Nukkit's {@code BlockStateMapping} for the given NBT state. + * Returns {@code null} if no mapping exists. + */ + @Nullable + BlockStateSnapshot getBlockStateSnapshot(NbtMap nbtState); +} diff --git a/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/adapter/NukkitImplLoader.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/adapter/NukkitImplLoader.java new file mode 100644 index 0000000000..250e3f202a --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/adapter/NukkitImplLoader.java @@ -0,0 +1,64 @@ +package com.fastasyncworldedit.nukkit.adapter; + +/** + * Detects the running Nukkit platform (MOT vs NKX) and loads the appropriate adapter. + */ +public final class NukkitImplLoader { + + private static NukkitImplAdapter instance; + + private NukkitImplLoader() { + } + + /** + * Detect and load the platform adapter. Caches the result for subsequent calls. + * + * @return the loaded adapter + * @throws RuntimeException if no adapter could be loaded + */ + public static NukkitImplAdapter detect() { + if (instance != null) { + return instance; + } + synchronized (NukkitImplLoader.class) { + if (instance != null) { + return instance; + } + instance = doDetect(); + return instance; + } + } + + /** + * Get the cached adapter instance. Must call {@link #detect()} first. + */ + public static NukkitImplAdapter get() { + if (instance == null) { + throw new IllegalStateException("NukkitImplLoader.detect() has not been called yet"); + } + return instance; + } + + private static NukkitImplAdapter doDetect() { + // Detect Nukkit-MOT by checking for a MOT-specific class + boolean isMot; + try { + Class.forName("cn.nukkit.GameVersion"); + isMot = true; + } catch (ClassNotFoundException e) { + isMot = false; + } + + String className = isMot + ? "com.fastasyncworldedit.nukkit.adapter.mot.MotNukkitAdapter" + : "com.fastasyncworldedit.nukkit.adapter.nkx.NkxNukkitAdapter"; + + try { + Class clazz = Class.forName(className); + return (NukkitImplAdapter) clazz.getDeclaredConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Failed to load Nukkit adapter: " + className, e); + } + } + +} diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/BiomeMapping.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/BiomeMapping.java similarity index 95% rename from worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/BiomeMapping.java rename to worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/BiomeMapping.java index 8d849cc41b..8577071a90 100644 --- a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/BiomeMapping.java +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/BiomeMapping.java @@ -1,10 +1,10 @@ -package com.fastasyncworldedit.nukkitmot.mapping; +package com.fastasyncworldedit.nukkit.mapping; import com.google.gson.Gson; import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; -import com.sk89q.worldedit.nukkitmot.WorldEditNukkitPlugin; +import com.sk89q.worldedit.nukkit.WorldEditNukkitPlugin; import java.io.IOException; import java.io.InputStream; diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/BlockMapping.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/BlockMapping.java similarity index 97% rename from worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/BlockMapping.java rename to worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/BlockMapping.java index 5bca67e773..db9d11fde7 100644 --- a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/BlockMapping.java +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/BlockMapping.java @@ -1,8 +1,7 @@ -package com.fastasyncworldedit.nukkitmot.mapping; +package com.fastasyncworldedit.nukkit.mapping; -import cn.nukkit.level.format.leveldb.BlockStateMapping; -import cn.nukkit.level.format.leveldb.NukkitLegacyMapper; import cn.nukkit.level.format.leveldb.structure.BlockStateSnapshot; +import com.fastasyncworldedit.nukkit.adapter.NukkitImplLoader; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.TypeAdapter; @@ -11,7 +10,7 @@ import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; -import com.sk89q.worldedit.nukkitmot.WorldEditNukkitPlugin; +import com.sk89q.worldedit.nukkit.WorldEditNukkitPlugin; import com.sk89q.worldedit.world.block.BlockState; import com.sk89q.worldedit.world.block.BlockTypesCache; import it.unimi.dsi.fastutil.ints.Int2CharOpenHashMap; @@ -228,7 +227,7 @@ private static boolean initJeBlockDefaultProperties() { private static boolean initBlockStateMapping() { // Extract block state version from palette - List palette = NukkitLegacyMapper.loadBlockPalette(); + List palette = NukkitImplLoader.get().loadBlockPalette(); if (!palette.isEmpty()) { blockStateVersion = palette.getFirst().getInt("version"); WorldEditNukkitPlugin.getInstance().getLogger().info("Block state version: " + blockStateVersion); @@ -296,7 +295,7 @@ private static NukkitBlockData createNukkitBlockData(BlockMappingEntry.BedrockSt .putInt("version", blockStateVersion) .build(); - BlockStateSnapshot snapshot = BlockStateMapping.get().getStateUnsafe(nbtState); + BlockStateSnapshot snapshot = NukkitImplLoader.get().getBlockStateSnapshot(nbtState); if (snapshot == null) { return null; } diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/ItemMapping.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/ItemMapping.java similarity index 81% rename from worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/ItemMapping.java rename to worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/ItemMapping.java index ee8bff24fc..b34c8ee677 100644 --- a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/ItemMapping.java +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/ItemMapping.java @@ -1,11 +1,11 @@ -package com.fastasyncworldedit.nukkitmot.mapping; +package com.fastasyncworldedit.nukkit.mapping; import cn.nukkit.item.Item; import com.google.gson.Gson; import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; -import com.sk89q.worldedit.nukkitmot.WorldEditNukkitPlugin; +import com.sk89q.worldedit.nukkit.WorldEditNukkitPlugin; import java.io.IOException; import java.io.InputStream; @@ -25,7 +25,7 @@ public final class ItemMapping { private static final Gson GSON = new Gson(); private static final Map JE_TO_BE = new HashMap<>(); - private static final Map BE_TO_JE = new HashMap<>(); + private static final Map BE_TO_JE = new HashMap<>(); // All known JE item IDs from items.json (loaded early for ItemTypesCache) private static final Set JE_ITEM_IDS = new HashSet<>(); @@ -83,7 +83,7 @@ public static void init() { if (nukkitItem != null) { NukkitItemData data = new NukkitItemData(nukkitItem.getId(), entry.bedrockData()); JE_TO_BE.put(javaId, data); - BE_TO_JE.putIfAbsent(nukkitItem.getId(), javaId); + BE_TO_JE.putIfAbsent(beKey(nukkitItem.getId(), entry.bedrockData()), javaId); } }); @@ -105,14 +105,24 @@ public static NukkitItemData jeToBe(String jeItemId) { } /** - * Convert Nukkit item ID to JE item ID. + * Convert Nukkit item ID + metadata to JE item ID. */ - public static String beToJe(int beItemId) { - String result = BE_TO_JE.get(beItemId); - if (result == null) { - return "minecraft:air"; + public static String beToJe(int beItemId, int metadata) { + // Try exact match first (id + metadata) + String result = BE_TO_JE.get(beKey(beItemId, metadata)); + if (result != null) { + return result; } - return result; + // Fallback: try metadata 0 + result = BE_TO_JE.get(beKey(beItemId, 0)); + if (result != null) { + return result; + } + return "minecraft:air"; + } + + private static long beKey(int itemId, int metadata) { + return ((long) itemId << 16) | (metadata & 0xFFFF); } public record NukkitItemData(int itemId, int metadata) { diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/JeBlockState.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/JeBlockState.java similarity index 98% rename from worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/JeBlockState.java rename to worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/JeBlockState.java index aea467cb4c..1e37699d61 100644 --- a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/JeBlockState.java +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/JeBlockState.java @@ -1,4 +1,4 @@ -package com.fastasyncworldedit.nukkitmot.mapping; +package com.fastasyncworldedit.nukkit.mapping; import java.util.Map; import java.util.TreeMap; diff --git a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/NukkitBlockData.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/NukkitBlockData.java similarity index 63% rename from worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/NukkitBlockData.java rename to worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/NukkitBlockData.java index e2e042a5bd..6b1acde3bf 100644 --- a/worldedit-nukkit-mot/src/main/java/com/fastasyncworldedit/nukkitmot/mapping/NukkitBlockData.java +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/NukkitBlockData.java @@ -1,6 +1,6 @@ -package com.fastasyncworldedit.nukkitmot.mapping; +package com.fastasyncworldedit.nukkit.mapping; -import cn.nukkit.block.Block; +import com.fastasyncworldedit.nukkit.adapter.NukkitImplLoader; /** * Represents a Nukkit block as its legacy ID and metadata. @@ -13,7 +13,7 @@ public record NukkitBlockData(int blockId, int metadata) { * Get the Nukkit full block ID: (blockId << DATA_BITS) | metadata */ public int getFullId() { - return (blockId << Block.DATA_BITS) | metadata; + return (blockId << NukkitImplLoader.get().getBlockDataBits()) | metadata; } } diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitAdapter.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitAdapter.java similarity index 95% rename from worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitAdapter.java rename to worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitAdapter.java index 1256fab043..9a3c703cf0 100644 --- a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitAdapter.java +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitAdapter.java @@ -1,11 +1,11 @@ -package com.sk89q.worldedit.nukkitmot; +package com.sk89q.worldedit.nukkit; import cn.nukkit.Player; import cn.nukkit.item.Item; import cn.nukkit.level.Level; import cn.nukkit.math.Vector3; -import com.fastasyncworldedit.nukkitmot.mapping.BlockMapping; -import com.fastasyncworldedit.nukkitmot.mapping.ItemMapping; +import com.fastasyncworldedit.nukkit.mapping.BlockMapping; +import com.fastasyncworldedit.nukkit.mapping.ItemMapping; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.extent.Extent; import com.sk89q.worldedit.math.BlockVector3; @@ -98,7 +98,7 @@ public static Location adapt(cn.nukkit.level.Location location) { * Convert a Nukkit Item to a WorldEdit ItemType. */ public static ItemType adapt(Item item) { - String jeId = ItemMapping.beToJe(item.getId()); + String jeId = ItemMapping.beToJe(item.getId(), item.getDamage()); ItemType type = ItemTypes.get(jeId); return type != null ? type : ItemTypes.AIR; } diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitBlockRegistry.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitBlockRegistry.java similarity index 88% rename from worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitBlockRegistry.java rename to worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitBlockRegistry.java index 5a5637b554..583cbbab98 100644 --- a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitBlockRegistry.java +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitBlockRegistry.java @@ -1,12 +1,12 @@ -package com.sk89q.worldedit.nukkitmot; +package com.sk89q.worldedit.nukkit; -import com.fastasyncworldedit.nukkitmot.mapping.BlockMapping; +import com.fastasyncworldedit.nukkit.mapping.BlockMapping; import com.sk89q.worldedit.world.block.BlockType; import com.sk89q.worldedit.world.registry.BlockMaterial; import com.sk89q.worldedit.world.registry.BundledBlockRegistry; -import java.util.Collection; import javax.annotation.Nullable; +import java.util.Collection; /** * Nukkit block registry that extends the bundled registry with Nukkit-specific material data. diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitCommandSender.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitCommandSender.java similarity index 95% rename from worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitCommandSender.java rename to worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitCommandSender.java index 0636b9b8d2..4dc99fcbd7 100644 --- a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitCommandSender.java +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitCommandSender.java @@ -1,12 +1,10 @@ -package com.sk89q.worldedit.nukkitmot; +package com.sk89q.worldedit.nukkit; import cn.nukkit.command.CommandSender; import com.sk89q.worldedit.extension.platform.AbstractNonPlayerActor; import com.sk89q.worldedit.session.SessionKey; -import com.sk89q.worldedit.util.Location; import com.sk89q.worldedit.util.auth.AuthorizationException; import com.sk89q.worldedit.util.formatting.text.Component; -import com.sk89q.worldedit.util.formatting.text.TextComponent; import com.sk89q.worldedit.util.formatting.text.serializer.plain.PlainComponentSerializer; import javax.annotation.Nullable; diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitConfiguration.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitConfiguration.java similarity index 90% rename from worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitConfiguration.java rename to worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitConfiguration.java index f46e3f32f2..0b1d339496 100644 --- a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitConfiguration.java +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitConfiguration.java @@ -1,4 +1,4 @@ -package com.sk89q.worldedit.nukkitmot; +package com.sk89q.worldedit.nukkit; import cn.nukkit.plugin.Plugin; import com.sk89q.worldedit.util.PropertiesConfiguration; diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitEntity.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitEntity.java similarity index 92% rename from worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitEntity.java rename to worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitEntity.java index 4d08bcc049..a301926045 100644 --- a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitEntity.java +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitEntity.java @@ -1,7 +1,7 @@ -package com.sk89q.worldedit.nukkitmot; +package com.sk89q.worldedit.nukkit; -import cn.nukkit.utils.Identifier; -import com.fastasyncworldedit.nukkitmot.NukkitNbtConverter; +import com.fastasyncworldedit.nukkit.NukkitNbtConverter; +import com.fastasyncworldedit.nukkit.adapter.NukkitImplLoader; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.entity.Entity; import com.sk89q.worldedit.extent.Extent; @@ -65,12 +65,12 @@ public BaseEntity getState() { return null; } - Identifier identifier = entity.getIdentifier(); + String identifier = NukkitImplLoader.get().getEntityIdentifier(entity); if (identifier == null) { return null; } - EntityType type = EntityTypes.get(identifier.toString()); + EntityType type = EntityTypes.get(identifier); if (type == null) { return null; } diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitGetBlocks.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitGetBlocks.java similarity index 87% rename from worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitGetBlocks.java rename to worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitGetBlocks.java index dfbe7d44f3..e9997e163e 100644 --- a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitGetBlocks.java +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitGetBlocks.java @@ -1,6 +1,5 @@ -package com.sk89q.worldedit.nukkitmot; +package com.sk89q.worldedit.nukkit; -import cn.nukkit.block.Block; import cn.nukkit.blockentity.BlockEntity; import cn.nukkit.level.Level; import cn.nukkit.level.format.generic.BaseFullChunk; @@ -13,12 +12,14 @@ import com.fastasyncworldedit.core.queue.IQueueExtent; import com.fastasyncworldedit.core.queue.implementation.blocks.CharGetBlocks; import com.fastasyncworldedit.core.registry.state.PropertyKey; -import com.sk89q.worldedit.registry.state.Property; -import com.fastasyncworldedit.nukkitmot.NukkitNbtConverter; -import com.fastasyncworldedit.nukkitmot.mapping.BiomeMapping; -import com.fastasyncworldedit.nukkitmot.mapping.BlockMapping; +import com.fastasyncworldedit.nukkit.NukkitNbtConverter; +import com.fastasyncworldedit.nukkit.adapter.NukkitImplAdapter; +import com.fastasyncworldedit.nukkit.adapter.NukkitImplLoader; +import com.fastasyncworldedit.nukkit.mapping.BiomeMapping; +import com.fastasyncworldedit.nukkit.mapping.BlockMapping; import com.sk89q.worldedit.entity.Entity; import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.registry.state.Property; import com.sk89q.worldedit.world.biome.BiomeType; import com.sk89q.worldedit.world.biome.BiomeTypes; import com.sk89q.worldedit.world.block.BlockState; @@ -178,8 +179,20 @@ public > T call(IQueueExtent owner, IChunk int index = (y << 8) | (z << 4) | x; int fullId = chunk.getFullBlock(x, baseY + y, z); char ordinal = BlockMapping.fullIdToJeOrdinal(fullId); - sectionData[index] = (ordinal == Character.MAX_VALUE) - ? BlockTypesCache.ReservedIDs.AIR : ordinal; + if (ordinal == Character.MAX_VALUE) { + sectionData[index] = BlockTypesCache.ReservedIDs.AIR; + } else { + // Apply waterlogged state from layer 1 + BlockState state = BlockTypesCache.states[ordinal]; + if (state != null && state.getBlockType().hasProperty(PropertyKey.WATERLOGGED)) { + int layer1Id = chunk.getBlockId(x, baseY + y, z, 1); + if (layer1Id == WATER_ID || layer1Id == STILL_WATER_ID) { + state = state.with(PropertyKey.WATERLOGGED, true); + ordinal = state.getOrdinalChar(); + } + } + sectionData[index] = ordinal; + } } } } @@ -195,6 +208,21 @@ public > T call(IQueueExtent owner, IChunk copy.storeTile(pos, NukkitNbtConverter.toFawe(be.namedTag)); } } + // Store current biomes before modification + if (set.hasBiomes()) { + for (int y = set.getMinSectionPosition() << 4; y <= (set.getMaxSectionPosition() << 4) + 15; y++) { + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + if (set.getBiomeType(x, y, z) != null) { + int biomeId = chunk.getBiomeId(x & 0xF, z & 0xF); + String jeBiome = BiomeMapping.beToJe(biomeId); + BiomeType type = BiomeTypes.get(jeBiome); + copy.storeBiome(x, y, z, type != null ? type : BiomeTypes.PLAINS); + } + } + } + } + } } // Apply block changes @@ -226,15 +254,18 @@ public > T call(IQueueExtent owner, IChunk ordinal = state.getOrdinalChar(); } } + NukkitImplAdapter adapter = NukkitImplLoader.get(); + int dataBits = adapter.getBlockDataBits(); + int dataMask = adapter.getBlockDataMask(); int fullId = BlockMapping.jeOrdinalToFullId(ordinal); - int blockId = fullId >> Block.DATA_BITS; - int meta = fullId & Block.DATA_MASK; + int blockId = fullId >> dataBits; + int meta = fullId & dataMask; chunk.setFullBlockId(x, baseY + y, z, 0, - (blockId << Block.DATA_BITS) | meta); + (blockId << dataBits) | meta); // Set or clear layer 1 water if (waterlogged) { chunk.setFullBlockId(x, baseY + y, z, 1, - STILL_WATER_ID << Block.DATA_BITS); + STILL_WATER_ID << dataBits); } else if (state != null && state.getBlockType().hasProperty(PropertyKey.WATERLOGGED)) { // Clear water from layer 1 if block supports waterlogged but isn't chunk.setFullBlockId(x, baseY + y, z, 1, 0); diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitGetBlocks_Copy.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitGetBlocks_Copy.java similarity index 98% rename from worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitGetBlocks_Copy.java rename to worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitGetBlocks_Copy.java index 513cf4f742..bafa9552fd 100644 --- a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitGetBlocks_Copy.java +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitGetBlocks_Copy.java @@ -1,4 +1,4 @@ -package com.sk89q.worldedit.nukkitmot; +package com.sk89q.worldedit.nukkit; import com.fastasyncworldedit.core.extent.processor.heightmap.HeightMapType; import com.fastasyncworldedit.core.nbt.FaweCompoundTag; @@ -6,7 +6,7 @@ import com.fastasyncworldedit.core.queue.IChunkGet; import com.fastasyncworldedit.core.queue.IChunkSet; import com.fastasyncworldedit.core.queue.IQueueExtent; -import com.fastasyncworldedit.nukkitmot.NukkitNbtConverter; +import com.fastasyncworldedit.nukkit.NukkitNbtConverter; import com.sk89q.worldedit.entity.Entity; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.world.biome.BiomeType; diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitItemRegistry.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitItemRegistry.java similarity index 77% rename from worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitItemRegistry.java rename to worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitItemRegistry.java index 1e0c50eaa7..7adb287824 100644 --- a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitItemRegistry.java +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitItemRegistry.java @@ -1,6 +1,6 @@ -package com.sk89q.worldedit.nukkitmot; +package com.sk89q.worldedit.nukkit; -import com.fastasyncworldedit.nukkitmot.mapping.ItemMapping; +import com.fastasyncworldedit.nukkit.mapping.ItemMapping; import com.sk89q.worldedit.world.registry.BundledItemRegistry; import java.util.Collection; diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitPlayer.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitPlayer.java similarity index 92% rename from worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitPlayer.java rename to worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitPlayer.java index 48bf912043..82e3d6790b 100644 --- a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitPlayer.java +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitPlayer.java @@ -1,4 +1,4 @@ -package com.sk89q.worldedit.nukkitmot; +package com.sk89q.worldedit.nukkit; import cn.nukkit.AdventureSettings; import cn.nukkit.Player; @@ -6,7 +6,9 @@ import cn.nukkit.level.Level; import cn.nukkit.network.protocol.UpdateBlockPacket; import cn.nukkit.permission.PermissionAttachment; -import com.fastasyncworldedit.nukkitmot.NukkitPlayerBlockBag; +import com.fastasyncworldedit.nukkit.NukkitPlayerBlockBag; +import com.fastasyncworldedit.nukkit.adapter.NukkitImplAdapter; +import com.fastasyncworldedit.nukkit.adapter.NukkitImplLoader; import com.sk89q.worldedit.blocks.BaseItemStack; import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.extension.platform.AbstractPlayerActor; @@ -202,7 +204,10 @@ public boolean setLocation(Location location) { @Override public Locale getLocale() { try { - String code = player.getLanguageCode().name(); // e.g. "zh_CN" + String code = NukkitImplLoader.get().getPlayerLanguageCode(player); + if (code == null) { + return Locale.getDefault(); + } return Locale.forLanguageTag(code.replace('_', '-')); } catch (Exception e) { return Locale.getDefault(); @@ -229,18 +234,17 @@ public > void sendFakeBlock(BlockVector3 pos, B bl level.sendBlocks(new Player[]{player}, new cn.nukkit.math.Vector3[]{vec}, UpdateBlockPacket.FLAG_ALL); } else { + NukkitImplAdapter adapter = NukkitImplLoader.get(); int fullId = NukkitAdapter.adaptFullId(block.toImmutableState()); - int blockId = fullId >> cn.nukkit.block.Block.DATA_BITS; - int meta = fullId & cn.nukkit.block.Block.DATA_MASK; + int blockId = fullId >> adapter.getBlockDataBits(); + int meta = fullId & adapter.getBlockDataMask(); UpdateBlockPacket pk = new UpdateBlockPacket(); pk.x = pos.x(); pk.y = pos.y(); pk.z = pos.z(); pk.flags = UpdateBlockPacket.FLAG_ALL; - pk.blockRuntimeId = cn.nukkit.level.GlobalBlockPalette.getOrCreateRuntimeId( - player.getGameVersion(), blockId, meta - ); + pk.blockRuntimeId = adapter.getBlockRuntimeId(player, blockId, meta); player.dataPacket(pk); } } diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitRegistries.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitRegistries.java similarity index 95% rename from worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitRegistries.java rename to worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitRegistries.java index b7a3e415d2..7b18e6d417 100644 --- a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitRegistries.java +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitRegistries.java @@ -1,4 +1,4 @@ -package com.sk89q.worldedit.nukkitmot; +package com.sk89q.worldedit.nukkit; import com.sk89q.worldedit.world.registry.BlockRegistry; import com.sk89q.worldedit.world.registry.BundledRegistries; diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitServerInterface.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitServerInterface.java similarity index 96% rename from worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitServerInterface.java rename to worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitServerInterface.java index 14bffe3a41..e20f2394f5 100644 --- a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitServerInterface.java +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitServerInterface.java @@ -1,11 +1,12 @@ -package com.sk89q.worldedit.nukkitmot; +package com.sk89q.worldedit.nukkit; import cn.nukkit.Server; import cn.nukkit.command.Command; import cn.nukkit.command.CommandSender; import cn.nukkit.level.Level; import com.fastasyncworldedit.core.extent.processor.lighting.RelighterFactory; -import com.fastasyncworldedit.nukkitmot.NukkitRelighter; +import com.fastasyncworldedit.nukkit.NukkitRelighter; +import com.fastasyncworldedit.nukkit.adapter.NukkitImplLoader; import com.sk89q.worldedit.LocalConfiguration; import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.command.util.PermissionCondition; @@ -143,7 +144,7 @@ public String getVersion() { @Override public String getPlatformName() { - return "Nukkit-MOT"; + return NukkitImplLoader.get().getPlatformName(); } @Override @@ -153,7 +154,7 @@ public String getPlatformVersion() { @Override public String id() { - return "intellectualsites:nukkit-mot"; + return "intellectualsites:nukkit"; } @Override diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitWorld.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitWorld.java similarity index 90% rename from worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitWorld.java rename to worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitWorld.java index cb4b201348..f89f6265b7 100644 --- a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitWorld.java +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitWorld.java @@ -1,11 +1,13 @@ -package com.sk89q.worldedit.nukkitmot; +package com.sk89q.worldedit.nukkit; import cn.nukkit.block.Block; import cn.nukkit.level.Level; import com.fastasyncworldedit.core.nbt.FaweCompoundTag; import com.fastasyncworldedit.core.queue.IChunkGet; import com.fastasyncworldedit.core.queue.implementation.packet.ChunkPacket; -import com.fastasyncworldedit.nukkitmot.mapping.BiomeMapping; +import com.fastasyncworldedit.nukkit.adapter.NukkitImplAdapter; +import com.fastasyncworldedit.nukkit.adapter.NukkitImplLoader; +import com.fastasyncworldedit.nukkit.mapping.BiomeMapping; import com.sk89q.worldedit.EditSession; import com.sk89q.worldedit.WorldEditException; import com.sk89q.worldedit.blocks.BaseItemStack; @@ -68,19 +70,24 @@ public String id() { @Override public > boolean setBlock(BlockVector3 position, B block, SideEffectSet sideEffects) throws WorldEditException { + NukkitImplAdapter adapter = NukkitImplLoader.get(); int fullId = NukkitAdapter.adaptFullId(block.toImmutableState()); - int blockId = fullId >> Block.DATA_BITS; - int meta = fullId & Block.DATA_MASK; + int blockId = fullId >> adapter.getBlockDataBits(); + int meta = fullId & adapter.getBlockDataMask(); Level level = getLevel(); Block nukkitBlock = Block.get(blockId, meta); - return level.setBlock(position.x(), position.y(), position.z(), nukkitBlock, true, true); + // Nukkit update param controls neighbor updates and physics + boolean update = sideEffects.shouldApply(SideEffect.NEIGHBORS) + || sideEffects.shouldApply(SideEffect.UPDATE); + return level.setBlock(position.x(), position.y(), position.z(), nukkitBlock, true, update); } @Override public Set applySideEffects( BlockVector3 position, BlockState previousType, SideEffectSet sideEffectSet ) throws WorldEditException { + // Nukkit applies all supported side effects atomically in setBlock return Set.of(); } @@ -91,7 +98,7 @@ public BaseBlock getFullBlock(BlockVector3 position) { cn.nukkit.blockentity.BlockEntity be = level.getBlockEntity(NukkitAdapter.adapt(position)); if (be != null && be.namedTag != null) { return state.toBaseBlock(LazyReference.computed( - com.fastasyncworldedit.nukkitmot.NukkitNbtConverter.toLinCompound(be.namedTag) + com.fastasyncworldedit.nukkit.NukkitNbtConverter.toLinCompound(be.namedTag) )); } return state.toBaseBlock(); @@ -101,7 +108,7 @@ public BaseBlock getFullBlock(BlockVector3 position) { public BlockState getBlock(BlockVector3 position) { Level level = getLevel(); Block block = level.getBlock(position.x(), position.y(), position.z()); - int fullId = (block.getId() << Block.DATA_BITS) | block.getDamage(); + int fullId = (block.getId() << NukkitImplLoader.get().getBlockDataBits()) | block.getDamage(); return NukkitAdapter.adaptBlockState(fullId); } @@ -216,7 +223,9 @@ public IChunkGet get(int chunkX, int chunkZ) { @Override public void sendFakeChunk(@Nullable Player player, ChunkPacket packet) { - // TODO: Implement fake chunk sending + // Not supported on Nukkit: requires converting JE block states to BE runtime ID palettes + // and serializing into Nukkit's LevelChunkPacket format (PalettedBlockStorage + 3D biomes). + // Affects non-critical features like clipboard visualization previews. } @Override @@ -277,7 +286,7 @@ public boolean tile(int x, int y, int z, FaweCompoundTag tile) throws WorldEditE if (chunk == null) { return false; } - cn.nukkit.nbt.tag.CompoundTag nbt = com.fastasyncworldedit.nukkitmot.NukkitNbtConverter.toNukkit(tile); + cn.nukkit.nbt.tag.CompoundTag nbt = com.fastasyncworldedit.nukkit.NukkitNbtConverter.toNukkit(tile); nbt.putInt("x", x); nbt.putInt("y", y); nbt.putInt("z", z); diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitWorldEditListener.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitWorldEditListener.java similarity index 99% rename from worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitWorldEditListener.java rename to worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitWorldEditListener.java index f410313389..6a5172ab15 100644 --- a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/NukkitWorldEditListener.java +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitWorldEditListener.java @@ -1,4 +1,4 @@ -package com.sk89q.worldedit.nukkitmot; +package com.sk89q.worldedit.nukkit; import cn.nukkit.Player; import cn.nukkit.block.Block; diff --git a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/WorldEditNukkitPlugin.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/WorldEditNukkitPlugin.java similarity index 78% rename from worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/WorldEditNukkitPlugin.java rename to worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/WorldEditNukkitPlugin.java index abd25b109a..6236389a13 100644 --- a/worldedit-nukkit-mot/src/main/java/com/sk89q/worldedit/nukkitmot/WorldEditNukkitPlugin.java +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/WorldEditNukkitPlugin.java @@ -1,11 +1,13 @@ -package com.sk89q.worldedit.nukkitmot; +package com.sk89q.worldedit.nukkit; import cn.nukkit.plugin.PluginBase; import com.fastasyncworldedit.core.Fawe; -import com.fastasyncworldedit.nukkitmot.FaweNukkit; -import com.fastasyncworldedit.nukkitmot.mapping.BiomeMapping; -import com.fastasyncworldedit.nukkitmot.mapping.BlockMapping; -import com.fastasyncworldedit.nukkitmot.mapping.ItemMapping; +import com.fastasyncworldedit.nukkit.FaweNukkit; +import com.fastasyncworldedit.nukkit.adapter.NukkitImplAdapter; +import com.fastasyncworldedit.nukkit.adapter.NukkitImplLoader; +import com.fastasyncworldedit.nukkit.mapping.BiomeMapping; +import com.fastasyncworldedit.nukkit.mapping.BlockMapping; +import com.fastasyncworldedit.nukkit.mapping.ItemMapping; import com.sk89q.worldedit.LocalConfiguration; import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.event.platform.PlatformReadyEvent; @@ -17,7 +19,7 @@ import com.sk89q.worldedit.world.weather.WeatherTypes; /** - * FastAsyncWorldEdit plugin entry point for Nukkit-MOT. + * FastAsyncWorldEdit plugin entry point for Nukkit. */ public class WorldEditNukkitPlugin extends PluginBase { @@ -29,6 +31,10 @@ public class WorldEditNukkitPlugin extends PluginBase { public void onLoad() { instance = this; + // Detect and load platform adapter (MOT vs NKX) + NukkitImplAdapter adapter = NukkitImplLoader.detect(); + getLogger().info("Detected Nukkit platform: " + adapter.getPlatformName()); + // Create data folder getDataFolder().mkdirs(); @@ -82,29 +88,32 @@ public void onEnable() { // Signal platform is ready worldEdit.getEventBus().post(new PlatformReadyEvent(platform)); - getLogger().info("FastAsyncWorldEdit for Nukkit-MOT enabled."); + getLogger().info("FastAsyncWorldEdit for Nukkit enabled."); } @Override public void onDisable() { - WorldEdit worldEdit = WorldEdit.getInstance(); + // FAWE cleanup (may be null if onEnable failed early) + Fawe fawe = Fawe.instance(); + if (fawe != null) { + fawe.onDisable(); + } - // FAWE cleanup - Fawe.instance().onDisable(); + WorldEdit worldEdit = WorldEdit.getInstance(); // Unload sessions worldEdit.getSessionManager().unload(); - // Signal platform is going down - worldEdit.getEventBus().post(new PlatformUnreadyEvent(platform)); - - // Unregister platform - worldEdit.getPlatformManager().unregister(platform); + // Signal platform is going down and unregister + if (platform != null) { + worldEdit.getEventBus().post(new PlatformUnreadyEvent(platform)); + worldEdit.getPlatformManager().unregister(platform); + } // Cancel scheduled tasks getServer().getScheduler().cancelTask(this); - getLogger().info("FastAsyncWorldEdit for Nukkit-MOT disabled."); + getLogger().info("FastAsyncWorldEdit for Nukkit disabled."); } private boolean checkSQLiteDriver() { diff --git a/worldedit-nukkit-mot/src/main/resources/plugin.yml b/worldedit-nukkit/src/main/resources/plugin.yml similarity index 81% rename from worldedit-nukkit-mot/src/main/resources/plugin.yml rename to worldedit-nukkit/src/main/resources/plugin.yml index eb02f3fe92..5c2e7b2b1f 100644 --- a/worldedit-nukkit-mot/src/main/resources/plugin.yml +++ b/worldedit-nukkit/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: FastAsyncWorldEdit -main: com.sk89q.worldedit.nukkitmot.WorldEditNukkitPlugin +main: com.sk89q.worldedit.nukkit.WorldEditNukkitPlugin version: "${internalVersion}" api: ["1.0.0"] load: STARTUP From c78fe2cf70a39dd02e0b8ee52fb152f234387a60 Mon Sep 17 00:00:00 2001 From: LT_Name <572413378@qq.com> Date: Tue, 3 Mar 2026 21:59:54 +0800 Subject: [PATCH 10/12] fix: nkx compatibility issues --- worldedit-nukkit/build.gradle.kts | 2 +- .../nk-adapters/adapter-mot/build.gradle.kts | 17 +++ .../nukkit/adapter/mot/MotNukkitAdapter.java | 96 +++++++++++++++ .../nk-adapters/adapter-nkx/build.gradle.kts | 21 ++++ .../nukkit/adapter/nkx/NkxNukkitAdapter.java | 114 ++++++++++++++++++ .../nukkit/adapter/NukkitImplAdapter.java | 32 +++++ .../worldedit/nukkit/NukkitGetBlocks.java | 31 +++-- .../nukkit/NukkitGetBlocks_Copy.java | 6 +- .../sk89q/worldedit/nukkit/NukkitWorld.java | 14 ++- 9 files changed, 318 insertions(+), 15 deletions(-) create mode 100644 worldedit-nukkit/nk-adapters/adapter-mot/build.gradle.kts create mode 100644 worldedit-nukkit/nk-adapters/adapter-mot/src/main/java/com/fastasyncworldedit/nukkit/adapter/mot/MotNukkitAdapter.java create mode 100644 worldedit-nukkit/nk-adapters/adapter-nkx/build.gradle.kts create mode 100644 worldedit-nukkit/nk-adapters/adapter-nkx/src/main/java/com/fastasyncworldedit/nukkit/adapter/nkx/NkxNukkitAdapter.java diff --git a/worldedit-nukkit/build.gradle.kts b/worldedit-nukkit/build.gradle.kts index 1112c21e5c..0021a931f4 100644 --- a/worldedit-nukkit/build.gradle.kts +++ b/worldedit-nukkit/build.gradle.kts @@ -60,7 +60,7 @@ dependencies { api(project(":worldedit-core")) api(project(":worldedit-libs:nukkit")) - compileOnly("cn.nukkit:Nukkit:MOT-SNAPSHOT") + compileOnly("cn.nukkit:nukkit:1.0-SNAPSHOT") implementation(libs.fastutil) implementation(libs.gson) diff --git a/worldedit-nukkit/nk-adapters/adapter-mot/build.gradle.kts b/worldedit-nukkit/nk-adapters/adapter-mot/build.gradle.kts new file mode 100644 index 0000000000..23f58d3534 --- /dev/null +++ b/worldedit-nukkit/nk-adapters/adapter-mot/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + `java-library` +} + +repositories { + mavenLocal() + mavenCentral() + maven { + name = "repo-lanink-cn" + url = uri("https://repo.lanink.cn/repository/maven-public/") + } +} + +dependencies { + compileOnly(project(":worldedit-nukkit")) + compileOnly("cn.nukkit:Nukkit:MOT-SNAPSHOT") +} diff --git a/worldedit-nukkit/nk-adapters/adapter-mot/src/main/java/com/fastasyncworldedit/nukkit/adapter/mot/MotNukkitAdapter.java b/worldedit-nukkit/nk-adapters/adapter-mot/src/main/java/com/fastasyncworldedit/nukkit/adapter/mot/MotNukkitAdapter.java new file mode 100644 index 0000000000..2663d8808d --- /dev/null +++ b/worldedit-nukkit/nk-adapters/adapter-mot/src/main/java/com/fastasyncworldedit/nukkit/adapter/mot/MotNukkitAdapter.java @@ -0,0 +1,96 @@ +package com.fastasyncworldedit.nukkit.adapter.mot; + +import cn.nukkit.Player; +import cn.nukkit.block.Block; +import cn.nukkit.entity.Entity; +import cn.nukkit.level.GlobalBlockPalette; +import cn.nukkit.level.Level; +import cn.nukkit.level.format.FullChunk; +import cn.nukkit.level.format.leveldb.BlockStateMapping; +import cn.nukkit.level.format.leveldb.NukkitLegacyMapper; +import cn.nukkit.level.format.leveldb.structure.BlockStateSnapshot; +import cn.nukkit.level.generator.object.tree.ObjectCherryTree; +import cn.nukkit.level.generator.object.tree.ObjectMangroveTree; +import cn.nukkit.level.generator.object.tree.ObjectPaleOakTree; +import cn.nukkit.math.NukkitRandom; +import cn.nukkit.math.Vector3; +import cn.nukkit.utils.Identifier; +import com.fastasyncworldedit.nukkit.adapter.NukkitImplAdapter; +import org.cloudburstmc.nbt.NbtMap; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.UUID; + +/** + * Adapter implementation for the Nukkit-MOT platform. + */ +public class MotNukkitAdapter implements NukkitImplAdapter { + + @Override + public String getPlatformName() { + return "Nukkit-MOT"; + } + + @Override + public int getBlockDataBits() { + return Block.DATA_BITS; + } + + @Override + @Nullable + public String getPlayerLanguageCode(Player player) { + var langCode = player.getLanguageCode(); + return langCode != null ? langCode.name() : null; + } + + @Override + public int getBlockRuntimeId(Player player, int blockId, int meta) { + return GlobalBlockPalette.getOrCreateRuntimeId(player.getGameVersion(), blockId, meta); + } + + @Override + @Nullable + public String getEntityIdentifier(Entity entity) { + Identifier identifier = entity.getIdentifier(); + return identifier != null ? identifier.toString() : null; + } + + @Override + public List loadBlockPalette() { + return NukkitLegacyMapper.loadBlockPalette(); + } + + @Override + @Nullable + public BlockStateSnapshot getBlockStateSnapshot(NbtMap nbtState) { + return BlockStateMapping.get().getStateUnsafe(nbtState); + } + + @Override + public boolean generateTree(String treeType, Level level, int x, int y, int z, NukkitRandom random, Vector3 pos) { + // MOT: Mangrove, Cherry, PaleOak all extend TreeGenerator + return switch (treeType) { + case "MANGROVE" -> new ObjectMangroveTree().generate(level, random, pos); + case "CHERRY" -> new ObjectCherryTree().generate(level, random, pos); + case "PALE_OAK" -> new ObjectPaleOakTree().generate(level, random, pos); + default -> false; + }; + } + + @Override + public int getBlockId(FullChunk chunk, int x, int y, int z, int layer) { + return chunk.getBlockId(x, y, z, layer); + } + + @Override + public void setFullBlockId(FullChunk chunk, int x, int y, int z, int layer, int fullId) { + chunk.setFullBlockId(x, y, z, layer, fullId); + } + + @Override + public UUID getEntityUUID(Entity entity) { + return entity.getUniqueId(); + } + +} diff --git a/worldedit-nukkit/nk-adapters/adapter-nkx/build.gradle.kts b/worldedit-nukkit/nk-adapters/adapter-nkx/build.gradle.kts new file mode 100644 index 0000000000..8c695ffa01 --- /dev/null +++ b/worldedit-nukkit/nk-adapters/adapter-nkx/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + `java-library` +} + +repositories { + mavenLocal() + mavenCentral() + maven { + name = "OpenCollab Releases" + url = uri("https://repo.opencollab.dev/maven-releases/") + } + maven { + name = "OpenCollab Snapshots" + url = uri("https://repo.opencollab.dev/maven-snapshots/") + } +} + +dependencies { + compileOnly(project(":worldedit-nukkit")) + compileOnly("cn.nukkit:nukkit:1.0-SNAPSHOT") +} diff --git a/worldedit-nukkit/nk-adapters/adapter-nkx/src/main/java/com/fastasyncworldedit/nukkit/adapter/nkx/NkxNukkitAdapter.java b/worldedit-nukkit/nk-adapters/adapter-nkx/src/main/java/com/fastasyncworldedit/nukkit/adapter/nkx/NkxNukkitAdapter.java new file mode 100644 index 0000000000..538f252899 --- /dev/null +++ b/worldedit-nukkit/nk-adapters/adapter-nkx/src/main/java/com/fastasyncworldedit/nukkit/adapter/nkx/NkxNukkitAdapter.java @@ -0,0 +1,114 @@ +package com.fastasyncworldedit.nukkit.adapter.nkx; + +import cn.nukkit.Player; +import cn.nukkit.block.Block; +import cn.nukkit.block.BlockLayer; +import cn.nukkit.entity.Entity; +import cn.nukkit.entity.EntityHuman; +import cn.nukkit.level.GlobalBlockPalette; +import cn.nukkit.level.Level; +import cn.nukkit.level.format.FullChunk; +import cn.nukkit.level.format.leveldb.BlockStateMapping; +import cn.nukkit.level.format.leveldb.NukkitLegacyMapper; +import cn.nukkit.level.format.leveldb.structure.BlockStateSnapshot; +import cn.nukkit.level.generator.object.tree.ObjectCherryTree; +import cn.nukkit.level.generator.object.tree.ObjectMangroveTree; +import cn.nukkit.math.NukkitRandom; +import cn.nukkit.math.Vector3; +import com.fastasyncworldedit.nukkit.adapter.NukkitImplAdapter; +import org.cloudburstmc.nbt.NbtMap; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.UUID; + +/** + * Adapter implementation for the NKX (upstream Nukkit) platform. + */ +public class NkxNukkitAdapter implements NukkitImplAdapter { + + @Override + public String getPlatformName() { + return "NKX"; + } + + @Override + public int getBlockDataBits() { + return Block.DATA_BITS; + } + + @Override + @Nullable + public String getPlayerLanguageCode(Player player) { + java.util.Locale locale = player.getLocale(); + if (locale == null) { + return null; + } + return locale.toString(); + } + + @Override + public int getBlockRuntimeId(Player player, int blockId, int meta) { + return GlobalBlockPalette.getOrCreateRuntimeId(blockId, meta); + } + + @Override + @Nullable + public String getEntityIdentifier(Entity entity) { + int networkId = entity.getNetworkId(); + if (networkId == -1) { + return null; + } + String saveId = entity.getSaveId(); + if (saveId != null && !saveId.isEmpty()) { + return saveId.contains(":") ? saveId : "minecraft:" + saveId; + } + return null; + } + + @Override + public List loadBlockPalette() { + return NukkitLegacyMapper.loadBlockPalette(); + } + + @Override + @Nullable + public BlockStateSnapshot getBlockStateSnapshot(NbtMap nbtState) { + return BlockStateMapping.get().getStateUnsafe(nbtState); + } + + @Override + public boolean generateTree(String treeType, Level level, int x, int y, int z, NukkitRandom random, Vector3 pos) { + // NKX: Mangrove and Cherry extend ObjectTree; PaleOak does not exist + switch (treeType) { + case "MANGROVE" -> new ObjectMangroveTree().placeObject(level, x, y, z, random); + case "CHERRY" -> new ObjectCherryTree().placeObject(level, x, y, z, random); + default -> { + return false; + } + } + return true; + } + + private static final BlockLayer[] LAYERS = {BlockLayer.NORMAL, BlockLayer.WATERLOGGED}; + + @Override + public int getBlockId(FullChunk chunk, int x, int y, int z, int layer) { + return chunk.getBlockId(x, y, z, LAYERS[layer]); + } + + @Override + public void setFullBlockId(FullChunk chunk, int x, int y, int z, int layer, int fullId) { + chunk.setFullBlockId(x, y, z, LAYERS[layer], fullId); + } + + @Override + public UUID getEntityUUID(Entity entity) { + // NKX: only EntityHuman (Player) has getUniqueId(); for other entities derive from getId() + if (entity instanceof EntityHuman human) { + return human.getUniqueId(); + } + return new UUID(0, entity.getId()); + } + +} diff --git a/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/adapter/NukkitImplAdapter.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/adapter/NukkitImplAdapter.java index 32a8552add..8ce165458a 100644 --- a/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/adapter/NukkitImplAdapter.java +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/adapter/NukkitImplAdapter.java @@ -2,11 +2,16 @@ import cn.nukkit.Player; import cn.nukkit.entity.Entity; +import cn.nukkit.level.Level; +import cn.nukkit.level.format.FullChunk; import cn.nukkit.level.format.leveldb.structure.BlockStateSnapshot; +import cn.nukkit.math.NukkitRandom; +import cn.nukkit.math.Vector3; import org.cloudburstmc.nbt.NbtMap; import javax.annotation.Nullable; import java.util.List; +import java.util.UUID; /** * Adapter interface abstracting API differences between Nukkit-MOT and NKX. @@ -67,4 +72,31 @@ default int getBlockDataMask() { */ @Nullable BlockStateSnapshot getBlockStateSnapshot(NbtMap nbtState); + + /** + * Generate a tree whose class hierarchy differs between MOT and NKX. + * Handles: Mangrove, Cherry, PaleOak. + * + * @return {@code true} if the tree was generated, {@code false} if the type is unsupported + */ + boolean generateTree(String treeType, Level level, int x, int y, int z, NukkitRandom random, Vector3 pos); + + /** + * Get block ID at the given layer. + * MOT uses {@code int} layer, NKX uses {@code BlockLayer} enum. + */ + int getBlockId(FullChunk chunk, int x, int y, int z, int layer); + + /** + * Set full block ID at the given layer. + * MOT uses {@code int} layer, NKX uses {@code BlockLayer} enum. + */ + void setFullBlockId(FullChunk chunk, int x, int y, int z, int layer, int fullId); + + /** + * Get a UUID for the given entity. + * MOT: all entities have {@code Entity.getUniqueId()}. + * NKX: only {@code EntityHuman} has it; for other entities, derive from {@code Entity.getId()}. + */ + UUID getEntityUUID(Entity entity); } diff --git a/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitGetBlocks.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitGetBlocks.java index e9997e163e..33413cce28 100644 --- a/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitGetBlocks.java +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitGetBlocks.java @@ -106,7 +106,7 @@ public BlockState getBlock(int x, int y, int z) { BlockState state = BlockTypesCache.states[ordinal]; // Check layer 1 for waterlogged if (state.getBlockType().hasProperty(PropertyKey.WATERLOGGED)) { - int layer1Id = chunk.getBlockId(x & 0xF, y, z & 0xF, 1); + int layer1Id = NukkitImplLoader.get().getBlockId(chunk, x & 0xF, y, z & 0xF, 1); if (layer1Id == WATER_ID || layer1Id == STILL_WATER_ID) { state = state.with(PropertyKey.WATERLOGGED, true); } @@ -137,7 +137,7 @@ public char[] update(int layer, char[] data, boolean aggressive) { // Apply waterlogged state from layer 1 BlockState state = BlockTypesCache.states[ordinal]; if (state != null && state.getBlockType().hasProperty(PropertyKey.WATERLOGGED)) { - int layer1Id = chunk.getBlockId(x, baseY + y, z, 1); + int layer1Id = NukkitImplLoader.get().getBlockId(chunk, x, baseY + y, z, 1); if (layer1Id == WATER_ID || layer1Id == STILL_WATER_ID) { state = state.with(PropertyKey.WATERLOGGED, true); ordinal = state.getOrdinalChar(); @@ -185,7 +185,7 @@ public > T call(IQueueExtent owner, IChunk // Apply waterlogged state from layer 1 BlockState state = BlockTypesCache.states[ordinal]; if (state != null && state.getBlockType().hasProperty(PropertyKey.WATERLOGGED)) { - int layer1Id = chunk.getBlockId(x, baseY + y, z, 1); + int layer1Id = NukkitImplLoader.get().getBlockId(chunk, x, baseY + y, z, 1); if (layer1Id == WATER_ID || layer1Id == STILL_WATER_ID) { state = state.with(PropertyKey.WATERLOGGED, true); ordinal = state.getOrdinalChar(); @@ -260,15 +260,15 @@ public > T call(IQueueExtent owner, IChunk int fullId = BlockMapping.jeOrdinalToFullId(ordinal); int blockId = fullId >> dataBits; int meta = fullId & dataMask; - chunk.setFullBlockId(x, baseY + y, z, 0, + adapter.setFullBlockId(chunk, x, baseY + y, z, 0, (blockId << dataBits) | meta); // Set or clear layer 1 water if (waterlogged) { - chunk.setFullBlockId(x, baseY + y, z, 1, + adapter.setFullBlockId(chunk, x, baseY + y, z, 1, STILL_WATER_ID << dataBits); } else if (state != null && state.getBlockType().hasProperty(PropertyKey.WATERLOGGED)) { // Clear water from layer 1 if block supports waterlogged but isn't - chunk.setFullBlockId(x, baseY + y, z, 1, 0); + adapter.setFullBlockId(chunk, x, baseY + y, z, 1, 0); } } } @@ -319,16 +319,18 @@ public > T call(IQueueExtent owner, IChunk if (entityRemoves != null && !entityRemoves.isEmpty()) { Map chunkEntities = level.getChunkEntities(chunkX, chunkZ); Set entitiesRemoved = new HashSet<>(); + NukkitImplAdapter uuidAdapter = NukkitImplLoader.get(); for (cn.nukkit.entity.Entity entity : chunkEntities.values()) { if (entity instanceof cn.nukkit.Player) { continue; } - if (entityRemoves.contains(entity.getUniqueId())) { + UUID entityUUID = uuidAdapter.getEntityUUID(entity); + if (entityRemoves.contains(entityUUID)) { if (copy != null) { - copy.storeEntity(entity); + copy.storeEntity(entity, entityUUID); } entity.close(); - entitiesRemoved.add(entity.getUniqueId()); + entitiesRemoved.add(entityUUID); } } set.getEntityRemoves().clear(); @@ -434,12 +436,17 @@ public Collection entities() { if (chunkEntities.isEmpty()) { return Collections.emptyList(); } + NukkitImplAdapter adapter = NukkitImplLoader.get(); List result = new ArrayList<>(); for (cn.nukkit.entity.Entity entity : chunkEntities.values()) { if (entity instanceof cn.nukkit.Player) { continue; } entity.saveNBT(); + // Ensure UUID is stored in NBT (NKX entities don't save it by default) + if (!entity.namedTag.contains("uuid")) { + entity.namedTag.putString("uuid", adapter.getEntityUUID(entity).toString()); + } result.add(NukkitNbtConverter.toFawe(entity.namedTag)); } return result; @@ -465,12 +472,16 @@ public Set getFullEntities() { @Override public FaweCompoundTag entity(UUID uuid) { Map chunkEntities = level.getChunkEntities(chunkX, chunkZ); + NukkitImplAdapter adapter = NukkitImplLoader.get(); for (cn.nukkit.entity.Entity entity : chunkEntities.values()) { if (entity instanceof cn.nukkit.Player) { continue; } - if (uuid.equals(entity.getUniqueId())) { + if (uuid.equals(adapter.getEntityUUID(entity))) { entity.saveNBT(); + if (!entity.namedTag.contains("uuid")) { + entity.namedTag.putString("uuid", uuid.toString()); + } return NukkitNbtConverter.toFawe(entity.namedTag); } } diff --git a/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitGetBlocks_Copy.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitGetBlocks_Copy.java index bafa9552fd..a5f4bcd67c 100644 --- a/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitGetBlocks_Copy.java +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitGetBlocks_Copy.java @@ -75,8 +75,12 @@ protected void storeTile(BlockVector3 pos, FaweCompoundTag tag) { tiles.put(pos, tag); } - protected void storeEntity(cn.nukkit.entity.Entity entity) { + protected void storeEntity(cn.nukkit.entity.Entity entity, java.util.UUID entityUUID) { entity.saveNBT(); + // Ensure UUID is stored in NBT (NKX entities don't save it by default) + if (!entity.namedTag.contains("uuid")) { + entity.namedTag.putString("uuid", entityUUID.toString()); + } entities.add(NukkitNbtConverter.toFawe(entity.namedTag)); } diff --git a/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitWorld.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitWorld.java index f89f6265b7..07129aed40 100644 --- a/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitWorld.java +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitWorld.java @@ -189,15 +189,23 @@ public boolean generateTree(TreeGenerator.TreeType type, EditSession editSession case SWAMP -> new cn.nukkit.level.generator.object.tree.ObjectSwampTree(); case ACACIA -> new cn.nukkit.level.generator.object.tree.ObjectSavannaTree(); case DARK_OAK -> new cn.nukkit.level.generator.object.tree.ObjectDarkOakTree(); - case MANGROVE, TALL_MANGROVE -> new cn.nukkit.level.generator.object.tree.ObjectMangroveTree(); - case CHERRY -> new cn.nukkit.level.generator.object.tree.ObjectCherryTree(); - case PALE_OAK, PALE_OAK_CREAKING -> new cn.nukkit.level.generator.object.tree.ObjectPaleOakTree(); default -> null; }; if (treeGen != null) { return treeGen.generate(level, random, pos); } + // Trees with divergent class hierarchies between MOT and NKX — delegate to adapter + String adapterType = switch (type) { + case MANGROVE, TALL_MANGROVE -> "MANGROVE"; + case CHERRY -> "CHERRY"; + case PALE_OAK, PALE_OAK_CREAKING -> "PALE_OAK"; + default -> null; + }; + if (adapterType != null) { + return NukkitImplLoader.get().generateTree(adapterType, level, x, y, z, random, pos); + } + return false; } From 14e5483cf91579c83981aefd4dc0343aefdff3de Mon Sep 17 00:00:00 2001 From: LT_Name <572413378@qq.com> Date: Thu, 5 Mar 2026 22:42:07 +0800 Subject: [PATCH 11/12] refactor(core): reformat code and improve structure --- worldedit-nukkit/build.gradle.kts | 4 +-- .../nukkit/adapter/nkx/NkxNukkitAdapter.java | 7 +++-- .../nukkit/adapter/NukkitImplAdapter.java | 1 + .../nukkit/mapping/BiomeMapping.java | 1 + .../nukkit/mapping/BlockMapping.java | 25 ++++++++++++----- .../nukkit/mapping/ItemMapping.java | 2 ++ .../nukkit/mapping/JeBlockState.java | 18 ++++++------ .../sk89q/worldedit/nukkit/NukkitEntity.java | 6 ++-- .../worldedit/nukkit/NukkitGetBlocks.java | 12 +++++--- .../nukkit/NukkitGetBlocks_Copy.java | 4 +-- .../sk89q/worldedit/nukkit/NukkitPlayer.java | 6 ++-- .../worldedit/nukkit/NukkitRegistries.java | 8 +++--- .../nukkit/NukkitServerInterface.java | 6 ++-- .../sk89q/worldedit/nukkit/NukkitWorld.java | 22 +++++++-------- .../nukkit/NukkitWorldEditListener.java | 28 +++++++++---------- .../nukkit/WorldEditNukkitPlugin.java | 8 +++--- .../src/main/resources/plugin.yml | 4 +-- 17 files changed, 94 insertions(+), 68 deletions(-) diff --git a/worldedit-nukkit/build.gradle.kts b/worldedit-nukkit/build.gradle.kts index 0021a931f4..518f7ce8d1 100644 --- a/worldedit-nukkit/build.gradle.kts +++ b/worldedit-nukkit/build.gradle.kts @@ -100,8 +100,8 @@ tasks.named("processResources") { tasks.named("shadowJar") { archiveFileName.set("${rootProject.name}-Nukkit-${project.version}.${archiveExtension.getOrElse("jar")}") configurations = listOf( - project.configurations.getByName("runtimeClasspath"), - adapters + project.configurations.getByName("runtimeClasspath"), + adapters ) dependencies { include(dependency(":worldedit-core")) diff --git a/worldedit-nukkit/nk-adapters/adapter-nkx/src/main/java/com/fastasyncworldedit/nukkit/adapter/nkx/NkxNukkitAdapter.java b/worldedit-nukkit/nk-adapters/adapter-nkx/src/main/java/com/fastasyncworldedit/nukkit/adapter/nkx/NkxNukkitAdapter.java index 538f252899..491ea2c0e2 100644 --- a/worldedit-nukkit/nk-adapters/adapter-nkx/src/main/java/com/fastasyncworldedit/nukkit/adapter/nkx/NkxNukkitAdapter.java +++ b/worldedit-nukkit/nk-adapters/adapter-nkx/src/main/java/com/fastasyncworldedit/nukkit/adapter/nkx/NkxNukkitAdapter.java @@ -20,6 +20,7 @@ import javax.annotation.Nullable; import java.util.List; +import java.util.Locale; import java.util.UUID; /** @@ -27,6 +28,8 @@ */ public class NkxNukkitAdapter implements NukkitImplAdapter { + private static final BlockLayer[] LAYERS = {BlockLayer.NORMAL, BlockLayer.WATERLOGGED}; + @Override public String getPlatformName() { return "NKX"; @@ -40,7 +43,7 @@ public int getBlockDataBits() { @Override @Nullable public String getPlayerLanguageCode(Player player) { - java.util.Locale locale = player.getLocale(); + Locale locale = player.getLocale(); if (locale == null) { return null; } @@ -90,8 +93,6 @@ public boolean generateTree(String treeType, Level level, int x, int y, int z, N return true; } - private static final BlockLayer[] LAYERS = {BlockLayer.NORMAL, BlockLayer.WATERLOGGED}; - @Override public int getBlockId(FullChunk chunk, int x, int y, int z, int layer) { return chunk.getBlockId(x, y, z, LAYERS[layer]); diff --git a/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/adapter/NukkitImplAdapter.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/adapter/NukkitImplAdapter.java index 8ce165458a..36bbd19a8c 100644 --- a/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/adapter/NukkitImplAdapter.java +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/adapter/NukkitImplAdapter.java @@ -99,4 +99,5 @@ default int getBlockDataMask() { * NKX: only {@code EntityHuman} has it; for other entities, derive from {@code Entity.getId()}. */ UUID getEntityUUID(Entity entity); + } diff --git a/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/BiomeMapping.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/BiomeMapping.java index 8577071a90..36f4f66823 100644 --- a/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/BiomeMapping.java +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/BiomeMapping.java @@ -83,6 +83,7 @@ private record BiomeEntry( @SerializedName("bedrock_id") int bedrockId ) { + } } diff --git a/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/BlockMapping.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/BlockMapping.java index db9d11fde7..21515dac64 100644 --- a/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/BlockMapping.java +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/BlockMapping.java @@ -125,7 +125,8 @@ public static void buildOrdinalMappings() { } } - WorldEditNukkitPlugin.getInstance().getLogger().info("Ordinal mappings built: " + mapped + " mapped, " + unmapped + " unmapped out of " + states.length + " total states"); + WorldEditNukkitPlugin.getInstance().getLogger().info( + "Ordinal mappings built: " + mapped + " mapped, " + unmapped + " unmapped out of " + states.length + " total states"); } /** @@ -209,15 +210,18 @@ private static boolean initJeBlockDefaultProperties() { return false; } - Map>> data = from(stream, new TypeToken<>() { - }); + Map>> data = from( + stream, new TypeToken<>() { + } + ); for (var entry : data.entrySet()) { JE_BLOCK_DEFAULT_PROPERTIES.put( "minecraft:" + entry.getKey(), (Map) entry.getValue().get(1) ); } - WorldEditNukkitPlugin.getInstance().getLogger().info("Loaded " + JE_BLOCK_DEFAULT_PROPERTIES.size() + " JE block default properties"); + WorldEditNukkitPlugin.getInstance().getLogger().info( + "Loaded " + JE_BLOCK_DEFAULT_PROPERTIES.size() + " JE block default properties"); } catch (IOException e) { WorldEditNukkitPlugin.getInstance().getLogger().error("Failed to load je_blocks.json: ", e); return false; @@ -239,8 +243,10 @@ private static boolean initBlockStateMapping() { return false; } - Map> root = from(stream, new TypeToken<>() { - }); + Map> root = from( + stream, new TypeToken<>() { + } + ); List mappings = root.get("mappings"); int mapped = 0; int failed = 0; @@ -256,7 +262,8 @@ private static boolean initBlockStateMapping() { failed++; } } - WorldEditNukkitPlugin.getInstance().getLogger().info("Block state mapping loaded: " + mapped + " mapped, " + failed + " failed"); + WorldEditNukkitPlugin.getInstance().getLogger().info( + "Block state mapping loaded: " + mapped + " mapped, " + failed + " failed"); } catch (IOException e) { WorldEditNukkitPlugin.getInstance().getLogger().error("Failed to load blocks.json: ", e); return false; @@ -324,6 +331,7 @@ public record BlockMappingEntry( @SerializedName("bedrock_state") BedrockState bedrockState ) { + public record JavaState( @SerializedName("Name") String name, @@ -331,6 +339,7 @@ public record JavaState( @SerializedName("Properties") Map properties ) { + } public record BedrockState( @@ -339,7 +348,9 @@ public record BedrockState( @Nullable Map state ) { + } + } public static class IgnoreFailureTypeAdapterFactory implements TypeAdapterFactory { diff --git a/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/ItemMapping.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/ItemMapping.java index b34c8ee677..112610d26c 100644 --- a/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/ItemMapping.java +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/ItemMapping.java @@ -126,6 +126,7 @@ private static long beKey(int itemId, int metadata) { } public record NukkitItemData(int itemId, int metadata) { + } private record ItemEntry( @@ -134,6 +135,7 @@ private record ItemEntry( @SerializedName("bedrock_data") int bedrockData ) { + } } diff --git a/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/JeBlockState.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/JeBlockState.java index 1e37699d61..ac1563d4be 100644 --- a/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/JeBlockState.java +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/JeBlockState.java @@ -52,6 +52,15 @@ public static JeBlockState create(String identifier, TreeMap pro return new JeBlockState(identifier, properties); } + private static int fnv1a32(byte[] data) { + int hash = 0x811c9dc5; + for (byte b : data) { + hash ^= (b & 0xff); + hash *= 0x01000193; + } + return hash; + } + public String getIdentifier() { return identifier; } @@ -113,13 +122,4 @@ public String toString() { return sb.append("]").toString(); } - private static int fnv1a32(byte[] data) { - int hash = 0x811c9dc5; - for (byte b : data) { - hash ^= (b & 0xff); - hash *= 0x01000193; - } - return hash; - } - } diff --git a/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitEntity.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitEntity.java index a301926045..48bbb16d25 100644 --- a/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitEntity.java +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitEntity.java @@ -78,9 +78,11 @@ public BaseEntity getState() { entity.saveNBT(); cn.nukkit.nbt.tag.CompoundTag namedTag = entity.namedTag; if (namedTag != null) { - return new BaseEntity(type, LazyReference.computed( + return new BaseEntity( + type, LazyReference.computed( NukkitNbtConverter.toLinCompound(namedTag) - )); + ) + ); } return new BaseEntity(type); } diff --git a/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitGetBlocks.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitGetBlocks.java index 33413cce28..c166afae29 100644 --- a/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitGetBlocks.java +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitGetBlocks.java @@ -260,12 +260,16 @@ public > T call(IQueueExtent owner, IChunk int fullId = BlockMapping.jeOrdinalToFullId(ordinal); int blockId = fullId >> dataBits; int meta = fullId & dataMask; - adapter.setFullBlockId(chunk, x, baseY + y, z, 0, - (blockId << dataBits) | meta); + adapter.setFullBlockId( + chunk, x, baseY + y, z, 0, + (blockId << dataBits) | meta + ); // Set or clear layer 1 water if (waterlogged) { - adapter.setFullBlockId(chunk, x, baseY + y, z, 1, - STILL_WATER_ID << dataBits); + adapter.setFullBlockId( + chunk, x, baseY + y, z, 1, + STILL_WATER_ID << dataBits + ); } else if (state != null && state.getBlockType().hasProperty(PropertyKey.WATERLOGGED)) { // Clear water from layer 1 if block supports waterlogged but isn't adapter.setFullBlockId(chunk, x, baseY + y, z, 1, 0); diff --git a/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitGetBlocks_Copy.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitGetBlocks_Copy.java index a5f4bcd67c..5d81c7cce7 100644 --- a/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitGetBlocks_Copy.java +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitGetBlocks_Copy.java @@ -40,9 +40,9 @@ public class NukkitGetBlocks_Copy implements IChunkGet { private final int minSectionPosition; private final int sectionCount; private final char[][] blocks; - private BiomeType[][] biomes; private final Map tiles = new HashMap<>(); private final Set entities = new HashSet<>(); + private BiomeType[][] biomes; public NukkitGetBlocks_Copy(int chunkX, int chunkZ, int minY, int maxY) { this.chunkX = chunkX; @@ -75,7 +75,7 @@ protected void storeTile(BlockVector3 pos, FaweCompoundTag tag) { tiles.put(pos, tag); } - protected void storeEntity(cn.nukkit.entity.Entity entity, java.util.UUID entityUUID) { + protected void storeEntity(cn.nukkit.entity.Entity entity, UUID entityUUID) { entity.saveNBT(); // Ensure UUID is stored in NBT (NKX entities don't save it by default) if (!entity.namedTag.contains("uuid")) { diff --git a/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitPlayer.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitPlayer.java index 82e3d6790b..a3e2511838 100644 --- a/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitPlayer.java +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitPlayer.java @@ -231,8 +231,10 @@ public > void sendFakeBlock(BlockVector3 pos, B bl if (block == null) { // Restore real block cn.nukkit.math.Vector3 vec = NukkitAdapter.adapt(pos); - level.sendBlocks(new Player[]{player}, new cn.nukkit.math.Vector3[]{vec}, - UpdateBlockPacket.FLAG_ALL); + level.sendBlocks( + new Player[]{player}, new cn.nukkit.math.Vector3[]{vec}, + UpdateBlockPacket.FLAG_ALL + ); } else { NukkitImplAdapter adapter = NukkitImplLoader.get(); int fullId = NukkitAdapter.adaptFullId(block.toImmutableState()); diff --git a/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitRegistries.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitRegistries.java index 7b18e6d417..7c9a0d6f9d 100644 --- a/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitRegistries.java +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitRegistries.java @@ -16,6 +16,10 @@ class NukkitRegistries extends BundledRegistries { NukkitRegistries() { } + public static NukkitRegistries getInstance() { + return INSTANCE; + } + @Override public BlockRegistry getBlockRegistry() { return blockRegistry; @@ -26,8 +30,4 @@ public ItemRegistry getItemRegistry() { return itemRegistry; } - public static NukkitRegistries getInstance() { - return INSTANCE; - } - } diff --git a/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitServerInterface.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitServerInterface.java index e20f2394f5..dbff9d5d48 100644 --- a/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitServerInterface.java +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitServerInterface.java @@ -11,6 +11,7 @@ import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.command.util.PermissionCondition; import com.sk89q.worldedit.entity.Player; +import com.sk89q.worldedit.event.platform.CommandEvent; import com.sk89q.worldedit.extension.platform.AbstractPlatform; import com.sk89q.worldedit.extension.platform.Actor; import com.sk89q.worldedit.extension.platform.Capability; @@ -25,6 +26,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.EnumMap; import java.util.List; @@ -208,7 +210,7 @@ private static class NukkitCommand extends Command { NukkitCommand(String name, String[] aliases, String[] permissions) { super(name); if (aliases.length > 1) { - setAliases(java.util.Arrays.copyOfRange(aliases, 1, aliases.length)); + setAliases(Arrays.copyOfRange(aliases, 1, aliases.length)); } if (permissions.length > 0) { setPermission(String.join(";", permissions)); @@ -232,7 +234,7 @@ public boolean execute(CommandSender sender, String label, String[] args) { } WorldEdit.getInstance().getEventBus().post( - new com.sk89q.worldedit.event.platform.CommandEvent(actor, commandLine) + new CommandEvent(actor, commandLine) ); return true; } diff --git a/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitWorld.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitWorld.java index 07129aed40..2b4844c8c3 100644 --- a/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitWorld.java +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitWorld.java @@ -321,17 +321,6 @@ public WeatherType getWeather() { return WeatherTypes.CLEAR; } - @Override - public long getRemainingWeatherDuration() { - Level level = getLevel(); - if (level.isThundering()) { - return level.getThunderTime(); - } else if (level.isRaining()) { - return level.getRainTime(); - } - return 0; - } - @Override public void setWeather(WeatherType weatherType) { Level level = getLevel(); @@ -347,6 +336,17 @@ public void setWeather(WeatherType weatherType) { } } + @Override + public long getRemainingWeatherDuration() { + Level level = getLevel(); + if (level.isThundering()) { + return level.getThunderTime(); + } else if (level.isRaining()) { + return level.getRainTime(); + } + return 0; + } + @Override public void setWeather(WeatherType weatherType, long duration) { setWeather(weatherType); diff --git a/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitWorldEditListener.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitWorldEditListener.java index 6a5172ab15..289036cfe1 100644 --- a/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitWorldEditListener.java +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitWorldEditListener.java @@ -39,6 +39,20 @@ public NukkitWorldEditListener(WorldEditNukkitPlugin plugin) { this.plugin = plugin; } + private static Direction adaptFace(BlockFace face) { + if (face == null) { + return Direction.UP; + } + return switch (face) { + case DOWN -> Direction.DOWN; + case UP -> Direction.UP; + case NORTH -> Direction.NORTH; + case SOUTH -> Direction.SOUTH; + case WEST -> Direction.WEST; + case EAST -> Direction.EAST; + }; + } + @EventHandler(priority = EventPriority.NORMAL) public void onPlayerInteract(PlayerInteractEvent event) { if (!plugin.getInternalPlatform().isHookingEvents()) { @@ -142,18 +156,4 @@ public void onPlayerQuit(PlayerQuitEvent event) { NukkitAdapter.uncachePlayer(nukkitPlayer); } - private static Direction adaptFace(BlockFace face) { - if (face == null) { - return Direction.UP; - } - return switch (face) { - case DOWN -> Direction.DOWN; - case UP -> Direction.UP; - case NORTH -> Direction.NORTH; - case SOUTH -> Direction.SOUTH; - case WEST -> Direction.WEST; - case EAST -> Direction.EAST; - }; - } - } diff --git a/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/WorldEditNukkitPlugin.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/WorldEditNukkitPlugin.java index 6236389a13..591908c647 100644 --- a/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/WorldEditNukkitPlugin.java +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/WorldEditNukkitPlugin.java @@ -27,6 +27,10 @@ public class WorldEditNukkitPlugin extends PluginBase { private NukkitConfiguration configuration; private NukkitServerInterface platform; + public static WorldEditNukkitPlugin getInstance() { + return instance; + } + @Override public void onLoad() { instance = this; @@ -158,8 +162,4 @@ NukkitServerInterface getInternalPlatform() { return platform; } - public static WorldEditNukkitPlugin getInstance() { - return instance; - } - } diff --git a/worldedit-nukkit/src/main/resources/plugin.yml b/worldedit-nukkit/src/main/resources/plugin.yml index 5c2e7b2b1f..a6037276f0 100644 --- a/worldedit-nukkit/src/main/resources/plugin.yml +++ b/worldedit-nukkit/src/main/resources/plugin.yml @@ -1,8 +1,8 @@ name: FastAsyncWorldEdit main: com.sk89q.worldedit.nukkit.WorldEditNukkitPlugin version: "${internalVersion}" -api: ["1.0.0"] +api: [ "1.0.0" ] load: STARTUP -softdepend: ["DbLib"] +softdepend: [ "DbLib" ] website: https://github.com/IntellectualSites/FastAsyncWorldEdit description: Blazingly fast world manipulation for builders, large networks and developers. From 7ae963faffdd3956bdebb2035b3d12882a15b273 Mon Sep 17 00:00:00 2001 From: LT_Name <572413378@qq.com> Date: Sat, 7 Mar 2026 09:44:01 +0800 Subject: [PATCH 12/12] remove test files --- .../src/main/resources/lang/zh/strings.json | 672 ------------------ 1 file changed, 672 deletions(-) delete mode 100644 worldedit-core/src/main/resources/lang/zh/strings.json diff --git a/worldedit-core/src/main/resources/lang/zh/strings.json b/worldedit-core/src/main/resources/lang/zh/strings.json deleted file mode 100644 index e2104ade4c..0000000000 --- a/worldedit-core/src/main/resources/lang/zh/strings.json +++ /dev/null @@ -1,672 +0,0 @@ -{ - "prefix": "&8(&4&lFAWE&8)&7 {0}", - "fawe.worldedit.history.find.element": "&2{0} {1} &7前 &3{2}分钟 &6{3} &c/{4}", - "fawe.worldedit.history.find.element.more": " - 变更: {0}\n - 范围: {1} -> {2}\n - 附加: {3}\n - 磁盘大小: {4}", - "fawe.worldedit.history.find.hover": "更改了 {0} 个方块,点击查看更多信息", - "fawe.worldedit.history.distr.summary_null": "无法找到输入的编辑摘要。", - "fawe.info.lighting.propagate.selection": "已在 {0} 个区块中传播光照。", - "fawe.info.updated.lighting.selection": "已在 {0} 个区块中更新光照。(数据包发送可能需要一点时间)", - "fawe.info.set.region": "选区已设置为你当前允许的区域", - "fawe.info.worldedit.toggle.tips.on": "已禁用 FAWE 提示。", - "fawe.info.worldedit.toggle.tips.off": "已启用 FAWE 提示。", - "fawe.info.worldedit.bypassed": "当前已绕过 FAWE 限制。", - "fawe.info.worldedit.restricted": "你的 FAWE 编辑现在受到限制。", - "fawe.info.worldedit.oom.admin": "可选方案:\n - //fast\n - 进行较小的编辑\n - 分配更多内存\n - 禁用 `max-memory-percent`", - "fawe.info.temporarily-not-working": "暂时无法使用", - "fawe.info.light-blocks": "光源方块比光源更可靠,请使用方块。此命令已弃用,将在未来版本中移除。", - "fawe.info.update-available.build": "FastAsyncWorldEdit 有可用更新。你落后了 {0} 个版本。\n你当前运行的是版本 {1},最新版本是 {2}。\n在 {3} 更新", - "fawe.info.update-available.release": "FastAsyncWorldEdit 有新版本可用: {0}。你当前使用的是 {1}。从 {2} 或 {3} 下载。", - "fawe.web.generating.link": "正在上传 {0},请稍候...", - "fawe.web.generating.link.failed": "生成下载链接失败!", - "fawe.web.download.link": "{0}", - "fawe.web.image.load.timeout": "图片加载超时,最长时间: {0}秒。请尝试使用较小分辨率的图片。", - "fawe.web.image.load.size.too-large": "图片尺寸过大!最大允许尺寸(宽 x 高): {0} 像素。", - "fawe.worldedit.general.texture.disabled": "纹理已重置", - "fawe.worldedit.general.texture.set": "纹理已设置为 {1}", - "fawe.worldedit.general.source.mask.disabled": "全局源遮罩已禁用", - "fawe.worldedit.general.source.mask": "全局源遮罩已设置", - "fawe.worldedit.general.transform.disabled": "全局变换已禁用", - "fawe.worldedit.general.transform": "全局变换已设置", - "fawe.worldedit.copy.command.copy": "已复制 {0} 个方块。", - "fawe.worldedit.cut.command.cut.lazy": "粘贴时将移除 {0} 个方块", - "fawe.worldedit.paste.command.paste": "剪贴板已粘贴到 {0}", - "fawe.worldedit.history.command.undo.disabled": "撤销已禁用,使用: //fast", - "fawe.worldedit.selection.selection.count": "统计了 {0} 个方块。", - "fawe.worldedit.anvil.world.is.loaded": "执行时世界不应处于使用状态。卸载世界,或使用 -f 参数覆盖(请先保存)", - "fawe.worldedit.brush.brush.reset": "已重置你的笔刷。(SHIFT + 点击)", - "fawe.worldedit.brush.brush.none": "你没有持有笔刷!", - "fawe.worldedit.brush.brush.scroll.action.set": "滚动操作已设置为 {0}", - "fawe.worldedit.brush.brush.scroll.action.unset": "已移除滚动操作", - "fawe.worldedit.brush.brush.visual.mode.set": "可视模式已设置为 {0}", - "fawe.worldedit.brush.brush.target.mode.set": "目标模式已设置为 {0}", - "fawe.worldedit.brush.brush.target.offset.set": "目标偏移已设置为 {0}", - "fawe.worldedit.brush.brush.equipped": "已装备笔刷 {0}", - "fawe.worldedit.brush.brush.try.other": "还有其他更合适的笔刷,例如:\n - //br height [radius=5] [#clipboard|file=null] [rotation=0] [yscale=1.00]", - "fawe.worldedit.brush.brush.copy": "左键点击对象底部进行复制,右键点击进行粘贴。如有必要请增大笔刷半径。", - "fawe.worldedit.brush.brush.height.invalid": "无效的高度图文件 ({0})", - "fawe.worldedit.brush.brush.spline": "点击添加一个点,在同一位置点击以完成", - "fawe.worldedit.brush.brush.line.primary": "已添加点 {0},点击另一个位置创建线条", - "fawe.worldedit.brush.brush.catenary.direction": "已添加点 {0},点击你想创建曲线的方向", - "fawe.worldedit.brush.brush.line.secondary": "已创建曲线", - "fawe.worldedit.brush.spline.primary.2": "已添加位置,在同一位置点击以连接!", - "fawe.worldedit.brush.brush.spline.secondary.error": "设置的位置不够!", - "fawe.worldedit.brush.spline.secondary": "已创建曲线", - "fawe.worldedit.brush.brush.source.mask.disabled": "笔刷源遮罩已禁用", - "fawe.worldedit.brush.brush.source.mask": "笔刷源遮罩已设置", - "fawe.worldedit.brush.brush.transform.disabled": "笔刷变换已禁用", - "fawe.worldedit.brush.brush.transform": "笔刷变换已设置", - "fawe.worldedit.rollback.rollingback.index": "正在撤销 {0} ...", - "fawe.worldedit.rollback.rollback.element": "已撤销 {0}。", - "fawe.worldedit.tool.tool.inspect": "检查工具已绑定到 {0}。", - "fawe.worldedit.tool.tool.inspect.info": "{0} 在 {3} 前将 {1} 更改为 {2}", - "fawe.worldedit.tool.tool.inspect.info.footer": "总计: {0} 次更改", - "fawe.worldedit.tool.tool.range.error": "最大范围: {0}。", - "fawe.worldedit.tool.tool.lrbuild.info": "左键设置为 {0};右键设置为 {1}。", - "fawe.worldedit.utility.nothing.confirmed": "你没有待确认的操作。", - "fawe.worldedit.schematic.schematic.move.exists": "{0} 已存在", - "fawe.worldedit.schematic.schematic.move.success": "{0} -> {1}", - "fawe.worldedit.schematic.schematic.move.failed": "{0} 未移动: {1}", - "fawe.worldedit.schematic.schematic.loaded": "已加载 {0}。使用 //paste 粘贴", - "fawe.worldedit.schematic.schematic.saved": "已保存 {0}。", - "fawe.worldedit.schematic.schematic.none": "未找到文件。", - "fawe.worldedit.schematic.schematic.load-failure": "文件无法读取或不存在: {0}。如果你指定了格式,可能指定的格式不正确。Sponge schematic v2 和 v3 都使用 .schem 文件扩展名。要让 FAWE 自动选择格式,请不要指定格式。如果你使用的是 litematica 原理图,则不受支持!", - "fawe.worldedit.clipboard.clipboard.uri.not.found": "你未加载 {0}", - "fawe.worldedit.clipboard.clipboard.cleared": "剪贴板已清除", - "fawe.worldedit.clipboard.clipboard.invalid.format": "未知的剪贴板格式: {0}", - "fawe.worldedit.visitor.visitor.block": "影响了 {0} 个方块", - "fawe.worldedit.selector.selector.fuzzy.pos1": "区域已从 {0} {1} 设置并扩展。", - "fawe.worldedit.selector.selector.fuzzy.pos2": "已添加 {0} {1} 的扩展。", - "fawe.progress.progress.message": "{1}/{0} ({2}%) @{3}个/秒 剩余{4}秒", - "fawe.progress.progress.finished": "[ 完成! ]", - "fawe.error.command.syntax": "用法: {0}", - "fawe.error.no-perm": "你缺少权限节点: {0}", - "fawe.error.block.not.allowed": "你不被允许使用: {0}", - "fawe.error.setting.disable": "缺少设置: {0}", - "fawe.error.brush.not.found": "可用笔刷: {0}", - "fawe.error.brush.incompatible": "笔刷与此版本不兼容", - "fawe.error.no.region": "你没有当前允许的区域", - "fawe.error.player.not.found": "未找到玩家: {0}", - "fawe.error.worldedit.some.fails": "有 {0} 个方块未被放置,因为它们在你允许的区域之外。", - "fawe.error.worldedit.some.fails.blockbag": "缺少方块: {0}", - "fawe.error.mask.angle": "无法将角度与方块步长组合", - "fawe.error.invalid-flag": "标志 {0} 在此处不适用", - "fawe.error.lighting": "处理光照时出错。你可能需要重新加载区块才能看到编辑效果。", - "fawe.error.parser.invalid-data": "无效数据: {0}", - "fawe.error.unsupported": "不支持!", - "fawe.error.invalid-block-type": "不匹配有效的方块类型: {0}", - "fawe.error.invalid-block-state-property": "无法解析属性 `{1}` 的值 `{0}`,方块状态: `{2}`", - "fawe.error.nbt.forbidden": "你不被允许使用 NBT。缺少权限: {0}", - "fawe.error.invalid-arguments": "无效的参数数量。预期: {0}", - "fawe.error.unrecognised-tag": "无法识别的标签: {0} {1}", - "fawe.error.unknown-block-tag": "未知的方块标签: {0}", - "fawe.error.block-tag-no-blocks": "方块标签 '{0}' 没有方块。", - "fawe.error.no-block-found": "未找到 '{0}' 对应的方块。", - "fawe.error.invalid-states": "无效状态: {0}", - "fawe.error.no-session": "没有可用的会话,因此没有可用的剪贴板。", - "fawe.error.empty-clipboard": "要使用 '{0}',请先将内容复制到剪贴板", - "fawe.error.selection-expand": "选区无法扩展。", - "fawe.error.selection-contract": "选区无法收缩。", - "fawe.error.selection-shift": "选区无法移动。", - "fawe.error.invalid-user": "必须提供用户。", - "fawe.error.radius-too-small": "半径必须 >=0", - "fawe.error.time-too-less": "时间必须 >=0", - "fawe.error.invalid-image": "无效图片: {0}", - "fawe.error.image-dimensions": "给定的图片尺寸过大,最大允许尺寸(宽 x 高): {0} 像素。", - "fawe.error.file-not-found": "未找到文件: {0}", - "fawe.error.file-is-invalid-directory": "文件是目录: {0}", - "fawe.error.stacktrace": "===============---=============", - "fawe.error.no-failure": "这不应该导致任何失败", - "fawe.error.invalid-bracketing": "无效的括号,你是否缺少 '{0}'。", - "fawe.error.too-simple": "复杂度必须在 0-100 范围内", - "fawe.error.outside-range": "参数 {0} 超出范围 {1}-{2}。", - "fawe.error.outside-range-lower": "参数 {0} 不能小于 {1}", - "fawe.error.outside-range-upper": "参数 {0} 不能大于 {1}", - "fawe.error.argument-size-mismatch": "参数 {0} 不能大于参数 {1}", - "fawe.error.input-parser-exception": "无效的空字符串代替布尔值。", - "fawe.error.invalid-boolean": "无效的布尔值 {0}", - "fawe.error.schematic.not.found": "未找到原理图 {0}。", - "fawe.error.parse.invalid-dangling-character": "无效的悬挂字符 {0}。", - "fawe.error.parse.unknown-mask": "未知遮罩: {0},参见: {1}", - "fawe.error.parse.unknown-pattern": "未知图案: {0},参见: {1}", - "fawe.error.parse.unknown-transform": "未知变换: {0},参见: {1}", - "fawe.error.parse.no-clipboard": "要使用 {0},请先将内容复制到剪贴板", - "fawe.error.parse.no-clipboard-source": "在给定的源中未找到剪贴板: {0}", - "fawe.error.clipboard.invalid": "====== 无效的剪贴板 ======", - "fawe.error.clipboard.invalid.info": "文件: {0} (长度: {1})", - "fawe.error.clipboard.load.failure": "从磁盘加载剪贴板时发生意外错误!", - "fawe.error.clipboard.on.disk.version.mismatch": "剪贴板版本不匹配: 预期 {0} 但得到 {1}。建议删除剪贴板文件夹并重启服务器。\n你的剪贴板文件夹位于 {2}。", - "fawe.error.limit.disallowed-block": "你的限制不允许使用方块 '{0}'", - "fawe.error.limit.disallowed-property": "你的限制不允许使用属性 '{0}'", - "fawe.error.region-mask-invalid": "无效的区域遮罩: {0}", - "fawe.error.occurred-continuing": "编辑期间发生可忽略的错误: {0}", - "fawe.error.limit.max-brush-radius": "限制中的最大笔刷半径: {0}", - "fawe.error.limit.max-radius": "限制中的最大半径: {0}", - "fawe.error.no-valid-on-hotbar": "快捷栏上没有有效的方块类型", - "fawe.error.no-process-non-synchronous-edit": "未找到处理器持有者,但编辑是非同步的", - "fawe.cancel.count": "已取消 {0} 个编辑。", - "fawe.cancel.reason.confirm": "使用 //confirm 执行 {0}", - "fawe.cancel.reason.confirm.region": "你的选区很大 ({0} -> {1},包含 {3} 个方块)。使用 //confirm 执行 {2}", - "fawe.cancel.reason.confirm.radius": "你的半径很大 ({0} > {1})。使用 //confirm 执行 {2}", - "fawe.cancel.reason.confirm.limit": "你超出了此操作的限制 ({0} > {1})。使用 //confirm 执行 {2}", - "fawe.cancel.reason": "你的 WorldEdit 操作已被取消: {0}。", - "fawe.cancel.reason.manual": "手动取消", - "fawe.cancel.reason.low.memory": "内存不足", - "fawe.cancel.reason.max.changes": "更改的方块过多", - "fawe.cancel.reason.max.checks": "检查的方块过多", - "fawe.cancel.reason.max.fails": "失败次数过多", - "fawe.cancel.reason.max.tiles": "方块实体过多", - "fawe.cancel.reason.max.entities": "实体过多", - "fawe.cancel.reason.max.iterations": "达到最大迭代次数", - "fawe.cancel.reason.outside.level": "超出世界范围", - "fawe.cancel.reason.outside.region": "超出允许区域(使用 /wea 绕过,或在 config.yml 中禁用 `region-restrictions`)", - "fawe.cancel.reason.outside.safe.region": "超出 +/- 30,000,000 方块的安全编辑区域。", - "fawe.cancel.reason.no.region": "没有允许的区域(使用 /wea 绕过,或在 config.yml 中禁用 `region-restrictions`)", - "fawe.cancel.reason.no.region.reason": "没有允许的区域: {0}", - "fawe.cancel.reason.no.region.plot.noworldeditflag": "地皮标志 NoWorldeditFlag 已设置", - "fawe.cancel.reason.no.region.plot.owner.offline": "区域所有者离线", - "fawe.cancel.reason.no.region.plot.owner.only": "只有区域所有者可以编辑", - "fawe.cancel.reason.no.region.not.added": "未被添加到区域", - "fawe.cancel.reason.player-only": "此操作需要玩家,不能从控制台执行,也不能没有操作者。", - "fawe.cancel.reason.actor-required": "此操作需要一个操作者。", - "fawe.cancel.reason.world.limit": "此操作无法在 y={0} 处执行,因为超出了世界限制。", - "fawe.cancel.worldedit.failed.load.chunk": "跳过加载区块: {0};{1}。尝试增加 chunk-wait。", - "fawe.navigation.no.block": "视线内没有方块!(或太远)", - "fawe.selection.sel.max": "最多 {0} 个点。", - "fawe.selection.sel.fuzzy": "模糊选择器: 左键点击选择所有相邻方块,右键点击添加。要选择空气空腔,请使用 //pos1。", - "fawe.selection.sel.fuzzy-instruction": "选择所有相连的方块(魔术棒)", - "fawe.selection.sel.convex.polyhedral": "凸多面体选择器: 左键=第一个顶点,右键添加更多。", - "fawe.selection.sel.polyhedral": "选择一个空心多面体", - "fawe.selection.sel.list": "要查看选区类型列表,请使用: //sel list", - "fawe.tips.tip.sel.list": "提示: 使用 //sel list 查看不同的选区模式", - "fawe.tips.tip.select.connected": "提示: 使用 //sel fuzzy 选择所有相连的方块", - "fawe.tips.tip.set.pos1": "提示: 使用 //set pos1 将 pos1 用作图案", - "fawe.tips.tip.farwand": "提示: 使用 //farwand 选择远处的点", - "fawe.tips.tip.discord": "需要 FAWE 使用帮助?https://discord.gg/intellectualsites", - "fawe.tips.tip.lazycut": "提示: 使用 //lazycut 更安全", - "fawe.tips.tip.fast": "提示: 使用 //fast 设置快速模式且不记录撤销", - "fawe.tips.tip.cancel": "提示: 你可以使用 //cancel 取消正在进行的编辑", - "fawe.tips.tip.mask": "提示: 使用 /gmask 设置全局目标遮罩", - "fawe.tips.tip.mask.angle": "提示: 使用 //replace /[-20][-3] bedrock 替换 3-20 方块的上坡", - "fawe.tips.tip.set.linear": "提示: 使用 //set #l3d[wood,bedrock] 线性设置方块", - "fawe.tips.tip.surface.spread": "提示: 使用 //set #surfacespread[5][0][5][#existing] 扩展平面", - "fawe.tips.tip.set.hand": "提示: 使用 //set hand 使用你当前手持的方块", - "fawe.tips.tip.replace.regex": "提示: 使用正则替换: //replace .*_log ", - "fawe.tips.tip.replace.regex.2": "提示: 使用正则替换: //replace .*stairs[facing=(north|south)] ", - "fawe.tips.tip.replace.regex.3": "提示: 使用运算符替换: //replace water[level>2] sand", - "fawe.tips.tip.replace.regex.4": "提示: 使用运算符替换: //replace true *[waterlogged=false]", - "fawe.tips.tip.replace.regex.5": "提示: 使用运算符替换: //replace true *[level-=1]", - "fawe.tips.tip.replace.id": "提示: 仅替换方块ID: //replace woodenstair #id[cobblestair]", - "fawe.tips.tip.replace.light": "提示: 使用 //replace #brightness[1][15] 0 移除光源", - "fawe.tips.tip.tab.complete": "提示: 替换命令支持 Tab 补全", - "fawe.tips.tip.flip": "提示: 使用 //flip 镜像", - "fawe.tips.tip.deform": "提示: 使用 //deform 变形", - "fawe.tips.tip.transform": "提示: 使用 //gtransform 设置变换", - "fawe.tips.tip.copypaste": "提示: 使用 //br copypaste 点击粘贴", - "fawe.tips.tip.source.mask": "提示: 使用 /gsmask 设置源遮罩", - "fawe.tips.tip.replace.marker": "提示: 使用 //replace wool #fullcopy 将方块替换为完整剪贴板", - "fawe.tips.tip.paste": "提示: 使用 //paste 放置", - "fawe.tips.tip.lazycopy": "提示: lazycopy 更快", - "fawe.tips.tip.download": "提示: 试试 //download", - "fawe.tips.tip.rotate": "提示: 使用 //rotate 旋转", - "fawe.tips.tip.copy.pattern": "提示: 要用作图案请尝试 #copy", - "fawe.tips.tip.regen.0": "提示: 使用 /regen [biome] 配合生物群系", - "fawe.tips.tip.regen.1": "提示: 使用 /regen [biome] [seed] 配合种子", - "fawe.tips.tip.biome.pattern": "提示: #biome[forest] 图案可用于任何命令", - "fawe.tips.tip.biome.mask": "提示: 使用 `$jungle` 遮罩限制到特定生物群系", - "fawe.regen.time": "正在重新生成区域,这可能需要一些时间!", - "worldedit.expand.description.vert": "将选区垂直扩展到世界限制。", - "worldedit.expand.expanded": "区域扩展了 {0} 个方块", - "worldedit.expand.expanded.vert": "区域扩展了 {0} 个方块(从顶到底)。", - "worldedit.biomeinfo.lineofsight": "视线所及位置的生物群系: {0}", - "worldedit.biomeinfo.position": "你所在位置的生物群系: {0}", - "worldedit.biomeinfo.selection": "你选区中的生物群系: {0}", - "worldedit.biomeinfo.not-locatable": "命令发送者必须在世界中才能使用 -p 标志。", - "worldedit.error.disabled": "此功能已禁用(参见 WorldEdit 配置)。", - "worldedit.error.no-match": "没有匹配 '{0}' 的结果。", - "worldedit.error.unknown": "发生未知错误: {0}", - "worldedit.error.parser.player-only": "输入 '{0}' 需要一个玩家!", - "worldedit.error.parser.bad-state-format": "{0} 中的状态格式错误", - "worldedit.error.parser.unknown-property": "方块 '{1}' 的未知属性 '{0}'", - "worldedit.error.parser.duplicate-property": "重复属性: {0}", - "worldedit.error.parser.unknown-value": "属性 '{1}' 的未知值 '{0}'", - "worldedit.error.parser.invalid-colon": "无效的冒号。", - "worldedit.error.parser.hanging-lbracket": "无效格式。'{0}' 处有悬挂的括号。", - "worldedit.error.parser.missing-rbracket": "状态缺少尾部 ']'", - "worldedit.error.incomplete-region": "请先创建一个区域选区。", - "worldedit.error.not-a-block": "此物品不是方块。", - "worldedit.error.unknown-entity": "实体名称 '{0}' 未被识别。", - "worldedit.error.unknown-mob": "生物名称 '{0}' 未被识别。", - "worldedit.error.parser.clipboard.missing-offset": "使用 @ 指定了偏移但未给出偏移值。使用 '#copy@[x,y,z]'。", - "worldedit.error.parser.clipboard.missing-coordinates": "剪贴板偏移需要 x,y,z 坐标。", - "worldedit.error.unknown-item": "物品名称 '{0}' 未被识别。", - "worldedit.error.parser.invalid-expression": "无效的表达式: {0}", - "worldedit.error.parser.negate-nothing": "无法对空内容取反!", - "worldedit.error.invalid-page": "无效的页码", - "worldedit.error.missing-extent": "未知的 Extent", - "worldedit.error.missing-session": "未知的 LocalSession", - "worldedit.error.missing-world": "你需要提供一个世界(尝试 //world)", - "worldedit.error.missing-actor": "未知的操作者", - "worldedit.error.missing-player": "未知的玩家", - "worldedit.error.no-file-selected": "未选择文件。", - "worldedit.error.file-resolution.outside-root": "路径在允许的根目录之外", - "worldedit.error.file-resolution.resolve-failed": "解析路径失败", - "worldedit.error.invalid-filename.invalid-characters": "无效字符或缺少扩展名", - "worldedit.error.invalid-number.matches": "需要数字;给定的是字符串 \"{0}\"。", - "worldedit.error.invalid-number": "需要数字;给定的是字符串。", - "worldedit.error.unknown-block": "方块名称 '{0}' 未被识别。", - "worldedit.error.disallowed-block": "方块 '{0}' 不被允许(参见 WorldEdit 配置)。", - "worldedit.error.max-changes": "操作中达到最大方块更改数 ({0})。", - "worldedit.error.max-brush-radius": "最大笔刷半径(在 worldedit-config.yml 中): {0}", - "worldedit.error.max-radius": "最大半径(在 worldedit-config.yml 中): {0}", - "worldedit.error.unknown-direction": "未知方向: {0}", - "worldedit.error.empty-clipboard": "你的剪贴板为空。请先使用 //copy。", - "worldedit.error.invalid-filename": "文件名 '{0}' 无效: {1}", - "worldedit.error.file-resolution": "文件 '{0}' 解析错误: {1}", - "worldedit.tool.error.cannot-bind": "无法将工具绑定到 {0}: {1}", - "worldedit.error.file-aborted": "文件选择已中止。", - "worldedit.error.world-unloaded": "世界已被卸载。", - "worldedit.error.named-world-unloaded": "世界 '{0}' 已被卸载。", - "worldedit.error.blocks-cant-be-used": "无法使用方块", - "worldedit.error.unknown-tag": "标签名称 '{0}' 未被识别。", - "worldedit.error.empty-tag": "标签名称 '{0}' 没有内容。", - "worldedit.error.unknown-biome": "生物群系名称 '{0}' 未被识别。", - "worldedit.brush.radius-too-large": "最大允许笔刷半径: {0}", - "worldedit.brush.apply.description": "应用笔刷,将功能应用到每个方块", - "worldedit.brush.apply.radius": "笔刷大小", - "worldedit.brush.apply.shape": "区域形状", - "worldedit.brush.apply.type": "要使用的笔刷类型", - "worldedit.brush.apply.item.warning": "此笔刷模拟物品使用。其效果可能不适用于所有平台,可能无法撤销,并可能与其他模组/插件产生奇怪的交互。使用风险自负。", - "worldedit.brush.paint.description": "绘画笔刷,将功能应用到表面", - "worldedit.brush.paint.size": "笔刷大小", - "worldedit.brush.paint.shape": "区域形状", - "worldedit.brush.paint.density": "笔刷密度", - "worldedit.brush.paint.type": "要使用的笔刷类型", - "worldedit.brush.paint.item.warning": "此笔刷模拟物品使用。其效果可能不适用于所有平台,可能无法撤销,并可能与其他模组/插件产生奇怪的交互。使用风险自负。", - "worldedit.brush.sphere.equip": "球体笔刷已装备 ({0})。", - "worldedit.brush.cylinder.equip": "圆柱笔刷已装备 ({0} x {1})。", - "worldedit.brush.clipboard.equip": "剪贴板笔刷已装备。", - "worldedit.brush.smooth.equip": "平滑笔刷已装备 ({0} x {1}次,使用 {2})。", - "worldedit.brush.smooth.nofilter": "任何方块", - "worldedit.brush.smooth.filter": "过滤器", - "worldedit.brush.snowsmooth.equip": "雪地平滑笔刷已装备 ({0} x {1}次,使用 {2}),{3} 个雪方块。", - "worldedit.brush.snowsmooth.nofilter": "任何方块", - "worldedit.brush.snowsmooth.filter": "过滤器", - "worldedit.brush.extinguish.equip": "灭火器已装备 ({0})。", - "worldedit.brush.gravity.equip": "重力笔刷已装备 ({0})。", - "worldedit.brush.butcher.equip": "屠杀笔刷已装备 ({0})。", - "worldedit.brush.operation.equip": "笔刷设置为 {0}。", - "worldedit.brush.morph.equip": "变形笔刷已装备: {0}。", - "worldedit.brush.biome.column-supported-types": "此笔刷形状不支持整列刷涂,请尝试圆柱形状。", - "worldedit.brush.none.equip": "笔刷已从当前物品解绑。", - "worldedit.brush.none.equipped": "你的当前物品没有绑定笔刷。尝试 /brush sphere 获取基本笔刷。", - "worldedit.setbiome.changed": "已在 {0} 列中更改生物群系。你可能需要重新加入游戏(或关闭并重新打开世界)才能看到变化。", - "worldedit.setbiome.warning": "你可能需要重新加入游戏(或关闭并重新打开世界)才能看到变化。", - "worldedit.setbiome.not-locatable": "命令发送者必须在世界中才能使用 -p 标志。", - "worldedit.drawsel.disabled": "服务端 CUI 已禁用。", - "worldedit.drawsel.enabled": "服务端 CUI 已启用。仅支持长方体区域,最大尺寸为 {0}x{1}x{2}。", - "worldedit.drawsel.disabled.already": "服务端 CUI 已经是禁用状态。", - "worldedit.drawsel.enabled.already": "服务端 CUI 已经是启用状态。", - "worldedit.limit.too-high": "你允许的最大限制是 {0}。", - "worldedit.limit.set": "方块更改限制设置为 {0}。", - "worldedit.limit.return-to-default": "(使用 //limit 恢复默认值。)", - "worldedit.timeout.too-high": "你允许的最大超时时间是 {0}毫秒。", - "worldedit.timeout.set": "超时时间设置为 {0}毫秒。", - "worldedit.timeout.return-to-default": " (使用 //timeout 恢复默认值。)", - "worldedit.fast.disabled": "快速模式已禁用。", - "worldedit.fast.enabled": "快速模式已启用。更改将不会写入历史记录(//undo 被禁用)。受影响区块的光照可能不正确,且/或你可能需要重新加入才能看到更改。", - "worldedit.fast.disabled.already": "快速模式已经是禁用状态。", - "worldedit.fast.enabled.already": "快速模式已经是启用状态。", - "worldedit.perf.sideeffect.set": "副作用 \"{0}\" 设置为 {1}", - "worldedit.perf.sideeffect.get": "副作用 \"{0}\" 当前为 {1}", - "worldedit.perf.sideeffect.already-set": "副作用 \"{0}\" 已经是 {1}", - "worldedit.perf.sideeffect.set-all": "所有副作用设置为 {0}", - "worldedit.reorder.current": "当前重排序模式为 {0}", - "worldedit.reorder.set": "重排序模式已设置为 {0}", - "worldedit.gmask.disabled": "全局遮罩已禁用。", - "worldedit.gmask.set": "全局遮罩已设置。", - "worldedit.toggleplace.pos1": "现在放置在位置 #1。", - "worldedit.toggleplace.player": "现在放置在你站立的方块处。", - "worldedit.toggleplace.not-locatable": "在此上下文中无法切换放置位置。", - "worldedit.searchitem.too-short": "请输入更长的搜索字符串(长度 > 2)。", - "worldedit.searchitem.either-b-or-i": "不能同时使用 'b' 和 'i' 标志。", - "worldedit.searchitem.searching": "(请稍候...正在搜索物品。)", - "worldedit.watchdog.no-hook": "此平台没有看门狗钩子。", - "worldedit.watchdog.active.already": "看门狗钩子已经处于激活状态。", - "worldedit.watchdog.inactive.already": "看门狗钩子已经处于非激活状态。", - "worldedit.watchdog.active": "看门狗钩子已激活。", - "worldedit.watchdog.inactive": "看门狗钩子已停用。", - "worldedit.world.remove": "已移除世界覆盖。", - "worldedit.world.set": "世界覆盖已设置为 {0}。(使用 //world 恢复默认)", - "worldedit.undo.undone": "已撤销 {0} 个可用编辑。", - "worldedit.undo.none": "没有可以撤销的内容。", - "worldedit.redo.redone": "已重做 {0} 个可用编辑。", - "worldedit.redo.none": "没有可以重做的内容。", - "worldedit.clearhistory.cleared": "历史记录已清除。", - "worldedit.raytrace.noblock": "视线内没有方块!", - "worldedit.raytrace.require-player": "射线追踪命令需要一个玩家!", - "worldedit.restore.not-configured": "快照/备份还原未配置。", - "worldedit.restore.not-available": "该快照不存在或不可用。", - "worldedit.restore.failed": "加载快照失败: {0}", - "worldedit.restore.loaded": "快照 '{0}' 已加载;正在还原...", - "worldedit.restore.restored": "已还原;{0} 个缺失区块和 {1} 个其他错误。", - "worldedit.restore.none-for-specific-world": "未找到世界 '{0}' 的快照。", - "worldedit.restore.none-for-world": "未找到此世界的快照。", - "worldedit.restore.none-found": "未找到快照。", - "worldedit.restore.none-found-console": "未找到快照。详情请查看控制台。", - "worldedit.restore.chunk-not-present": "区块不存在于快照中。", - "worldedit.restore.chunk-load-failed": "无法加载任何区块。(存档损坏?)", - "worldedit.restore.block-place-failed": "错误导致无法还原任何方块。", - "worldedit.restore.block-place-error": "最后一个错误: {0}", - "worldedit.snapshot.use.newest": "现在使用最新的快照。", - "worldedit.snapshot.use": "快照设置为: {0}", - "worldedit.snapshot.none-before": "找不到 {0} 之前的快照。", - "worldedit.snapshot.none-after": "找不到 {0} 之后的快照。", - "worldedit.snapshot.index-above-0": "无效索引,必须大于或等于 1。", - "worldedit.snapshot.index-oob": "无效索引,必须在 1 到 {0} 之间。", - "worldedit.schematic.unknown-format": "未知的原理图格式: {0}。", - "worldedit.schematic.load.does-not-exist": "原理图 {0} 不存在!", - "worldedit.schematic.load.loading": "(请稍候...正在加载原理图。)", - "worldedit.schematic.load.unsupported-version": "不支持此原理图。版本: {0}。如果你使用的是 litematica 原理图,则不受支持!", - "worldedit.schematic.save.already-exists": "该原理图已存在。使用 -f 标志覆盖。", - "worldedit.schematic.save.failed-directory": "无法创建原理图文件夹!", - "worldedit.schematic.save.saving": "(请稍候...正在保存原理图。)", - "worldedit.schematic.save.still-saving": "(请稍候...仍在保存原理图。)", - "worldedit.schematic.share.unsupported-format": "原理图分享目标 \"{0}\" 不支持 \"{1}\" 格式。", - "worldedit.schematic.share.response.arkitektonika.download": "下载: {0}", - "worldedit.schematic.share.response.arkitektonika.delete": "删除: {0}", - "worldedit.schematic.share.response.arkitektonika.click-here": "[点击这里]", - "worldedit.schematic.delete.empty": "未找到原理图 {0}!", - "worldedit.schematic.delete.does-not-exist": "原理图 {0} 不存在!", - "worldedit.schematic.delete.failed": "删除 {0} 失败!是否为只读?", - "worldedit.schematic.delete.deleted": "{0} 已被删除。", - "worldedit.schematic.formats.title": "可用的剪贴板格式(名称: 查找名称)", - "worldedit.schematic.load.symbol": "[加载]", - "worldedit.schematic.plus.symbol": "[+]", - "worldedit.schematic.minus.symbol": "[-]", - "worldedit.schematic.x.symbol": "[X]", - "worldedit.schematic.0.symbol": "[O]", - "worldedit.schematic.dash.symbol": " - ", - "worldedit.schematic.click-to-load": "点击加载", - "worldedit.schematic.load": "加载", - "worldedit.schematic.list": "列表", - "worldedit.schematic.available": "可用原理图", - "worldedit.schematic.unload": "卸载", - "worldedit.schematic.delete": "删除", - "worldedit.schematic.visualize": "可视化", - "worldedit.schematic.clipboard": "添加到(多重)剪贴板", - "worldedit.schematic.unknown-filename": "未知文件名: {0}", - "worldedit.schematic.file-not-exist": "文件无法读取或不存在: {0}", - "worldedit.schematic.already-exists": "该原理图已存在!", - "worldedit.schematic.failed-to-save": "保存原理图失败", - "worldedit.schematic.directory-does-not-exist": "目录 '{0}' 不存在!", - "worldedit.schematic.file-perm-fail": "创建 '{0}' 失败!请检查文件权限。", - "worldedit.schematic.sorting-old-new": "不能同时按最旧和最新排序。", - "worldedit.schematic.unsupported-minecraft-version": "此版本的 WorldEdit 不支持你的 Minecraft 版本。在解决此问题之前,原理图将无法使用。", - "worldedit.pos.already-set": "位置已设置。", - "worldedit.pos.console-require-coords": "在控制台中必须提供坐标。", - "worldedit.hpos.no-block": "视线内没有方块!", - "worldedit.hpos.already-set": "位置已设置。", - "worldedit.chunk.selected-multiple": "已选区块: ({0}, {1}, {2}) - ({3}, {4}, {5})", - "worldedit.chunk.selected": "已选区块: {0}, {1}, {2}", - "worldedit.wand.invalid": "魔杖物品配置错误或已禁用。", - "worldedit.wand.selwand.info": "左键: 选择位置 #1;右键: 选择位置 #2", - "worldedit.wand.selwand.now.tool": "选择魔杖现在是普通工具。你可以使用 {0} 禁用它,使用 {1} 重新绑定到任何物品,或使用 {2} 获取新魔杖。", - "worldedit.wand.navwand.info": "左键: 跳转到位置;右键: 穿过墙壁", - "worldedit.contract.contracted": "区域收缩了 {0} 个方块。", - "worldedit.shift.shifted": "区域已移动。", - "worldedit.outset.outset": "区域已外扩。", - "worldedit.inset.inset": "区域已内缩。", - "worldedit.trim.trim": "区域已裁剪。", - "worldedit.trim.no-blocks": "没有方块匹配裁剪遮罩。", - "worldedit.size.offset": "{0}: {1} @ {2} ({3} 个方块)", - "worldedit.size.type": "类型: {0}", - "worldedit.size.size": "大小: {0}", - "worldedit.size.distance": "长方体距离: {0}", - "worldedit.size.blocks": "方块数量: {0}", - "worldedit.count.counted": "统计: {0}", - "worldedit.distr.no-blocks": "没有统计到方块。", - "worldedit.distr.no-previous": "没有之前的分布。", - "worldedit.distr.total": "方块总数: {0}", - "worldedit.select.cleared": "选区已清除。", - "worldedit.select.cuboid.message": "长方体: 左键选择点 1,右键选择点 2", - "worldedit.select.cuboid.description": "选择长方体的两个角", - "worldedit.select.extend.message": "长方体: 左键设置起始点,右键扩展", - "worldedit.select.extend.description": "快速长方体选择模式", - "worldedit.select.poly.message": "二维多边形选择器: 左键/右键添加一个点。", - "worldedit.select.poly.limit-message": "最多 {0} 个点。", - "worldedit.select.poly.description": "选择带高度的二维多边形", - "worldedit.select.ellipsoid.message": "椭球选择器: 左键=中心,右键扩展", - "worldedit.select.ellipsoid.description": "选择一个椭球", - "worldedit.select.sphere.message": "球体选择器: 左键=中心,右键设置半径", - "worldedit.select.sphere.description": "选择一个球体", - "worldedit.select.cyl.message": "圆柱选择器: 左键=中心,右键扩展", - "worldedit.select.cyl.description": "选择一个圆柱", - "worldedit.select.convex.message": "凸多面体选择器: 左键=第一个顶点,右键添加更多。", - "worldedit.select.convex.limit-message": "最多 {0} 个点。", - "worldedit.select.convex.description": "选择一个凸多面体", - "worldedit.select.default-set": "你的默认区域选择器现在是 {0}。", - "worldedit.chunkinfo.chunk": "区块: {0}, {1}", - "worldedit.chunkinfo.old-filename": "旧格式: {0}", - "worldedit.chunkinfo.mcregion-filename": "McRegion: region/{0}", - "worldedit.listchunks.listfor": "列出以下区块: {0}", - "worldedit.drain.drained": "已排干 {0} 个方块。", - "worldedit.fill.created": "已填充 {0} 个方块。", - "worldedit.fillr.created": "已递归填充 {0} 个方块。", - "worldedit.fixlava.fixed": "已修复 {0} 个方块。", - "worldedit.fixwater.fixed": "已修复 {0} 个方块。", - "worldedit.removeabove.removed": "已移除 {0} 个方块。", - "worldedit.removebelow.removed": "已移除 {0} 个方块。", - "worldedit.removenear.removed": "已移除 {0} 个方块。", - "worldedit.replacenear.replaced": "已替换 {0} 个方块。", - "worldedit.snow.created": "已覆盖 {0} 个表面。", - "worldedit.thaw.removed": "已融化 {0} 个方块。", - "worldedit.green.changed": "已绿化 {0} 个方块。", - "worldedit.extinguish.removed": "已熄灭 {0} 处火焰。", - "worldedit.butcher.killed": "在半径 {1} 内已杀死 {0} 个生物。", - "worldedit.butcher.explain-all": "使用 -1 移除已加载区块中的所有生物", - "worldedit.remove.removed": "已标记移除 {0} 个实体。", - "worldedit.remove.explain-all": "使用 -1 移除已加载区块中的所有实体", - "worldedit.calc.invalid": "'{0}' 无法被解析为有效的表达式", - "worldedit.calc.invalid.with-error": "'{0}' 无法被解析为有效的表达式: '{1}'", - "worldedit.paste.pasted": "剪贴板已粘贴到 {0}", - "worldedit.paste.selected": "已选择剪贴板粘贴区域。", - "worldedit.rotate.no-interpolation": "注意: 尚不支持插值,因此建议使用 90 的倍数角度。", - "worldedit.rotate.rotated": "剪贴板副本已旋转。", - "worldedit.flip.flipped": "剪贴板副本已翻转。", - "worldedit.clearclipboard.cleared": "剪贴板已清除。", - "worldedit.set.done": "操作完成 ({0})。", - "worldedit.set.done.verbose": "操作完成 ({0})。", - "worldedit.line.changed": "已更改 {0} 个方块。", - "worldedit.line.invalid-type": "//line 仅适用于长方体选区或凸多面体选区", - "worldedit.line.cuboid-only": "//line 仅适用于长方体选区", - "worldedit.curve.changed": "已更改 {0} 个方块。", - "worldedit.curve.invalid-type": "//curve 仅适用于凸多面体选区", - "worldedit.curve.convex-only": "//curve 仅适用于凸多面体选区", - "worldedit.replace.replaced": "已替换 {0} 个方块。", - "worldedit.stack.changed": "{0} 个方块已更改。使用 //undo 撤销", - "worldedit.stack.intersecting-region": "使用方块单位时,堆叠偏移不能与区域碰撞", - "worldedit.regen.regenerated": "区域已重新生成。", - "worldedit.regen.failed": "无法重新生成区块。详情请查看控制台。", - "worldedit.walls.changed": "已更改 {0} 个方块。", - "worldedit.faces.changed": "已更改 {0} 个方块。", - "worldedit.overlay.overlaid": "已覆盖 {0} 个方块。", - "worldedit.naturalize.naturalized": "已将 {0} 个方块自然化。", - "worldedit.center.changed": "中心已设置。({0} 个方块已更改)", - "worldedit.smooth.changed": "地形高度图已平滑。{0} 个方块已更改。", - "worldedit.snowsmooth.changed": "雪地高度图已平滑。{0} 个方块已更改。", - "worldedit.move.moved": "已移动 {0} 个方块。", - "worldedit.deform.deformed": "已变形 {0} 个方块。", - "worldedit.hollow.changed": "已更改 {0} 个方块。", - "worldedit.forest.created": "已创建 {0} 棵树。", - "worldedit.flora.created": "已创建 {0} 个植物。", - "worldedit.unstuck.moved": "你自由了!", - "worldedit.ascend.obstructed": "上方没有找到空闲位置。", - "worldedit.ascend.moved": "上升了 {0} 层。", - "worldedit.descend.obstructed": "下方没有找到空闲位置。", - "worldedit.descend.moved": "下降了 {0} 层。", - "worldedit.ceil.obstructed": "上方没有找到空闲位置。", - "worldedit.ceil.moved": "嗖!", - "worldedit.thru.obstructed": "前方没有找到空闲位置。", - "worldedit.thru.moved": "嗖!", - "worldedit.jumpto.moved": "噗!", - "worldedit.jumpto.none": "视线内没有方块(或太远)!", - "worldedit.up.obstructed": "你会撞到上方的东西。", - "worldedit.up.moved": "嗖!", - "worldedit.cone.invalid-radius": "你必须指定 1 或 2 个半径值。", - "worldedit.cone.created": "已创建 {0} 个方块。", - "worldedit.cyl.invalid-radius": "你必须指定 1 或 2 个半径值。", - "worldedit.cyl.created": "已创建 {0} 个方块。", - "worldedit.hcyl.thickness-too-large": "厚度不能大于 x 或 z 半径。", - "worldedit.sphere.invalid-radius": "你必须指定 1 或 3 个半径值。", - "worldedit.sphere.created": "已创建 {0} 个方块。", - "worldedit.blob.created": "已创建 {0} 个方块。", - "worldedit.feature.created": "地物已创建,放置了 {0} 个方块。", - "worldedit.generate.feature.failed": "此地物无法放置在这里。请确保该区域满足要求。", - "worldedit.forestgen.created": "已创建 {0} 棵树。", - "worldedit.pumpkins.created": "已创建 {0} 个南瓜地。", - "worldedit.feature.failed": "生成地物失败。此位置是否适合?", - "worldedit.pyramid.created": "已创建 {0} 个方块。", - "worldedit.generate.created": "已创建 {0} 个方块。", - "worldedit.generatebiome.changed": "影响了 {0} 个生物群系。", - "worldedit.structure.created": "结构已创建,放置了 {0} 个方块。", - "worldedit.generate.structure.failed": "生成结构失败。此位置是否适合?", - "worldedit.reload.config": "配置已重新加载!", - "worldedit.report.written": "FAWE 报告已写入 {0}", - "worldedit.report.error": "写入报告失败: {0}", - "worldedit.report.callback": "FAWE 报告: {0}.report", - "worldedit.timezone.invalid": "无效的时区", - "worldedit.timezone.set": "此会话的时区设置为: {0}", - "worldedit.timezone.current": "该时区的当前时间是: {0}", - "worldedit.version.version": "FAWE 版本:\n - 日期 {0}\n - 提交 {1}\n - 构建 {2}\n - 平台 {3}", - "worldedit.trace.no-tracing-extents": "追踪: 没有使用任何 Extent。", - "worldedit.trace.action-failed": "追踪: 操作 {0} 在 {1} 处被 Extent {2} 丢弃", - "worldedit.trace.active.already": "追踪模式已经处于激活状态。", - "worldedit.trace.inactive.already": "追踪模式已经处于非激活状态。", - "worldedit.trace.active": "追踪模式已激活。", - "worldedit.trace.inactive": "追踪模式已停用。", - "worldedit.command.time-elapsed": "耗时 {0}秒(历史: 更改了 {1};{2} 方块/秒)。", - "worldedit.command.permissions": "你没有权限执行此操作。你是否在正确的模式?", - "worldedit.command.player-only": "此命令必须由玩家使用。", - "worldedit.command.error.report": "&c请报告此错误: [查看控制台]", - "worldedit.command.deprecation": "此命令已弃用。", - "worldedit.command.deprecation-message": "请使用 '{0}' 代替。", - "worldedit.pastebin.uploading": "(请稍候...正在将输出发送到粘贴服务...)", - "worldedit.session.cant-find-session": "无法找到 {0} 的会话", - "worldedit.platform.no-file-dialog": "你的环境不支持文件对话框。", - "worldedit.tool.max-block-changes": "已达到最大方块更改限制。", - "worldedit.tool.no-block": "视线内没有方块!", - "worldedit.tool.repl.equip": "方块替换工具已绑定到 {0}。", - "worldedit.tool.repl.switched": "替换工具已切换为: {0}", - "worldedit.tool.data-cycler.equip": "方块数据循环工具已绑定到 {0}。", - "worldedit.tool.data-cycler.block-not-permitted": "你没有权限循环该方块的数据值。", - "worldedit.tool.data-cycler.cant-cycle": "该方块的数据无法循环!", - "worldedit.tool.data-cycler.new-value": "{0} 的值现在是 {1}。", - "worldedit.tool.data-cycler.cycling": "现在循环 {0}。", - "worldedit.tool.deltree.equip": "浮空树移除工具已绑定到 {0}。", - "worldedit.tool.deltree.not-tree": "那不是一棵树。", - "worldedit.tool.deltree.not-floating": "那不是一棵浮空树。", - "worldedit.tool.tree.equip": "树木工具已绑定到 {0}。", - "worldedit.tool.tree.obstructed": "树无法放在那里。", - "worldedit.tool.structure.equip": "结构放置工具已绑定到 {0}。", - "worldedit.tool.feature.equip": "地物放置工具已绑定到 {0}。", - "worldedit.tool.info.equip": "信息工具已绑定到 {0}。", - "worldedit.tool.inspect.equip": "检查工具已绑定到 {0}。", - "worldedit.tool.info.blockstate.hover": "方块状态(点击复制)", - "worldedit.tool.info.internalid.hover": "内部 ID", - "worldedit.tool.info.legacy.hover": "旧版 id:data", - "worldedit.tool.info.light.hover": "方块光照/上方光照", - "worldedit.tool.none.equip": "工具已从当前物品解绑。", - "worldedit.tool.selwand.equip": "选择魔杖已绑定到 {0}。", - "worldedit.tool.navwand.equip": "导航魔杖已绑定到 {0}。", - "worldedit.tool.floodfill.equip": "方块填充工具已绑定到 {0}。", - "worldedit.tool.farwand.equip": "远距离魔杖工具已绑定到 {0}。", - "worldedit.tool.lrbuild.equip": "远程建造工具已绑定到 {0}。", - "worldedit.tool.lrbuild.set": "左键设置为 {0};右键设置为 {1}。", - "worldedit.tool.stack.equip": "堆叠工具已绑定到 {0}。", - "worldedit.tool.unbind-instruction": "持有该物品时运行 {0} 以解绑。", - "worldedit.tool.superpickaxe.mode.single": "模式现在是单点。使用镐子左键点击。// 禁用。", - "worldedit.tool.superpickaxe.mode.area": "模式现在是区域。使用镐子左键点击。// 禁用。", - "worldedit.tool.superpickaxe.mode.recursive": "模式现在是递归。使用镐子左键点击。// 禁用。", - "worldedit.tool.superpickaxe.max-range": "最大范围是 {0}。", - "worldedit.tool.superpickaxe.enabled.already": "超级镐已经处于启用状态。", - "worldedit.tool.superpickaxe.disabled.already": "超级镐已经处于禁用状态。", - "worldedit.tool.superpickaxe.enabled": "超级镐已启用。", - "worldedit.tool.superpickaxe.disabled": "超级镐已禁用。", - "worldedit.tool.mask.set": "笔刷遮罩已设置。", - "worldedit.tool.mask.disabled": "笔刷遮罩已禁用。", - "worldedit.tool.material.set": "笔刷材质已设置。", - "worldedit.tool.range.set": "笔刷范围已设置。", - "worldedit.tool.size.set": "笔刷大小已设置。", - "worldedit.tool.tracemask.set": "追踪遮罩已设置。", - "worldedit.tool.tracemask.disabled": "追踪遮罩已禁用。", - "worldedit.execute.script-permissions": "你没有权限使用该脚本。", - "worldedit.executelast.no-script": "请先使用 /cs 指定脚本名称。", - "worldedit.script.read-error": "脚本读取错误: {0}", - "worldedit.script.unsupported": "当前仅支持 .js 脚本", - "worldedit.script.file-not-found": "脚本不存在: {0}", - "worldedit.script.no-script-engine": "未找到已安装的脚本引擎。\n请参见 https://worldedit.enginehub.org/en/latest/usage/other/craftscripts/", - "worldedit.script.failed": "执行失败: {0}", - "worldedit.script.failed-console": "执行失败(查看控制台): {0}", - "worldedit.operation.affected.biome": "影响了 {0} 个生物群系", - "worldedit.operation.affected.block": "影响了 {0} 个方块", - "worldedit.operation.affected.column": "影响了 {0} 列", - "worldedit.operation.affected.entity": "影响了 {0} 个实体", - "worldedit.operation.deform.expression": "使用 {0} 进行了变形", - "worldedit.error.parser.invalid-nbt": "输入中的 NBT 数据无效: '{0}'。错误: {1}", - "worldedit.selection.convex.info.vertices": "顶点: {0}", - "worldedit.selection.convex.info.triangles": "三角面: {0}", - "worldedit.selection.convex.explain.primary": "已用顶点 {0} 开始新选区。", - "worldedit.selection.convex.explain.secondary": "已将顶点 {0} 添加到选区。", - "worldedit.selection.cuboid.info.pos1": "位置 1: {0}", - "worldedit.selection.cuboid.info.pos2": "位置 2: {0}", - "worldedit.selection.cuboid.explain.primary": "第一个位置设置为 {0}。", - "worldedit.selection.cuboid.explain.primary-area": "第一个位置设置为 {0} ({1})。", - "worldedit.selection.cuboid.explain.secondary": "第二个位置设置为 {0}。", - "worldedit.selection.cuboid.explain.secondary-area": "第二个位置设置为 {0} ({1})。", - "worldedit.selection.extend.explain.primary": "选区起始于 {0} ({1})。", - "worldedit.selection.extend.explain.secondary": "选区扩展至包含 {0} ({1})。", - "worldedit.selection.ellipsoid.info.center": "中心: {0}", - "worldedit.selection.ellipsoid.info.radius": "X/Y/Z 半径: {0}", - "worldedit.selection.ellipsoid.explain.primary": "中心位置设置为 {0}。", - "worldedit.selection.ellipsoid.explain.primary-area": "中心位置设置为 {0} ({1})。", - "worldedit.selection.ellipsoid.explain.secondary": "半径设置为 {0}。", - "worldedit.selection.ellipsoid.explain.secondary-area": "半径设置为 {0} ({1})。", - "worldedit.selection.cylinder.info.center": "中心: {0}", - "worldedit.selection.cylinder.info.radius": "半径: {0}", - "worldedit.selection.cylinder.explain.primary": "在 {0} 开始新的圆柱选区。", - "worldedit.selection.cylinder.explain.secondary": "半径设置为 {0}/{1} 个方块。({2})", - "worldedit.selection.cylinder.explain.secondary-missing": "在设置半径之前,必须先选择中心点。", - "worldedit.selection.polygon2d.info": "点数: {0}", - "worldedit.selection.polygon2d.explain.primary": "在 {0} 开始新的多边形。", - "worldedit.selection.polygon2d.explain.secondary": "在 {1} 添加了第 {0} 个点。", - "worldedit.selection.sphere.explain.secondary": "半径设置为 {0}。", - "worldedit.selection.sphere.explain.secondary-defined": "半径设置为 {0} ({1})。", - "worldedit.sideeffect.history": "历史记录", - "worldedit.sideeffect.history.description": "记录更改的历史", - "worldedit.sideeffect.heightmaps": "高度图", - "worldedit.sideeffect.heightmaps.description": "更新高度图", - "worldedit.sideeffect.lighting": "光照", - "worldedit.sideeffect.lighting.description": "更新方块光照", - "worldedit.sideeffect.neighbors": "相邻方块", - "worldedit.sideeffect.neighbors.description": "更新编辑中方块的形状", - "worldedit.sideeffect.update": "更新", - "worldedit.sideeffect.update.description": "通知被更改的方块", - "worldedit.sideeffect.validation": "验证", - "worldedit.sideeffect.validation.description": "验证并修复不一致的世界状态,例如断开连接的方块", - "worldedit.sideeffect.entity_ai": "实体 AI", - "worldedit.sideeffect.entity_ai.description": "更新方块变更的实体 AI 路径", - "worldedit.sideeffect.events": "模组/插件事件", - "worldedit.sideeffect.events.description": "在适用时通知其他模组/插件这些更改", - "worldedit.sideeffect.state.on": "开启", - "worldedit.sideeffect.state.delayed": "延迟", - "worldedit.sideeffect.state.off": "关闭", - "worldedit.sideeffect.box.current": "当前", - "worldedit.sideeffect.box.change-to": "点击设置为 {0}", - "worldedit.help.command-not-found": "未找到命令 '{0}'。", - "worldedit.help.no-subcommands": "'{0}' 没有子命令。(也许 '{1}' 是一个参数?)", - "worldedit.help.subcommand-not-found": "在 '{1}' 下未找到子命令 '{0}'。", - "worldedit.cli.stopping": "正在停止!", - "worldedit.cli.unknown-command": "未知命令!", - "worldedit.version.bukkit.unsupported-adapter": "此 FastAsyncWorldEdit 版本不完全支持你的 Bukkit 版本。方块实体(如箱子)将为空,方块属性(如旋转)将缺失,其他功能也可能无法使用。请更新 FastAsyncWorldEdit 以恢复此功能:\n{0}", - "worldedit.bukkit.no-edit-without-adapter": "在不支持的版本上编辑已被禁用。" -}