diff --git a/settings.gradle.kts b/settings.gradle.kts index 77b27b66d9..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").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/build.gradle.kts b/worldedit-libs/nukkit/build.gradle.kts new file mode 100644 index 0000000000..3f6c7e06cb --- /dev/null +++ b/worldedit-libs/nukkit/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("buildlogic.libs") +} diff --git a/worldedit-nukkit/build.gradle.kts b/worldedit-nukkit/build.gradle.kts new file mode 100644 index 0000000000..518f7ce8d1 --- /dev/null +++ b/worldedit-nukkit/build.gradle.kts @@ -0,0 +1,149 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +plugins { + `java-library` + id("buildlogic.platform") +} + +project.description = "Nukkit" + +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 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 +} + +val mcmeta: Configuration by configurations.register("mcmeta") { + isCanBeConsumed = false +} + +dependencies { + api(project(":worldedit-core")) + api(project(":worldedit-libs:nukkit")) + + compileOnly("cn.nukkit:nukkit:1.0-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") +} + +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) + 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-${project.version}.${archiveExtension.getOrElse("jar")}") + configurations = listOf( + project.configurations.getByName("runtimeClasspath"), + adapters + ) + dependencies { + include(dependency(":worldedit-core")) + include(dependency(":worldedit-libs:nukkit")) + 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)) + } + 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)) + } +} + +tasks.named("assemble").configure { + dependsOn("shadowJar") +} + +configure { + publications.named("maven") { + from(components["java"]) + } +} 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..491ea2c0e2 --- /dev/null +++ b/worldedit-nukkit/nk-adapters/adapter-nkx/src/main/java/com/fastasyncworldedit/nukkit/adapter/nkx/NkxNukkitAdapter.java @@ -0,0 +1,115 @@ +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.Locale; +import java.util.UUID; + +/** + * Adapter implementation for the NKX (upstream Nukkit) platform. + */ +public class NkxNukkitAdapter implements NukkitImplAdapter { + + private static final BlockLayer[] LAYERS = {BlockLayer.NORMAL, BlockLayer.WATERLOGGED}; + + @Override + public String getPlatformName() { + return "NKX"; + } + + @Override + public int getBlockDataBits() { + return Block.DATA_BITS; + } + + @Override + @Nullable + public String getPlayerLanguageCode(Player player) { + 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; + } + + @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/FaweNukkit.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/FaweNukkit.java new file mode 100644 index 0000000000..99a07111a6 --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/FaweNukkit.java @@ -0,0 +1,109 @@ +package com.fastasyncworldedit.nukkit; + +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.AsyncPreloader; +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; + private Preloader preloader; + + 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) { + if (preloader == null && initialise) { + preloader = new AsyncPreloader(); + } + return preloader; + } + + @Override + public FAWEPlatformAdapterImpl getPlatformAdapter() { + return platformAdapter; + } + + public Plugin getPlugin() { + return plugin; + } + +} diff --git a/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitNbtConverter.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitNbtConverter.java new file mode 100644 index 0000000000..0675e6cebc --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitNbtConverter.java @@ -0,0 +1,159 @@ +package com.fastasyncworldedit.nukkit; + +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/src/main/java/com/fastasyncworldedit/nukkit/NukkitPlatformAdapter.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitPlatformAdapter.java new file mode 100644 index 0000000000..4b85155b61 --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitPlatformAdapter.java @@ -0,0 +1,16 @@ +package com.fastasyncworldedit.nukkit; + +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/src/main/java/com/fastasyncworldedit/nukkit/NukkitPlayerBlockBag.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitPlayerBlockBag.java new file mode 100644 index 0000000000..53d0538909 --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitPlayerBlockBag.java @@ -0,0 +1,130 @@ +package com.fastasyncworldedit.nukkit; + +import cn.nukkit.Player; +import cn.nukkit.item.Item; +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; +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()); + 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()) { + 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/src/main/java/com/fastasyncworldedit/nukkit/NukkitQueueHandler.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitQueueHandler.java new file mode 100644 index 0000000000..b405e2492f --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitQueueHandler.java @@ -0,0 +1,21 @@ +package com.fastasyncworldedit.nukkit; + +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/src/main/java/com/fastasyncworldedit/nukkit/NukkitRelighter.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitRelighter.java new file mode 100644 index 0000000000..9c64d2144e --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitRelighter.java @@ -0,0 +1,63 @@ +package com.fastasyncworldedit.nukkit; + +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/src/main/java/com/fastasyncworldedit/nukkit/NukkitTaskManager.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitTaskManager.java new file mode 100644 index 0000000000..cd66451501 --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/NukkitTaskManager.java @@ -0,0 +1,60 @@ +package com.fastasyncworldedit.nukkit; + +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/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..36bbd19a8c --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/adapter/NukkitImplAdapter.java @@ -0,0 +1,103 @@ +package com.fastasyncworldedit.nukkit.adapter; + +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. + *

+ * 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); + + /** + * 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/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/src/main/java/com/fastasyncworldedit/nukkit/mapping/BiomeMapping.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/BiomeMapping.java new file mode 100644 index 0000000000..36f4f66823 --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/BiomeMapping.java @@ -0,0 +1,89 @@ +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.nukkit.WorldEditNukkitPlugin; + +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 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); + }); + + WorldEditNukkitPlugin.getInstance().getLogger().info("Loaded " + JE_TO_BE.size() + " biome mappings"); + } 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/src/main/java/com/fastasyncworldedit/nukkit/mapping/BlockMapping.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/BlockMapping.java new file mode 100644 index 0000000000..21515dac64 --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/BlockMapping.java @@ -0,0 +1,381 @@ +package com.fastasyncworldedit.nukkit.mapping; + +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; +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.nukkit.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 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 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++; + } + } + + WorldEditNukkitPlugin.getInstance().getLogger().info( + "Ordinal mappings built: " + mapped + " mapped, " + unmapped + " unmapped out of " + states.length + " total states"); + } + + /** + * 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) { + WorldEditNukkitPlugin.getInstance().getLogger().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) + ); + } + 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; + } + return true; + } + + private static boolean initBlockStateMapping() { + // Extract block state version from palette + List palette = NukkitImplLoader.get().loadBlockPalette(); + if (!palette.isEmpty()) { + blockStateVersion = palette.getFirst().getInt("version"); + WorldEditNukkitPlugin.getInstance().getLogger().info("Block state version: " + blockStateVersion); + } + + try (InputStream stream = BlockMapping.class.getClassLoader().getResourceAsStream("mapping/blocks.json")) { + if (stream == null) { + WorldEditNukkitPlugin.getInstance().getLogger().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++; + } + } + 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; + } + 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 = NukkitImplLoader.get().getBlockStateSnapshot(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/src/main/java/com/fastasyncworldedit/nukkit/mapping/ItemMapping.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/ItemMapping.java new file mode 100644 index 0000000000..112610d26c --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/ItemMapping.java @@ -0,0 +1,141 @@ +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.nukkit.WorldEditNukkitPlugin; + +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 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()); + WorldEditNukkitPlugin.getInstance().getLogger().info("Loaded " + JE_ITEM_IDS.size() + " JE item IDs"); + } 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(beKey(nukkitItem.getId(), entry.bedrockData()), javaId); + } + }); + + WorldEditNukkitPlugin.getInstance().getLogger().info("Loaded " + JE_TO_BE.size() + " item mappings"); + } 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 + metadata to JE item ID. + */ + 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; + } + // 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) { + + } + + private record ItemEntry( + @SerializedName("bedrock_identifier") + String bedrockId, + @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 new file mode 100644 index 0000000000..ac1563d4be --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/JeBlockState.java @@ -0,0 +1,125 @@ +package com.fastasyncworldedit.nukkit.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); + } + + 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; + } + + 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(); + } + +} diff --git a/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/NukkitBlockData.java b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/NukkitBlockData.java new file mode 100644 index 0000000000..6b1acde3bf --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/fastasyncworldedit/nukkit/mapping/NukkitBlockData.java @@ -0,0 +1,19 @@ +package com.fastasyncworldedit.nukkit.mapping; + +import com.fastasyncworldedit.nukkit.adapter.NukkitImplLoader; + +/** + * 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 << NukkitImplLoader.get().getBlockDataBits()) | metadata; + } + +} diff --git a/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitAdapter.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitAdapter.java new file mode 100644 index 0000000000..9a3c703cf0 --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitAdapter.java @@ -0,0 +1,148 @@ +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.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; +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(), item.getDamage()); + 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/src/main/java/com/sk89q/worldedit/nukkit/NukkitBlockRegistry.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitBlockRegistry.java new file mode 100644 index 0000000000..583cbbab98 --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitBlockRegistry.java @@ -0,0 +1,28 @@ +package com.sk89q.worldedit.nukkit; + +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 javax.annotation.Nullable; +import java.util.Collection; + +/** + * 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/src/main/java/com/sk89q/worldedit/nukkit/NukkitCommandSender.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitCommandSender.java new file mode 100644 index 0000000000..4dc99fcbd7 --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitCommandSender.java @@ -0,0 +1,120 @@ +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.auth.AuthorizationException; +import com.sk89q.worldedit.util.formatting.text.Component; +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/src/main/java/com/sk89q/worldedit/nukkit/NukkitConfiguration.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitConfiguration.java new file mode 100644 index 0000000000..0b1d339496 --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitConfiguration.java @@ -0,0 +1,17 @@ +package com.sk89q.worldedit.nukkit; + +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/src/main/java/com/sk89q/worldedit/nukkit/NukkitEntity.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitEntity.java new file mode 100644 index 0000000000..48bbb16d25 --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitEntity.java @@ -0,0 +1,128 @@ +package com.sk89q.worldedit.nukkit; + +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; +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 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; + } + + String identifier = NukkitImplLoader.get().getEntityIdentifier(entity); + if (identifier == null) { + return null; + } + + EntityType type = EntityTypes.get(identifier); + if (type == null) { + return null; + } + + entity.saveNBT(); + 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/src/main/java/com/sk89q/worldedit/nukkit/NukkitGetBlocks.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitGetBlocks.java new file mode 100644 index 0000000000..c166afae29 --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitGetBlocks.java @@ -0,0 +1,554 @@ +package com.sk89q.worldedit.nukkit; + +import cn.nukkit.blockentity.BlockEntity; +import cn.nukkit.level.Level; +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.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; +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; +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 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(level.getMinBlockY() >> 4, level.getMaxBlockY() >> 4); + this.level = level; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.minY = level.getMinBlockY(); + this.maxY = level.getMaxBlockY(); + } + + @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 = NukkitImplLoader.get().getBlockId(chunk, 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 = 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(); + } + } + 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, minY, maxY); + 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); + 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 = 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(); + } + } + sectionData[index] = 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)); + } + } + // 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 + 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)) { + Property waterloggedProp = state.getBlockType().getProperty(PropertyKey.WATERLOGGED); + if (waterloggedProp != null && state.getState(waterloggedProp) == Boolean.TRUE) { + waterlogged = true; + state = state.with(waterloggedProp, false); + ordinal = state.getOrdinalChar(); + } + } + NukkitImplAdapter adapter = NukkitImplLoader.get(); + int dataBits = adapter.getBlockDataBits(); + int dataMask = adapter.getBlockDataMask(); + int fullId = BlockMapping.jeOrdinalToFullId(ordinal); + int blockId = fullId >> dataBits; + int meta = fullId & dataMask; + 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 + ); + } 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); + } + } + } + } + } + + // 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); + } + } + } + + // Apply entity removals + Set entityRemoves = set.getEntityRemoves(); + 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; + } + UUID entityUUID = uuidAdapter.getEntityUUID(entity); + if (entityRemoves.contains(entityUUID)) { + if (copy != null) { + copy.storeEntity(entity, entityUUID); + } + entity.close(); + entitiesRemoved.add(entityUUID); + } + } + 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); + } + + 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() { + Map chunkEntities = level.getChunkEntities(chunkX, chunkZ); + 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; + } + + @Override + public Set getFullEntities() { + 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); + NukkitImplAdapter adapter = NukkitImplLoader.get(); + for (cn.nukkit.entity.Entity entity : chunkEntities.values()) { + if (entity instanceof cn.nukkit.Player) { + continue; + } + if (uuid.equals(adapter.getEntityUUID(entity))) { + entity.saveNBT(); + if (!entity.namedTag.contains("uuid")) { + entity.namedTag.putString("uuid", uuid.toString()); + } + return NukkitNbtConverter.toFawe(entity.namedTag); + } + } + 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 maxY; + } + + @Override + public int getMinY() { + return minY; + } + +} 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 new file mode 100644 index 0000000000..5d81c7cce7 --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitGetBlocks_Copy.java @@ -0,0 +1,294 @@ +package com.sk89q.worldedit.nukkit; + +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.nukkit.NukkitNbtConverter; +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 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; +import java.util.concurrent.Future; + +/** + * Immutable snapshot copy of a chunk for undo/history. + */ +public class NukkitGetBlocks_Copy implements IChunkGet { + + 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 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; + this.chunkZ = chunkZ; + 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 - minSectionPosition; + blocks[index] = data; + } + + protected void storeBiome(int x, int y, int z, BiomeType biome) { + if (biomes == null) { + biomes = new BiomeType[sectionCount][]; + } + int layer = (y >> 4) - minSectionPosition; + 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); + } + + 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")) { + entity.namedTag.putString("uuid", entityUUID.toString()); + } + entities.add(NukkitNbtConverter.toFawe(entity.namedTag)); + } + + private int layerIndex(int layer) { + return layer - minSectionPosition; + } + + @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) - minSectionPosition; + if (layer < 0 || layer >= sectionCount || 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 >= sectionCount || 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 this.entities; + } + + @Override + public Set getFullEntities() { + 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 < sectionCount && blocks[index] != null; + } + + @Override + public char[] load(int layer) { + int index = layerIndex(layer); + if (index < 0 || index >= sectionCount) { + 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 >= sectionCount) { + 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 sectionCount; + } + + @Override + public int getMinSectionPosition() { + return minSectionPosition; + } + + @Override + public int getMaxSectionPosition() { + return maxY >> 4; + } + + @Override + public int getMaxY() { + return maxY; + } + + @Override + public int getMinY() { + return minY; + } + + @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/src/main/java/com/sk89q/worldedit/nukkit/NukkitItemRegistry.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitItemRegistry.java new file mode 100644 index 0000000000..7adb287824 --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitItemRegistry.java @@ -0,0 +1,18 @@ +package com.sk89q.worldedit.nukkit; + +import com.fastasyncworldedit.nukkit.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/src/main/java/com/sk89q/worldedit/nukkit/NukkitPlayer.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitPlayer.java new file mode 100644 index 0000000000..a3e2511838 --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitPlayer.java @@ -0,0 +1,299 @@ +package com.sk89q.worldedit.nukkit; + +import cn.nukkit.AdventureSettings; +import cn.nukkit.Player; +import cn.nukkit.Server; +import cn.nukkit.level.Level; +import cn.nukkit.network.protocol.UpdateBlockPacket; +import cn.nukkit.permission.PermissionAttachment; +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; +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.util.Locale; +import java.util.UUID; + +public class NukkitPlayer extends AbstractPlayerActor { + + private final Player player; + private PermissionAttachment permAttachment; + + 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("§d" + part); + } + } + + @Deprecated + @Override + public void printDebug(String msg) { + for (String part : msg.split("\n")) { + player.sendMessage("§7" + part); + } + } + + @Deprecated + @Override + public void printError(String msg) { + for (String part : msg.split("\n")) { + player.sendMessage("§c" + 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 new NukkitPlayerBlockBag(player); + } + + @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) { + if (permAttachment == null) { + permAttachment = player.addAttachment(WorldEditNukkitPlugin.getInstance()); + } + permAttachment.setPermission(permission, value); + } + + void removePermissionAttachment() { + if (permAttachment != null) { + permAttachment.remove(); + permAttachment = null; + } + } + + @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.getAdventureSettings().set(AdventureSettings.Type.ALLOW_FLIGHT, true); + player.getAdventureSettings().set(AdventureSettings.Type.FLYING, flying); + player.getAdventureSettings().update(); + } + + @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() { + try { + String code = NukkitImplLoader.get().getPlayerLanguageCode(player); + if (code == null) { + return Locale.getDefault(); + } + return Locale.forLanguageTag(code.replace('_', '-')); + } catch (Exception e) { + return Locale.getDefault(); + } + } + + @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) { + 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 { + NukkitImplAdapter adapter = NukkitImplLoader.get(); + int fullId = NukkitAdapter.adaptFullId(block.toImmutableState()); + 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 = adapter.getBlockRuntimeId(player, blockId, meta); + player.dataPacket(pk); + } + } + + @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/src/main/java/com/sk89q/worldedit/nukkit/NukkitRegistries.java b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitRegistries.java new file mode 100644 index 0000000000..7c9a0d6f9d --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitRegistries.java @@ -0,0 +1,33 @@ +package com.sk89q.worldedit.nukkit; + +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() { + } + + public static NukkitRegistries getInstance() { + return INSTANCE; + } + + @Override + public BlockRegistry getBlockRegistry() { + return blockRegistry; + } + + @Override + public ItemRegistry getItemRegistry() { + return itemRegistry; + } + +} 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 new file mode 100644 index 0000000000..dbff9d5d48 --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitServerInterface.java @@ -0,0 +1,244 @@ +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.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; +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; +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.Arrays; +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 NukkitImplLoader.get().getPlatformName(); + } + + @Override + public String getPlatformVersion() { + return plugin.getDescription().getVersion(); + } + + @Override + public String id() { + return "intellectualsites:nukkit"; + } + + @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(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 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 new file mode 100644 index 0000000000..2b4844c8c3 --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitWorld.java @@ -0,0 +1,372 @@ +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.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; +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.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.weather.WeatherType; +import com.sk89q.worldedit.world.weather.WeatherTypes; + +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 { + NukkitImplAdapter adapter = NukkitImplLoader.get(); + int fullId = NukkitAdapter.adaptFullId(block.toImmutableState()); + int blockId = fullId >> adapter.getBlockDataBits(); + int meta = fullId & adapter.getBlockDataMask(); + + Level level = getLevel(); + Block nukkitBlock = Block.get(blockId, meta); + // 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(); + } + + @Override + public BaseBlock getFullBlock(BlockVector3 position) { + 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.nukkit.NukkitNbtConverter.toLinCompound(be.namedTag) + )); + } + return state.toBaseBlock(); + } + + @Override + public BlockState getBlock(BlockVector3 position) { + Level level = getLevel(); + Block block = level.getBlock(position.x(), position.y(), position.z()); + int fullId = (block.getId() << NukkitImplLoader.get().getBlockDataBits()) | 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.useBreakOn(NukkitAdapter.adapt(position)); + } + + @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(); + 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; + } + + @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) { + // 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 + 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.nukkit.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 WeatherType getWeather() { + Level level = getLevel(); + if (level.isThundering()) { + return WeatherTypes.THUNDER_STORM; + } else if (level.isRaining()) { + return WeatherTypes.RAIN; + } + return WeatherTypes.CLEAR; + } + + @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 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); + 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 + } + +} 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 new file mode 100644 index 0000000000..289036cfe1 --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/NukkitWorldEditListener.java @@ -0,0 +1,159 @@ +package com.sk89q.worldedit.nukkit; + +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.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.concurrent.TimeUnit; + +/** + * 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 duplicate {@code PlayerInteractEvent} and {@link #onBlockBreak} can be skipped. + * Entries expire after 1 seconds in case {@code BlockBreakEvent} is never fired. + */ + private final Cache handledLeftClick = CacheBuilder.newBuilder() + .expireAfterWrite(1, TimeUnit.SECONDS) + .weakKeys() + .build(); + + 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()) { + return; + } + + Player nukkitPlayer = event.getPlayer(); + NukkitPlayer player = NukkitAdapter.adapt(nukkitPlayer); + WorldEdit we = WorldEdit.getInstance(); + + 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()); + if (we.handleBlockLeftClick(player, loc, direction)) { + handledLeftClick.put(nukkitPlayer, Boolean.TRUE); + 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()); + if (we.handleBlockRightClick(player, loc, direction)) { + 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) { + if (!plugin.getInternalPlatform().isHookingEvents()) { + return; + } + + Player nukkitPlayer = event.getPlayer(); + + // Skip if already handled by PlayerInteractEvent(LEFT_CLICK_BLOCK) + if (handledLeftClick.getIfPresent(nukkitPlayer) != null) { + event.setCancelled(true); + return; + } + + NukkitPlayer player = NukkitAdapter.adapt(nukkitPlayer); + Block block = event.getBlock(); + Location loc = new Location( + player.getWorld(), + Vector3.at(block.getFloorX(), block.getFloorY(), block.getFloorZ()) + ); + Direction direction = adaptFace(event.getFace()); + if (WorldEdit.getInstance().handleBlockLeftClick(player, loc, direction)) { + handledLeftClick.put(nukkitPlayer, Boolean.TRUE); + event.setCancelled(true); + } + } + + @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)) + ); + NukkitAdapter.uncachePlayer(nukkitPlayer); + } + +} 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 new file mode 100644 index 0000000000..591908c647 --- /dev/null +++ b/worldedit-nukkit/src/main/java/com/sk89q/worldedit/nukkit/WorldEditNukkitPlugin.java @@ -0,0 +1,165 @@ +package com.sk89q.worldedit.nukkit; + +import cn.nukkit.plugin.PluginBase; +import com.fastasyncworldedit.core.Fawe; +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; +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. + */ +public class WorldEditNukkitPlugin extends PluginBase { + + private static WorldEditNukkitPlugin instance; + private NukkitConfiguration configuration; + private NukkitServerInterface platform; + + public static WorldEditNukkitPlugin getInstance() { + return instance; + } + + @Override + 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(); + + // 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 enabled."); + } + + @Override + public void onDisable() { + // FAWE cleanup (may be null if onEnable failed early) + Fawe fawe = Fawe.instance(); + if (fawe != null) { + fawe.onDisable(); + } + + WorldEdit worldEdit = WorldEdit.getInstance(); + + // Unload sessions + worldEdit.getSessionManager().unload(); + + // 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 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; + } + +} diff --git a/worldedit-nukkit/src/main/resources/plugin.yml b/worldedit-nukkit/src/main/resources/plugin.yml new file mode 100644 index 0000000000..a6037276f0 --- /dev/null +++ b/worldedit-nukkit/src/main/resources/plugin.yml @@ -0,0 +1,8 @@ +name: FastAsyncWorldEdit +main: com.sk89q.worldedit.nukkit.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.