diff --git a/build.gradle.kts b/build.gradle.kts index 468881e..a98ff0c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,8 +7,8 @@ plugins { id("com.github.johnrengelman.shadow") version "8.1.1" } -group = "com.moulberry.com.moulberry.axiom" -version = "1.0.0-SNAPSHOT" +group = "com.moulberry.axiom" +version = "1.2.1" description = "Serverside component for Axiom on Paper" java { @@ -17,13 +17,27 @@ java { } repositories { + mavenCentral() maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") maven("https://jitpack.io") + maven("https://maven.enginehub.org/repo/") + maven("https://repo.papermc.io/repository/maven-public/") } dependencies { paperweight.paperDevBundle("1.20.1-R0.1-SNAPSHOT") implementation("xyz.jpenilla:reflection-remapper:0.1.0-SNAPSHOT") + + // Zstd Compression Library + implementation("com.github.luben:zstd-jni:1.5.5-4") + + // WorldGuard support + compileOnly("com.sk89q.worldguard:worldguard-bukkit:7.1.0-SNAPSHOT") + + // PlotSquared support + implementation(platform("com.intellectualsites.bom:bom-newest:1.37")) + compileOnly("com.intellectualsites.plotsquared:plotsquared-core") + compileOnly("com.intellectualsites.plotsquared:plotsquared-bukkit") { isTransitive = false } } tasks { diff --git a/src/main/java/com/moulberry/axiom/AxiomConstants.java b/src/main/java/com/moulberry/axiom/AxiomConstants.java new file mode 100644 index 0000000..f553d14 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/AxiomConstants.java @@ -0,0 +1,22 @@ +package com.moulberry.axiom; + +import net.minecraft.core.BlockPos; +import org.bukkit.NamespacedKey; + +public class AxiomConstants { + + public static final long MIN_POSITION_LONG = BlockPos.asLong(-33554432, -2048, -33554432); + static { + if (MIN_POSITION_LONG != 0b1000000000000000000000000010000000000000000000000000100000000000L) { + throw new Error("BlockPos representation changed!"); + } + } + + public static final int API_VERSION = 5; + public static final NamespacedKey ACTIVE_HOTBAR_INDEX = new NamespacedKey("axiom", "active_hotbar_index"); + public static final NamespacedKey HOTBAR_DATA = new NamespacedKey("axiom", "hotbar_data"); + + public static final NamespacedKey ACTIVE_VIEW = new NamespacedKey("axiom", "active_view"); + public static final NamespacedKey VIEWS = new NamespacedKey("axiom", "views"); + +} diff --git a/src/main/java/com/moulberry/axiom/AxiomPaper.java b/src/main/java/com/moulberry/axiom/AxiomPaper.java index eff5682..0b096eb 100644 --- a/src/main/java/com/moulberry/axiom/AxiomPaper.java +++ b/src/main/java/com/moulberry/axiom/AxiomPaper.java @@ -1,245 +1,55 @@ package com.moulberry.axiom; -import com.moulberry.axiom.packet.AxiomBigPayloadHandler; -import com.moulberry.axiom.packet.SetBlockBufferPacketListener; -import com.moulberry.axiom.packet.SetBlockPacketListener; -import com.moulberry.axiom.persistence.ItemStackDataType; -import com.moulberry.axiom.persistence.UUIDDataType; +import com.moulberry.axiom.buffer.CompressedBlockEntity; +import com.moulberry.axiom.packet.*; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.papermc.paper.event.player.PlayerFailMoveEvent; import io.papermc.paper.network.ChannelInitializeListener; import io.papermc.paper.network.ChannelInitializeListenerHolder; import net.kyori.adventure.key.Key; -import net.kyori.adventure.text.Component; -import net.minecraft.core.BlockPos; -import net.minecraft.core.registries.Registries; import net.minecraft.network.Connection; import net.minecraft.network.ConnectionProtocol; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.PacketFlow; import net.minecraft.network.protocol.game.ServerboundCustomPayloadPacket; -import net.minecraft.resources.ResourceKey; -import net.minecraft.world.level.GameType; -import net.minecraft.world.level.Level; import org.bukkit.*; -import org.bukkit.craftbukkit.v1_20_R1.entity.CraftPlayer; -import org.bukkit.craftbukkit.v1_20_R1.inventory.CraftItemStack; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; -import org.bukkit.inventory.ItemStack; -import org.bukkit.persistence.PersistentDataContainer; -import org.bukkit.persistence.PersistentDataType; import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.plugin.messaging.Messenger; import org.checkerframework.checker.nullness.qual.NonNull; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; public class AxiomPaper extends JavaPlugin implements Listener { - public static final long MIN_POSITION_LONG = BlockPos.asLong(-33554432, -2048, -33554432); - static { - if (MIN_POSITION_LONG != 0b1000000000000000000000000010000000000000000000000000100000000000L) { - throw new Error("BlockPos representation changed!"); - } - } - - private static final int API_VERSION = 4; - private static final NamespacedKey ACTIVE_HOTBAR_INDEX = new NamespacedKey("axiom", "active_hotbar_index"); - private static final NamespacedKey HOTBAR_DATA = new NamespacedKey("axiom", "hotbar_data"); - - private static final NamespacedKey ACTIVE_VIEW = new NamespacedKey("axiom", "active_view"); - private static final NamespacedKey VIEWS = new NamespacedKey("axiom", "views"); - @Override public void onEnable() { Bukkit.getPluginManager().registerEvents(this, this); + CompressedBlockEntity.initialize(this); - Bukkit.getMessenger().registerOutgoingPluginChannel(this, "axiom:enable"); - Bukkit.getMessenger().registerOutgoingPluginChannel(this, "axiom:initialize_hotbars"); - Bukkit.getMessenger().registerOutgoingPluginChannel(this, "axiom:set_editor_views"); + Messenger msg = Bukkit.getMessenger(); - HashSet activeAxiomPlayers = new HashSet<>(); + msg.registerOutgoingPluginChannel(this, "axiom:enable"); + msg.registerOutgoingPluginChannel(this, "axiom:initialize_hotbars"); + msg.registerOutgoingPluginChannel(this, "axiom:set_editor_views"); + msg.registerOutgoingPluginChannel(this, "axiom:block_entities"); - Bukkit.getMessenger().registerIncomingPluginChannel(this, "axiom:hello", (channel, player, message) -> { - if (!player.hasPermission("axiom.*")) { - return; - } + final Set activeAxiomPlayers = Collections.newSetFromMap(new ConcurrentHashMap<>()); - FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); - int apiVersion = friendlyByteBuf.readVarInt(); - friendlyByteBuf.readNbt(); // Discard - - if (apiVersion != API_VERSION) { - player.kick(Component.text("Unsupported Axiom API Version. Server supports " + API_VERSION + - ", while client is " + apiVersion)); - return; - } - - activeAxiomPlayers.add(player.getUniqueId()); - - // Enable - FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); - buf.writeBoolean(true); - buf.writeByte(0); // todo: world properties - buf.writeInt(0x100000); // Max Buffer Size - buf.writeBoolean(false); // No source info - buf.writeBoolean(false); // No source settings - buf.writeVarInt(5); // Maximum Reach - buf.writeVarInt(16); // Max editor views - buf.writeBoolean(true); // Editable Views - player.sendPluginMessage(this, "axiom:enable", buf.accessByteBufWithCorrectSize()); - - // Initialize Hotbars - PersistentDataContainer container = player.getPersistentDataContainer(); - int activeHotbarIndex = container.getOrDefault(ACTIVE_HOTBAR_INDEX, PersistentDataType.BYTE, (byte) 0); - PersistentDataContainer hotbarItems = container.get(HOTBAR_DATA, PersistentDataType.TAG_CONTAINER); - if (hotbarItems != null) { - buf = new FriendlyByteBuf(Unpooled.buffer()); - buf.writeByte((byte) activeHotbarIndex); - for (int i=0; i<9*9; i++) { - // Ignore selected hotbar - if (i / 9 == activeHotbarIndex) { - buf.writeItem(net.minecraft.world.item.ItemStack.EMPTY); - } else { - ItemStack stack = hotbarItems.get(new NamespacedKey("axiom", "slot_"+i), ItemStackDataType.INSTANCE); - buf.writeItem(CraftItemStack.asNMSCopy(stack)); - } - } - player.sendPluginMessage(this, "axiom:initialize_hotbars", buf.accessByteBufWithCorrectSize()); - } - - // Initialize Views - UUID activeView = container.get(ACTIVE_VIEW, UUIDDataType.INSTANCE); - if (activeView != null) { - buf = new FriendlyByteBuf(Unpooled.buffer()); - buf.writeUUID(activeView); - - PersistentDataContainer[] views = container.get(VIEWS, PersistentDataType.TAG_CONTAINER_ARRAY); - buf.writeVarInt(views.length); - for (PersistentDataContainer view : views) { - View.load(view).write(buf); - } - - player.sendPluginMessage(this, "axiom:set_editor_views", buf.accessByteBufWithCorrectSize()); - } - }); - Bukkit.getMessenger().registerIncomingPluginChannel(this, "axiom:set_gamemode", (channel, player, message) -> { - if (!player.hasPermission("axiom.*")) { - return; - } - - FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); - GameType gameType = GameType.byId(friendlyByteBuf.readByte()); - ((CraftPlayer)player).getHandle().setGameMode(gameType); - }); - Bukkit.getMessenger().registerIncomingPluginChannel(this, "axiom:set_fly_speed", (channel, player, message) -> { - if (!player.hasPermission("axiom.*")) { - return; - } - - FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); - float flySpeed = friendlyByteBuf.readFloat(); - ((CraftPlayer)player).getHandle().getAbilities().setFlyingSpeed(flySpeed); - }); - Bukkit.getMessenger().registerIncomingPluginChannel(this, "axiom:set_block", new SetBlockPacketListener(this)); - Bukkit.getMessenger().registerIncomingPluginChannel(this, "axiom:set_hotbar_slot", (channel, player, message) -> { - if (!player.hasPermission("axiom.*")) { - return; - } - - FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); - int index = friendlyByteBuf.readByte(); - if (index < 0 || index >= 9*9) return; - net.minecraft.world.item.ItemStack nmsStack = friendlyByteBuf.readItem(); - - PersistentDataContainer container = player.getPersistentDataContainer(); - PersistentDataContainer hotbarItems = container.get(HOTBAR_DATA, PersistentDataType.TAG_CONTAINER); - if (hotbarItems == null) hotbarItems = container.getAdapterContext().newPersistentDataContainer(); - hotbarItems.set(new NamespacedKey("axiom", "slot_"+index), ItemStackDataType.INSTANCE, CraftItemStack.asCraftMirror(nmsStack)); - container.set(HOTBAR_DATA, PersistentDataType.TAG_CONTAINER, hotbarItems); - }); - Bukkit.getMessenger().registerIncomingPluginChannel(this, "axiom:switch_active_hotbar", (channel, player, message) -> { - if (!player.hasPermission("axiom.*")) { - return; - } - - FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); - int oldHotbarIndex = friendlyByteBuf.readByte(); - int activeHotbarIndex = friendlyByteBuf.readByte(); - - ItemStack[] hotbarItems = new ItemStack[9]; - for (int i=0; i<9; i++) { - hotbarItems[i] = CraftItemStack.asCraftMirror(friendlyByteBuf.readItem()); - } - - PersistentDataContainer container = player.getPersistentDataContainer(); - PersistentDataContainer containerHotbarItems = container.get(HOTBAR_DATA, PersistentDataType.TAG_CONTAINER); - if (containerHotbarItems == null) containerHotbarItems = container.getAdapterContext().newPersistentDataContainer(); - - for (int i=0; i<9; i++) { - if (oldHotbarIndex != activeHotbarIndex) { - int index = oldHotbarIndex*9 + i; - ItemStack stack = player.getInventory().getItem(i); - if (stack == null) { - stack = new ItemStack(Material.AIR); - } else { - stack = stack.clone(); - } - containerHotbarItems.set(new NamespacedKey("axiom", "slot_"+index), ItemStackDataType.INSTANCE, stack); - } - int index = activeHotbarIndex*9 + i; - containerHotbarItems.set(new NamespacedKey("axiom", "slot_"+index), ItemStackDataType.INSTANCE, hotbarItems[i].clone()); - if (player.getGameMode() == GameMode.CREATIVE) player.getInventory().setItem(i, hotbarItems[i]); - } - - container.set(HOTBAR_DATA, PersistentDataType.TAG_CONTAINER, containerHotbarItems); - container.set(ACTIVE_HOTBAR_INDEX, PersistentDataType.BYTE, (byte) activeHotbarIndex); - }); - Bukkit.getMessenger().registerIncomingPluginChannel(this, "axiom:teleport", (channel, player, message) -> { - if (!player.hasPermission("axiom.*")) { - return; - } - - FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); - ResourceKey resourceKey = friendlyByteBuf.readResourceKey(Registries.DIMENSION); - double x = friendlyByteBuf.readDouble(); - double y = friendlyByteBuf.readDouble(); - double z = friendlyByteBuf.readDouble(); - float yRot = friendlyByteBuf.readFloat(); - float xRot = friendlyByteBuf.readFloat(); - - NamespacedKey namespacedKey = new NamespacedKey(resourceKey.location().getNamespace(), resourceKey.location().getPath()); - World world = Bukkit.getWorld(namespacedKey); - if (world != null) { - player.teleport(new Location(world, x, y, z, yRot, xRot)); - } - }); - Bukkit.getMessenger().registerIncomingPluginChannel(this, "axiom:set_editor_views", (channel, player, message) -> { - if (!player.hasPermission("axiom.*")) { - return; - } - - FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); - UUID uuid = friendlyByteBuf.readUUID(); - List views = friendlyByteBuf.readList(View::read); - - PersistentDataContainer container = player.getPersistentDataContainer(); - container.set(ACTIVE_VIEW, UUIDDataType.INSTANCE, uuid); - - PersistentDataContainer[] containerArray = new PersistentDataContainer[views.size()]; - for (int i = 0; i < views.size(); i++) { - PersistentDataContainer viewContainer = container.getAdapterContext().newPersistentDataContainer(); - views.get(i).save(viewContainer); - containerArray[i] = viewContainer; - } - container.set(VIEWS, PersistentDataType.TAG_CONTAINER_ARRAY, containerArray); - }); + msg.registerIncomingPluginChannel(this, "axiom:hello", new HelloPacketListener(this, activeAxiomPlayers)); + msg.registerIncomingPluginChannel(this, "axiom:set_gamemode", new SetGamemodePacketListener()); + msg.registerIncomingPluginChannel(this, "axiom:set_fly_speed", new SetFlySpeedPacketListener()); + msg.registerIncomingPluginChannel(this, "axiom:set_block", new SetBlockPacketListener(this)); + msg.registerIncomingPluginChannel(this, "axiom:set_hotbar_slot", new SetHotbarSlotPacketListener()); + msg.registerIncomingPluginChannel(this, "axiom:switch_active_hotbar", new SwitchActiveHotbarPacketListener()); + msg.registerIncomingPluginChannel(this, "axiom:teleport", new TeleportPacketListener()); + msg.registerIncomingPluginChannel(this, "axiom:set_editor_views", new SetEditorViewsPacketListener()); + msg.registerIncomingPluginChannel(this, "axiom:request_block_entity", new RequestBlockEntityPacketListener(this)); SetBlockBufferPacketListener setBlockBufferPacketListener = new SetBlockBufferPacketListener(this); diff --git a/src/main/java/com/moulberry/axiom/buffer/BlockBuffer.java b/src/main/java/com/moulberry/axiom/buffer/BlockBuffer.java index cb3f90c..6cc39bb 100644 --- a/src/main/java/com/moulberry/axiom/buffer/BlockBuffer.java +++ b/src/main/java/com/moulberry/axiom/buffer/BlockBuffer.java @@ -1,15 +1,19 @@ package com.moulberry.axiom.buffer; +import com.moulberry.axiom.AxiomConstants; import com.moulberry.axiom.AxiomPaper; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectSet; +import it.unimi.dsi.fastutil.shorts.Short2ObjectMap; +import it.unimi.dsi.fastutil.shorts.Short2ObjectOpenHashMap; import net.minecraft.core.BlockPos; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.chunk.PalettedContainer; +import org.jetbrains.annotations.Nullable; public class BlockBuffer { @@ -18,8 +22,8 @@ public class BlockBuffer { private final Long2ObjectMap> values; private PalettedContainer last = null; - private long lastId = AxiomPaper.MIN_POSITION_LONG; - private int count; + private long lastId = AxiomConstants.MIN_POSITION_LONG; + private final Long2ObjectMap> blockEntities = new Long2ObjectOpenHashMap<>(); public BlockBuffer() { this.values = new Long2ObjectOpenHashMap<>(); @@ -29,17 +33,24 @@ public class BlockBuffer { this.values = values; } - public int getCount() { - return this.count; - } - public void save(FriendlyByteBuf friendlyByteBuf) { for (Long2ObjectMap.Entry> entry : this.entrySet()) { friendlyByteBuf.writeLong(entry.getLongKey()); entry.getValue().write(friendlyByteBuf); + + Short2ObjectMap blockEntities = this.blockEntities.get(entry.getLongKey()); + if (blockEntities != null) { + friendlyByteBuf.writeVarInt(blockEntities.size()); + for (Short2ObjectMap.Entry entry2 : blockEntities.short2ObjectEntrySet()) { + friendlyByteBuf.writeShort(entry2.getShortKey()); + entry2.getValue().write(friendlyByteBuf); + } + } else { + friendlyByteBuf.writeVarInt(0); + } } - friendlyByteBuf.writeLong(AxiomPaper.MIN_POSITION_LONG); + friendlyByteBuf.writeLong(AxiomConstants.MIN_POSITION_LONG); } public static BlockBuffer load(FriendlyByteBuf friendlyByteBuf) { @@ -47,10 +58,21 @@ public class BlockBuffer { while (true) { long index = friendlyByteBuf.readLong(); - if (index == AxiomPaper.MIN_POSITION_LONG) break; + if (index == AxiomConstants.MIN_POSITION_LONG) break; PalettedContainer palettedContainer = buffer.getOrCreateSection(index); palettedContainer.read(friendlyByteBuf); + + int blockEntitySize = Math.min(4096, friendlyByteBuf.readVarInt()); + if (blockEntitySize > 0) { + Short2ObjectMap map = new Short2ObjectOpenHashMap<>(blockEntitySize); + for (int i = 0; i < blockEntitySize; i++) { + short offset = friendlyByteBuf.readShort(); + CompressedBlockEntity blockEntity = CompressedBlockEntity.read(friendlyByteBuf); + map.put(offset, blockEntity); + } + buffer.blockEntities.put(index, map); + } } return buffer; @@ -58,10 +80,34 @@ public class BlockBuffer { public void clear() { this.last = null; - this.lastId = AxiomPaper.MIN_POSITION_LONG; + this.lastId = AxiomConstants.MIN_POSITION_LONG; this.values.clear(); } + public void putBlockEntity(int x, int y, int z, CompressedBlockEntity blockEntity) { + long cpos = BlockPos.asLong(x >> 4, y >> 4, z >> 4); + Short2ObjectMap chunkMap = this.blockEntities.computeIfAbsent(cpos, k -> new Short2ObjectOpenHashMap<>()); + + int key = (x & 0xF) | ((y & 0xF) << 4) | ((z & 0xF) << 8); + chunkMap.put((short)key, blockEntity); + } + + @Nullable + public CompressedBlockEntity getBlockEntity(int x, int y, int z) { + long cpos = BlockPos.asLong(x >> 4, y >> 4, z >> 4); + Short2ObjectMap chunkMap = this.blockEntities.get(cpos); + + if (chunkMap == null) return null; + + int key = (x & 0xF) | ((y & 0xF) << 4) | ((z & 0xF) << 8); + return chunkMap.get((short)key); + } + + @Nullable + public Short2ObjectMap getBlockEntityChunkMap(long cpos) { + return this.blockEntities.get(cpos); + } + public BlockState get(int x, int y, int z) { var container = this.getSectionForCoord(x, y, z); if (container == null) { @@ -79,23 +125,11 @@ public class BlockBuffer { public void set(int x, int y, int z, BlockState state) { var container = this.getOrCreateSectionForCoord(x, y, z); var old = container.getAndSet(x & 0xF, y & 0xF, z & 0xF, state); - - if (old == EMPTY_STATE) { - if (state != EMPTY_STATE) this.count += 1; - } else if (state == EMPTY_STATE) { - this.count -= 1; - } } public void set(int cx, int cy, int cz, int lx, int ly, int lz, BlockState state) { var container = this.getOrCreateSection(BlockPos.asLong(cx, cy, cz)); var old = container.getAndSet(lx, ly, lz, state); - - if (old == EMPTY_STATE) { - if (state != EMPTY_STATE) this.count += 1; - } else if (state == EMPTY_STATE) { - this.count -= 1; - } } public BlockState remove(int x, int y, int z) { diff --git a/src/main/java/com/moulberry/axiom/buffer/CompressedBlockEntity.java b/src/main/java/com/moulberry/axiom/buffer/CompressedBlockEntity.java new file mode 100644 index 0000000..76f9bba --- /dev/null +++ b/src/main/java/com/moulberry/axiom/buffer/CompressedBlockEntity.java @@ -0,0 +1,66 @@ +package com.moulberry.axiom.buffer; + +import com.github.luben.zstd.Zstd; +import com.github.luben.zstd.ZstdDictCompress; +import com.github.luben.zstd.ZstdDictDecompress; +import com.moulberry.axiom.AxiomPaper; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.network.FriendlyByteBuf; + +import java.io.*; +import java.util.Objects; + +public record CompressedBlockEntity(int originalSize, byte compressionDict, byte[] compressed) { + + private static ZstdDictCompress zstdDictCompress = null; + private static ZstdDictDecompress zstdDictDecompress = null; + + public static void initialize(AxiomPaper plugin) { + try (InputStream is = Objects.requireNonNull(plugin.getResource("zstd_dictionaries/block_entities_v1.dict"))) { + byte[] bytes = is.readAllBytes(); + zstdDictCompress = new ZstdDictCompress(bytes, Zstd.defaultCompressionLevel()); + zstdDictDecompress = new ZstdDictDecompress(bytes); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static CompressedBlockEntity compress(CompoundTag tag, ByteArrayOutputStream baos) { + try { + baos.reset(); + DataOutputStream dos = new DataOutputStream(baos); + NbtIo.write(tag, dos); + byte[] uncompressed = baos.toByteArray(); + byte[] compressed = Zstd.compress(uncompressed, zstdDictCompress); + return new CompressedBlockEntity(uncompressed.length, (byte) 0, compressed); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public CompoundTag decompress() { + if (this.compressionDict != 0) throw new UnsupportedOperationException("Unknown compression dict: " + this.compressionDict); + + try { + byte[] nbt = Zstd.decompress(this.compressed, zstdDictDecompress, this.originalSize); + return NbtIo.read(new DataInputStream(new ByteArrayInputStream(nbt))); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static CompressedBlockEntity read(FriendlyByteBuf friendlyByteBuf) { + int originalSize = friendlyByteBuf.readVarInt(); + byte compressionDict = friendlyByteBuf.readByte(); + byte[] compressed = friendlyByteBuf.readByteArray(); + return new CompressedBlockEntity(originalSize, compressionDict, compressed); + } + + public void write(FriendlyByteBuf friendlyByteBuf) { + friendlyByteBuf.writeVarInt(this.originalSize); + friendlyByteBuf.writeByte(this.compressionDict); + friendlyByteBuf.writeByteArray(this.compressed); + } + +} diff --git a/src/main/java/com/moulberry/axiom/buffer/Position2ByteMap.java b/src/main/java/com/moulberry/axiom/buffer/Position2ByteMap.java index d4c7d6f..438872e 100644 --- a/src/main/java/com/moulberry/axiom/buffer/Position2ByteMap.java +++ b/src/main/java/com/moulberry/axiom/buffer/Position2ByteMap.java @@ -1,5 +1,6 @@ package com.moulberry.axiom.buffer; +import com.moulberry.axiom.AxiomConstants; import com.moulberry.axiom.AxiomPaper; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; @@ -20,7 +21,7 @@ public class Position2ByteMap { private final LongFunction defaultFunction; private final Long2ObjectMap map = new Long2ObjectOpenHashMap<>(); - private long lastChunkPos = AxiomPaper.MIN_POSITION_LONG; + private long lastChunkPos = AxiomConstants.MIN_POSITION_LONG; private byte[] lastChunk = null; public Position2ByteMap() { @@ -47,7 +48,7 @@ public class Position2ByteMap { friendlyByteBuf.writeLong(entry.getLongKey()); friendlyByteBuf.writeBytes(entry.getValue()); } - friendlyByteBuf.writeLong(AxiomPaper.MIN_POSITION_LONG); + friendlyByteBuf.writeLong(AxiomConstants.MIN_POSITION_LONG); } public static Position2ByteMap load(FriendlyByteBuf friendlyByteBuf) { @@ -55,7 +56,7 @@ public class Position2ByteMap { while (true) { long pos = friendlyByteBuf.readLong(); - if (pos == AxiomPaper.MIN_POSITION_LONG) break; + if (pos == AxiomConstants.MIN_POSITION_LONG) break; byte[] bytes = new byte[16*16*16]; friendlyByteBuf.readBytes(bytes); @@ -67,7 +68,7 @@ public class Position2ByteMap { public void clear() { this.map.clear(); - this.lastChunkPos = AxiomPaper.MIN_POSITION_LONG; + this.lastChunkPos = AxiomConstants.MIN_POSITION_LONG; this.lastChunk = null; } diff --git a/src/main/java/com/moulberry/axiom/event/AxiomFlySpeedChangeEvent.java b/src/main/java/com/moulberry/axiom/event/AxiomFlySpeedChangeEvent.java new file mode 100644 index 0000000..a914830 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/event/AxiomFlySpeedChangeEvent.java @@ -0,0 +1,50 @@ +package com.moulberry.axiom.event; + +import org.bukkit.GameMode; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +public class AxiomFlySpeedChangeEvent extends Event implements Cancellable { + + private static final HandlerList HANDLERS = new HandlerList(); + + private final Player player; + private final float flySpeed; + private boolean cancelled = false; + + public AxiomFlySpeedChangeEvent(Player player, float flySpeed) { + this.player = player; + this.flySpeed = flySpeed; + } + + public float getFlySpeed() { + return flySpeed; + } + + public Player getPlayer() { + return this.player; + } + + @Override + public boolean isCancelled() { + return this.cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } + + public static HandlerList getHandlerList() { + return HANDLERS; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } + +} diff --git a/src/main/java/com/moulberry/axiom/event/AxiomGameModeChangeEvent.java b/src/main/java/com/moulberry/axiom/event/AxiomGameModeChangeEvent.java new file mode 100644 index 0000000..86da786 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/event/AxiomGameModeChangeEvent.java @@ -0,0 +1,51 @@ +package com.moulberry.axiom.event; + +import org.bukkit.GameMode; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +public class AxiomGameModeChangeEvent extends Event implements Cancellable { + + private static final HandlerList HANDLERS = new HandlerList(); + + private final Player player; + private final GameMode gameMode; + private boolean cancelled = false; + + public AxiomGameModeChangeEvent(Player player, GameMode gameMode) { + this.player = player; + this.gameMode = gameMode; + } + + public GameMode getGameMode() { + return gameMode; + } + + public Player getPlayer() { + return this.player; + } + + @Override + public boolean isCancelled() { + return this.cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } + + public static HandlerList getHandlerList() { + return HANDLERS; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } + +} diff --git a/src/main/java/com/moulberry/axiom/event/AxiomHandshakeEvent.java b/src/main/java/com/moulberry/axiom/event/AxiomHandshakeEvent.java new file mode 100644 index 0000000..735c762 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/event/AxiomHandshakeEvent.java @@ -0,0 +1,52 @@ +package com.moulberry.axiom.event; + +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +public class AxiomHandshakeEvent extends Event implements Cancellable { + + private static final HandlerList HANDLERS = new HandlerList(); + + private final Player player; + private boolean cancelled = false; + private int maxBufferSize = 0x100000; + + public AxiomHandshakeEvent(Player player) { + this.player = player; + } + + public Player getPlayer() { + return this.player; + } + + public int getMaxBufferSize() { + return this.maxBufferSize; + } + + public void setMaxBufferSize(int maxBufferSize) { + this.maxBufferSize = maxBufferSize; + } + + @Override + public boolean isCancelled() { + return this.cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } + + public static HandlerList getHandlerList() { + return HANDLERS; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } + +} diff --git a/src/main/java/com/moulberry/axiom/event/AxiomModifyWorldEvent.java b/src/main/java/com/moulberry/axiom/event/AxiomModifyWorldEvent.java new file mode 100644 index 0000000..405948f --- /dev/null +++ b/src/main/java/com/moulberry/axiom/event/AxiomModifyWorldEvent.java @@ -0,0 +1,54 @@ +package com.moulberry.axiom.event; + +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +public class AxiomModifyWorldEvent extends Event implements Cancellable { + + private static final HandlerList HANDLERS = new HandlerList(); + + private final Player player; + private final World world; + private boolean cancelled; + + public AxiomModifyWorldEvent(Player player, World world) { + this.player = player; + this.world = world; + + // By default, changes are only allowed if the player is in the same world + // This behaviour can be changed by doing setCancelled(false) + this.cancelled = player.getWorld() != world; + } + + public World getWorld() { + return world; + } + + public Player getPlayer() { + return this.player; + } + + @Override + public boolean isCancelled() { + return this.cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } + + public static HandlerList getHandlerList() { + return HANDLERS; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } + +} diff --git a/src/main/java/com/moulberry/axiom/event/AxiomTeleportEvent.java b/src/main/java/com/moulberry/axiom/event/AxiomTeleportEvent.java new file mode 100644 index 0000000..2b7f800 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/event/AxiomTeleportEvent.java @@ -0,0 +1,51 @@ +package com.moulberry.axiom.event; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +public class AxiomTeleportEvent extends Event implements Cancellable { + + private static final HandlerList HANDLERS = new HandlerList(); + + private final Player player; + private final Location location; + private boolean cancelled = false; + + public AxiomTeleportEvent(Player player, Location location) { + this.player = player; + this.location = location; + } + + public Location getLocation() { + return this.location.clone(); + } + + public Player getPlayer() { + return this.player; + } + + @Override + public boolean isCancelled() { + return this.cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } + + public static HandlerList getHandlerList() { + return HANDLERS; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } + +} diff --git a/src/main/java/com/moulberry/axiom/integration/RegionProtection.java b/src/main/java/com/moulberry/axiom/integration/RegionProtection.java new file mode 100644 index 0000000..9b95e46 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/integration/RegionProtection.java @@ -0,0 +1,27 @@ +package com.moulberry.axiom.integration; + +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.entity.Player; + +public class RegionProtection { + + private final RegionProtectionWorldGuard worldGuard; + + public RegionProtection(Player player, World world) { + if (Bukkit.getPluginManager().isPluginEnabled("WorldGuard")) { + this.worldGuard = RegionProtectionWorldGuard.tryCreate(player, world); + } else { + this.worldGuard = null; + } + } + + public boolean canBuildInSection(int cx, int cy, int cz) { + if (this.worldGuard != null && !this.worldGuard.canBuildInSection(cx, cy, cz)) return false; + // todo: PlotSquared + return true; + } + + + +} diff --git a/src/main/java/com/moulberry/axiom/integration/RegionProtectionWorldGuard.java b/src/main/java/com/moulberry/axiom/integration/RegionProtectionWorldGuard.java new file mode 100644 index 0000000..784fb3e --- /dev/null +++ b/src/main/java/com/moulberry/axiom/integration/RegionProtectionWorldGuard.java @@ -0,0 +1,57 @@ +package com.moulberry.axiom.integration; + +import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldguard.LocalPlayer; +import com.sk89q.worldguard.WorldGuard; +import com.sk89q.worldguard.bukkit.WorldGuardPlugin; +import com.sk89q.worldguard.internal.platform.WorldGuardPlatform; +import com.sk89q.worldguard.protection.ApplicableRegionSet; +import com.sk89q.worldguard.protection.flags.Flags; +import com.sk89q.worldguard.protection.managers.RegionManager; +import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion; +import com.sk89q.worldguard.protection.regions.ProtectedRegion; +import com.sk89q.worldguard.protection.regions.RegionContainer; +import com.sk89q.worldguard.protection.regions.RegionQuery; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.Nullable; + +public class RegionProtectionWorldGuard { + + private final LocalPlayer player; + private final RegionManager regionManager; + + public RegionProtectionWorldGuard(LocalPlayer player, RegionManager regionManager) { + this.player = player; + this.regionManager = regionManager; + } + + @Nullable + public static RegionProtectionWorldGuard tryCreate(Player player, World world) { + WorldGuardPlatform platform = WorldGuard.getInstance().getPlatform(); + + RegionContainer regionContainer = platform.getRegionContainer(); + com.sk89q.worldedit.world.World worldEditWorld = BukkitAdapter.adapt(world); + LocalPlayer worldGuardPlayer = WorldGuardPlugin.inst().wrapPlayer(player); + + // Don't do any protection if player has bypass + if (platform.getSessionManager().hasBypass(worldGuardPlayer, worldEditWorld)) { + return null; + } + + RegionManager regionManager = regionContainer.get(worldEditWorld); + if (regionManager == null) return null; + + return new RegionProtectionWorldGuard(worldGuardPlayer, regionManager); + } + + public boolean canBuildInSection(int cx, int cy, int cz) { + BlockVector3 min = BlockVector3.at(cx*16, cy*16, cz*16); + BlockVector3 max = BlockVector3.at(cx*16+15, cy*16+15, cz*16+15); + ProtectedRegion test = new ProtectedCuboidRegion("dummy", min, max); + ApplicableRegionSet regions = this.regionManager.getApplicableRegions(test, RegionQuery.QueryOption.COMPUTE_PARENTS); + return regions.testState(this.player, Flags.BUILD); + } + +} diff --git a/src/main/java/com/moulberry/axiom/packet/AxiomBigPayloadHandler.java b/src/main/java/com/moulberry/axiom/packet/AxiomBigPayloadHandler.java index 0d85bbc..eb64174 100644 --- a/src/main/java/com/moulberry/axiom/packet/AxiomBigPayloadHandler.java +++ b/src/main/java/com/moulberry/axiom/packet/AxiomBigPayloadHandler.java @@ -27,9 +27,9 @@ public class AxiomBigPayloadHandler extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { try { + int readerIndex = in.readerIndex(); int i = in.readableBytes(); if (i != 0) { - int readerIndex = in.readerIndex(); FriendlyByteBuf buf = new FriendlyByteBuf(in); int packetId = buf.readVarInt(); @@ -38,13 +38,14 @@ public class AxiomBigPayloadHandler extends ByteToMessageDecoder { if (identifier.equals(SET_BUFFER)) { ServerPlayer player = connection.getPlayer(); if (player != null && player.getBukkitEntity().hasPermission("axiom.*")) { - listener.onReceive(player, buf); - return; + if (listener.onReceive(player, buf)) { + return; + } } } } - in.readerIndex(readerIndex); } + in.readerIndex(readerIndex); } catch (Exception e) { e.printStackTrace(); } diff --git a/src/main/java/com/moulberry/axiom/packet/HelloPacketListener.java b/src/main/java/com/moulberry/axiom/packet/HelloPacketListener.java new file mode 100644 index 0000000..e09e520 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/packet/HelloPacketListener.java @@ -0,0 +1,107 @@ +package com.moulberry.axiom.packet; + +import com.moulberry.axiom.AxiomConstants; +import com.moulberry.axiom.AxiomPaper; +import com.moulberry.axiom.View; +import com.moulberry.axiom.event.AxiomHandshakeEvent; +import com.moulberry.axiom.persistence.ItemStackDataType; +import com.moulberry.axiom.persistence.UUIDDataType; +import io.netty.buffer.Unpooled; +import net.kyori.adventure.text.Component; +import net.minecraft.network.FriendlyByteBuf; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.craftbukkit.v1_20_R1.inventory.CraftItemStack; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.plugin.messaging.PluginMessageListener; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; +import java.util.UUID; + +public class HelloPacketListener implements PluginMessageListener { + + private final AxiomPaper plugin; + private final Set activeAxiomPlayers; + + public HelloPacketListener(AxiomPaper plugin, Set activeAxiomPlayers) { + this.plugin = plugin; + this.activeAxiomPlayers = activeAxiomPlayers; + } + + @Override + public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, @NotNull byte[] message) { + if (!player.hasPermission("axiom.*")) { + return; + } + + FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); + int apiVersion = friendlyByteBuf.readVarInt(); + friendlyByteBuf.readNbt(); // Discard + + if (apiVersion != AxiomConstants.API_VERSION) { + player.kick(Component.text("Unsupported Axiom API Version. Server supports " + AxiomConstants.API_VERSION + + ", while client is " + apiVersion)); + return; + } + + // Call handshake event + AxiomHandshakeEvent handshakeEvent = new AxiomHandshakeEvent(player); + Bukkit.getPluginManager().callEvent(handshakeEvent); + if (handshakeEvent.isCancelled()) { + return; + } + + activeAxiomPlayers.add(player.getUniqueId()); + + // Enable + FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + buf.writeBoolean(true); + buf.writeByte(0); // todo: world properties + buf.writeInt(handshakeEvent.getMaxBufferSize()); // Max Buffer Size + buf.writeBoolean(false); // No source info + buf.writeBoolean(false); // No source settings + buf.writeVarInt(5); // Maximum Reach + buf.writeVarInt(16); // Max editor views + buf.writeBoolean(true); // Editable Views + player.sendPluginMessage(this.plugin, "axiom:enable", buf.accessByteBufWithCorrectSize()); + + // Initialize Hotbars + PersistentDataContainer container = player.getPersistentDataContainer(); + int activeHotbarIndex = container.getOrDefault(AxiomConstants.ACTIVE_HOTBAR_INDEX, PersistentDataType.BYTE, (byte) 0); + PersistentDataContainer hotbarItems = container.get(AxiomConstants.HOTBAR_DATA, PersistentDataType.TAG_CONTAINER); + if (hotbarItems != null) { + buf = new FriendlyByteBuf(Unpooled.buffer()); + buf.writeByte((byte) activeHotbarIndex); + for (int i=0; i<9*9; i++) { + // Ignore selected hotbar + if (i / 9 == activeHotbarIndex) { + buf.writeItem(net.minecraft.world.item.ItemStack.EMPTY); + } else { + ItemStack stack = hotbarItems.get(new NamespacedKey("axiom", "slot_"+i), ItemStackDataType.INSTANCE); + buf.writeItem(CraftItemStack.asNMSCopy(stack)); + } + } + player.sendPluginMessage(this.plugin, "axiom:initialize_hotbars", buf.accessByteBufWithCorrectSize()); + } + + // Initialize Views + UUID activeView = container.get(AxiomConstants.ACTIVE_VIEW, UUIDDataType.INSTANCE); + if (activeView != null) { + buf = new FriendlyByteBuf(Unpooled.buffer()); + buf.writeUUID(activeView); + + PersistentDataContainer[] views = container.get(AxiomConstants.VIEWS, PersistentDataType.TAG_CONTAINER_ARRAY); + buf.writeVarInt(views.length); + for (PersistentDataContainer view : views) { + View.load(view).write(buf); + } + + player.sendPluginMessage(this.plugin, "axiom:set_editor_views", buf.accessByteBufWithCorrectSize()); + } + } + +} diff --git a/src/main/java/com/moulberry/axiom/packet/RequestBlockEntityPacketListener.java b/src/main/java/com/moulberry/axiom/packet/RequestBlockEntityPacketListener.java new file mode 100644 index 0000000..84d3cf0 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/packet/RequestBlockEntityPacketListener.java @@ -0,0 +1,90 @@ +package com.moulberry.axiom.packet; + +import com.moulberry.axiom.AxiomPaper; +import com.moulberry.axiom.buffer.CompressedBlockEntity; +import io.netty.buffer.Unpooled; +import it.unimi.dsi.fastutil.longs.*; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import org.bukkit.craftbukkit.v1_20_R1.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.bukkit.plugin.messaging.PluginMessageListener; +import org.jetbrains.annotations.NotNull; + +import java.io.ByteArrayOutputStream; + +public class RequestBlockEntityPacketListener implements PluginMessageListener { + + private final AxiomPaper plugin; + + public RequestBlockEntityPacketListener(AxiomPaper plugin) { + this.plugin = plugin; + } + + @Override + public void onPluginMessageReceived(@NotNull String channel, @NotNull Player bukkitPlayer, @NotNull byte[] message) { + FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); + long id = friendlyByteBuf.readLong(); + + if (!bukkitPlayer.hasPermission("axiom.*")) { + // We always send an 'empty' response in order to make the client happy + sendEmptyResponse(bukkitPlayer, id); + return; + } + + ServerPlayer player = ((CraftPlayer)bukkitPlayer).getHandle(); + MinecraftServer server = player.getServer(); + if (server == null) { + sendEmptyResponse(bukkitPlayer, id); + return; + } + + ResourceKey worldKey = friendlyByteBuf.readResourceKey(Registries.DIMENSION); + ServerLevel level = server.getLevel(worldKey); + if (level == null) { + sendEmptyResponse(bukkitPlayer, id); + return; + } + + Long2ObjectMap map = new Long2ObjectOpenHashMap<>(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + BlockPos.MutableBlockPos mutableBlockPos = new BlockPos.MutableBlockPos(); + + // Save and compress block entities + int count = friendlyByteBuf.readVarInt(); + for (int i = 0; i < count; i++) { + long pos = friendlyByteBuf.readLong(); + BlockEntity blockEntity = level.getBlockEntity(mutableBlockPos.set(pos)); + if (blockEntity != null) { + CompoundTag tag = blockEntity.saveWithoutMetadata(); + map.put(pos, CompressedBlockEntity.compress(tag, baos)); + } + } + + // Send response packet + FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer(16)); + buf.writeLong(id); + buf.writeVarInt(map.size()); + for (Long2ObjectMap.Entry entry : map.long2ObjectEntrySet()) { + buf.writeLong(entry.getLongKey()); + entry.getValue().write(buf); + } + bukkitPlayer.sendPluginMessage(this.plugin, "axiom:block_entities", buf.accessByteBufWithCorrectSize()); + } + + private void sendEmptyResponse(Player player, long id) { + FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer(16)); + buf.writeLong(id); + buf.writeByte(0); // no block entities + player.sendPluginMessage(this.plugin, "axiom:block_entities", buf.accessByteBufWithCorrectSize()); + } + +} diff --git a/src/main/java/com/moulberry/axiom/packet/SetBlockBufferPacketListener.java b/src/main/java/com/moulberry/axiom/packet/SetBlockBufferPacketListener.java index 03c1aa2..44b7479 100644 --- a/src/main/java/com/moulberry/axiom/packet/SetBlockBufferPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/SetBlockBufferPacketListener.java @@ -3,8 +3,26 @@ package com.moulberry.axiom.packet; import com.moulberry.axiom.AxiomPaper; import com.moulberry.axiom.buffer.BiomeBuffer; import com.moulberry.axiom.buffer.BlockBuffer; +import com.moulberry.axiom.buffer.CompressedBlockEntity; +import com.moulberry.axiom.event.AxiomModifyWorldEvent; +import com.moulberry.axiom.integration.RegionProtection; +import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.world.World; +import com.sk89q.worldguard.LocalPlayer; +import com.sk89q.worldguard.WorldGuard; +import com.sk89q.worldguard.bukkit.WorldGuardPlugin; +import com.sk89q.worldguard.protection.ApplicableRegionSet; +import com.sk89q.worldguard.protection.flags.Flags; +import com.sk89q.worldguard.protection.flags.StateFlag; +import com.sk89q.worldguard.protection.managers.RegionManager; +import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion; +import com.sk89q.worldguard.protection.regions.ProtectedRegion; +import com.sk89q.worldguard.protection.regions.RegionContainer; +import com.sk89q.worldguard.protection.regions.RegionQuery; import io.netty.buffer.Unpooled; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.shorts.Short2ObjectMap; import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; import net.minecraft.core.Registry; @@ -29,6 +47,9 @@ import net.minecraft.world.level.chunk.LevelChunkSection; import net.minecraft.world.level.chunk.PalettedContainer; import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.level.lighting.LightEngine; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.craftbukkit.v1_20_R1.CraftWorld; import org.bukkit.craftbukkit.v1_20_R1.entity.CraftPlayer; import org.bukkit.entity.Player; import org.bukkit.plugin.messaging.PluginMessageListener; @@ -59,9 +80,9 @@ public class SetBlockBufferPacketListener { } } - public void onReceive(ServerPlayer player, FriendlyByteBuf friendlyByteBuf) { + public boolean onReceive(ServerPlayer player, FriendlyByteBuf friendlyByteBuf) { MinecraftServer server = player.getServer(); - if (server == null) return; + if (server == null) return false; ResourceKey worldKey = friendlyByteBuf.readResourceKey(Registries.DIMENSION); friendlyByteBuf.readUUID(); // Discard, we don't need to associate buffers @@ -74,20 +95,30 @@ public class SetBlockBufferPacketListener { byte type = friendlyByteBuf.readByte(); if (type == 0) { BlockBuffer buffer = BlockBuffer.load(friendlyByteBuf); - applyBlockBuffer(server, buffer, worldKey); + applyBlockBuffer(player, server, buffer, worldKey); } else if (type == 1) { BiomeBuffer buffer = BiomeBuffer.load(friendlyByteBuf); applyBiomeBuffer(server, buffer, worldKey); } else { throw new RuntimeException("Unknown buffer type: " + type); } + + return true; } - private void applyBlockBuffer(MinecraftServer server, BlockBuffer buffer, ResourceKey worldKey) { + private void applyBlockBuffer(ServerPlayer player, MinecraftServer server, BlockBuffer buffer, ResourceKey worldKey) { server.execute(() -> { ServerLevel world = server.getLevel(worldKey); if (world == null) return; + // Call AxiomModifyWorldEvent event + AxiomModifyWorldEvent modifyWorldEvent = new AxiomModifyWorldEvent(player.getBukkitEntity(), world.getWorld()); + Bukkit.getPluginManager().callEvent(modifyWorldEvent); + if (modifyWorldEvent.isCancelled()) return; + + RegionProtection regionProtection = new RegionProtection(player.getBukkitEntity(), world.getWorld()); + + // Allowed, apply buffer BlockPos.MutableBlockPos blockPos = new BlockPos.MutableBlockPos(); var lightEngine = world.getChunkSource().getLightEngine(); @@ -104,6 +135,10 @@ public class SetBlockBufferPacketListener { continue; } + if (!regionProtection.canBuildInSection(cx, cy, cz)) { + continue; + } + LevelChunk chunk = world.getChunk(cx, cz); chunk.setUnsaved(true); @@ -125,6 +160,8 @@ public class SetBlockBufferPacketListener { } } + Short2ObjectMap blockEntityChunkMap = buffer.getBlockEntityChunkMap(entry.getLongKey()); + sectionStates.acquire(); try { for (int x = 0; x < 16; x++) { @@ -159,26 +196,41 @@ public class SetBlockBufferPacketListener { } } - boolean oldHasBlockEntity = old.hasBlockEntity(); - if (old.is(block)) { - if (blockState.hasBlockEntity()) { - BlockEntity blockEntity = chunk.getBlockEntity(blockPos, LevelChunk.EntityCreationType.CHECK); - if (blockEntity == null) { - blockEntity = ((EntityBlock)block).newBlockEntity(blockPos, blockState); - if (blockEntity != null) { - chunk.addAndRegisterBlockEntity(blockEntity); - } - } else { - blockEntity.setBlockState(blockState); + if (blockState.hasBlockEntity()) { + BlockEntity blockEntity = chunk.getBlockEntity(blockPos, LevelChunk.EntityCreationType.CHECK); - try { - this.updateBlockEntityTicker.invoke(chunk, blockEntity); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } + if (blockEntity == null) { + // There isn't a block entity here, create it! + blockEntity = ((EntityBlock)block).newBlockEntity(blockPos, blockState); + if (blockEntity != null) { + chunk.addAndRegisterBlockEntity(blockEntity); + } + } else if (blockEntity.getType().isValid(blockState)) { + // Block entity is here and the type is correct + blockEntity.setBlockState(blockState); + + try { + this.updateBlockEntityTicker.invoke(chunk, blockEntity); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } else { + // Block entity type isn't correct, we need to recreate it + chunk.removeBlockEntity(blockPos); + + blockEntity = ((EntityBlock)block).newBlockEntity(blockPos, blockState); + if (blockEntity != null) { + chunk.addAndRegisterBlockEntity(blockEntity); } } - } else if (oldHasBlockEntity) { + if (blockEntity != null && blockEntityChunkMap != null) { + int key = x | (y << 4) | (z << 8); + CompressedBlockEntity savedBlockEntity = blockEntityChunkMap.get((short) key); + if (savedBlockEntity != null) { + blockEntity.load(savedBlockEntity.decompress()); + } + } + } else if (old.hasBlockEntity()) { chunk.removeBlockEntity(blockPos); } diff --git a/src/main/java/com/moulberry/axiom/packet/SetBlockPacketListener.java b/src/main/java/com/moulberry/axiom/packet/SetBlockPacketListener.java index 44ae910..f1a5c7c 100644 --- a/src/main/java/com/moulberry/axiom/packet/SetBlockPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/SetBlockPacketListener.java @@ -1,6 +1,7 @@ package com.moulberry.axiom.packet; import com.moulberry.axiom.AxiomPaper; +import com.moulberry.axiom.event.AxiomModifyWorldEvent; import io.netty.buffer.Unpooled; import net.minecraft.core.BlockPos; import net.minecraft.core.SectionPos; @@ -15,6 +16,7 @@ import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.chunk.LevelChunkSection; import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.level.lighting.LightEngine; +import org.bukkit.Bukkit; import org.bukkit.craftbukkit.v1_20_R1.entity.CraftPlayer; import org.bukkit.entity.Player; import org.bukkit.plugin.messaging.PluginMessageListener; @@ -52,6 +54,12 @@ public class SetBlockPacketListener implements PluginMessageListener { return; } + // Check if player is allowed to modify this world + AxiomModifyWorldEvent modifyWorldEvent = new AxiomModifyWorldEvent(bukkitPlayer, bukkitPlayer.getWorld()); + Bukkit.getPluginManager().callEvent(modifyWorldEvent); + if (modifyWorldEvent.isCancelled()) return; + + // Read packet FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); BlockPos blockPos = friendlyByteBuf.readBlockPos(); BlockState blockState = friendlyByteBuf.readById(Block.BLOCK_STATE_REGISTRY); @@ -60,6 +68,7 @@ public class SetBlockPacketListener implements PluginMessageListener { ServerPlayer player = ((CraftPlayer)bukkitPlayer).getHandle(); + // Update blocks if (updateNeighbors) { player.level().setBlock(blockPos, blockState, 3); } else { @@ -111,25 +120,23 @@ public class SetBlockPacketListener implements PluginMessageListener { if (blockEntity != null) { chunk.addAndRegisterBlockEntity(blockEntity); } + } else if (blockEntity.getType().isValid(blockState)) { + // Block entity is here and the type is correct + // Just update the state and ticker and move on + blockEntity.setBlockState(blockState); + + try { + this.updateBlockEntityTicker.invoke(chunk, blockEntity); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } } else { - if (blockEntity.getType().isValid(blockState)) { - // Block entity is here and the type is correct - // Just update the state and ticker and move on - blockEntity.setBlockState(blockState); + // Block entity type isn't correct, we need to recreate it + chunk.removeBlockEntity(blockPos); - try { - this.updateBlockEntityTicker.invoke(chunk, blockEntity); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } - } else { - // Block entity type isn't correct, we need to recreate it - chunk.removeBlockEntity(blockPos); - - blockEntity = ((EntityBlock)block).newBlockEntity(blockPos, blockState); - if (blockEntity != null) { - chunk.addAndRegisterBlockEntity(blockEntity); - } + blockEntity = ((EntityBlock)block).newBlockEntity(blockPos, blockState); + if (blockEntity != null) { + chunk.addAndRegisterBlockEntity(blockEntity); } } } else if (old.hasBlockEntity()) { diff --git a/src/main/java/com/moulberry/axiom/packet/SetEditorViewsPacketListener.java b/src/main/java/com/moulberry/axiom/packet/SetEditorViewsPacketListener.java new file mode 100644 index 0000000..1fd9bda --- /dev/null +++ b/src/main/java/com/moulberry/axiom/packet/SetEditorViewsPacketListener.java @@ -0,0 +1,41 @@ +package com.moulberry.axiom.packet; + +import com.moulberry.axiom.AxiomConstants; +import com.moulberry.axiom.View; +import com.moulberry.axiom.persistence.UUIDDataType; +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import org.bukkit.entity.Player; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.plugin.messaging.PluginMessageListener; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.UUID; + +public class SetEditorViewsPacketListener implements PluginMessageListener { + + @Override + public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, @NotNull byte[] message) { + if (!player.hasPermission("axiom.*")) { + return; + } + + FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); + UUID uuid = friendlyByteBuf.readUUID(); + List views = friendlyByteBuf.readList(View::read); + + PersistentDataContainer container = player.getPersistentDataContainer(); + container.set(AxiomConstants.ACTIVE_VIEW, UUIDDataType.INSTANCE, uuid); + + PersistentDataContainer[] containerArray = new PersistentDataContainer[views.size()]; + for (int i = 0; i < views.size(); i++) { + PersistentDataContainer viewContainer = container.getAdapterContext().newPersistentDataContainer(); + views.get(i).save(viewContainer); + containerArray[i] = viewContainer; + } + container.set(AxiomConstants.VIEWS, PersistentDataType.TAG_CONTAINER_ARRAY, containerArray); + } + +} diff --git a/src/main/java/com/moulberry/axiom/packet/SetFlySpeedPacketListener.java b/src/main/java/com/moulberry/axiom/packet/SetFlySpeedPacketListener.java new file mode 100644 index 0000000..83b5d44 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/packet/SetFlySpeedPacketListener.java @@ -0,0 +1,36 @@ +package com.moulberry.axiom.packet; + +import com.moulberry.axiom.event.AxiomFlySpeedChangeEvent; +import com.moulberry.axiom.event.AxiomGameModeChangeEvent; +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.level.GameType; +import org.bukkit.Bukkit; +import org.bukkit.GameMode; +import org.bukkit.craftbukkit.v1_20_R1.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.bukkit.plugin.messaging.PluginMessageListener; +import org.jetbrains.annotations.NotNull; + +public class SetFlySpeedPacketListener implements PluginMessageListener { + + @Override + public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, @NotNull byte[] message) { + if (!player.hasPermission("axiom.*")) { + return; + } + + + FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); + float flySpeed = friendlyByteBuf.readFloat(); + + // Call event + AxiomFlySpeedChangeEvent flySpeedChangeEvent = new AxiomFlySpeedChangeEvent(player, flySpeed); + Bukkit.getPluginManager().callEvent(flySpeedChangeEvent); + if (flySpeedChangeEvent.isCancelled()) return; + + // Change flying speed + ((CraftPlayer)player).getHandle().getAbilities().setFlyingSpeed(flySpeed); + } + +} diff --git a/src/main/java/com/moulberry/axiom/packet/SetGamemodePacketListener.java b/src/main/java/com/moulberry/axiom/packet/SetGamemodePacketListener.java new file mode 100644 index 0000000..c9c18e3 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/packet/SetGamemodePacketListener.java @@ -0,0 +1,34 @@ +package com.moulberry.axiom.packet; + +import com.moulberry.axiom.event.AxiomGameModeChangeEvent; +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.level.GameType; +import org.bukkit.Bukkit; +import org.bukkit.GameMode; +import org.bukkit.craftbukkit.v1_20_R1.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.bukkit.plugin.messaging.PluginMessageListener; +import org.jetbrains.annotations.NotNull; + +public class SetGamemodePacketListener implements PluginMessageListener { + + @Override + public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, @NotNull byte[] message) { + if (!player.hasPermission("axiom.*")) { + return; + } + + FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); + GameType gameType = GameType.byId(friendlyByteBuf.readByte()); + + // Call event + AxiomGameModeChangeEvent gameModeChangeEvent = new AxiomGameModeChangeEvent(player, GameMode.getByValue(gameType.getId())); + Bukkit.getPluginManager().callEvent(gameModeChangeEvent); + if (gameModeChangeEvent.isCancelled()) return; + + // Change gamemode + ((CraftPlayer)player).getHandle().setGameMode(gameType); + } + +} diff --git a/src/main/java/com/moulberry/axiom/packet/SetHotbarSlotPacketListener.java b/src/main/java/com/moulberry/axiom/packet/SetHotbarSlotPacketListener.java new file mode 100644 index 0000000..a91cd63 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/packet/SetHotbarSlotPacketListener.java @@ -0,0 +1,36 @@ +package com.moulberry.axiom.packet; + +import com.moulberry.axiom.AxiomConstants; +import com.moulberry.axiom.persistence.ItemStackDataType; +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import org.bukkit.NamespacedKey; +import org.bukkit.craftbukkit.v1_20_R1.entity.CraftPlayer; +import org.bukkit.craftbukkit.v1_20_R1.inventory.CraftItemStack; +import org.bukkit.entity.Player; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.plugin.messaging.PluginMessageListener; +import org.jetbrains.annotations.NotNull; + +public class SetHotbarSlotPacketListener implements PluginMessageListener { + + @Override + public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, @NotNull byte[] message) { + if (!player.hasPermission("axiom.*")) { + return; + } + + FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); + int index = friendlyByteBuf.readByte(); + if (index < 0 || index >= 9*9) return; + net.minecraft.world.item.ItemStack nmsStack = friendlyByteBuf.readItem(); + + PersistentDataContainer container = player.getPersistentDataContainer(); + PersistentDataContainer hotbarItems = container.get(AxiomConstants.HOTBAR_DATA, PersistentDataType.TAG_CONTAINER); + if (hotbarItems == null) hotbarItems = container.getAdapterContext().newPersistentDataContainer(); + hotbarItems.set(new NamespacedKey("axiom", "slot_"+index), ItemStackDataType.INSTANCE, CraftItemStack.asCraftMirror(nmsStack)); + container.set(AxiomConstants.HOTBAR_DATA, PersistentDataType.TAG_CONTAINER, hotbarItems); + } + +} diff --git a/src/main/java/com/moulberry/axiom/packet/SwitchActiveHotbarPacketListener.java b/src/main/java/com/moulberry/axiom/packet/SwitchActiveHotbarPacketListener.java new file mode 100644 index 0000000..38badf8 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/packet/SwitchActiveHotbarPacketListener.java @@ -0,0 +1,59 @@ +package com.moulberry.axiom.packet; + +import com.moulberry.axiom.AxiomConstants; +import com.moulberry.axiom.persistence.ItemStackDataType; +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import org.bukkit.GameMode; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.craftbukkit.v1_20_R1.inventory.CraftItemStack; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.plugin.messaging.PluginMessageListener; +import org.jetbrains.annotations.NotNull; + +public class SwitchActiveHotbarPacketListener implements PluginMessageListener { + + @Override + public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, @NotNull byte[] message) { + if (!player.hasPermission("axiom.*")) { + return; + } + + FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); + int oldHotbarIndex = friendlyByteBuf.readByte(); + int activeHotbarIndex = friendlyByteBuf.readByte(); + + ItemStack[] hotbarItems = new ItemStack[9]; + for (int i=0; i<9; i++) { + hotbarItems[i] = CraftItemStack.asCraftMirror(friendlyByteBuf.readItem()); + } + + PersistentDataContainer container = player.getPersistentDataContainer(); + PersistentDataContainer containerHotbarItems = container.get(AxiomConstants.HOTBAR_DATA, PersistentDataType.TAG_CONTAINER); + if (containerHotbarItems == null) containerHotbarItems = container.getAdapterContext().newPersistentDataContainer(); + + for (int i=0; i<9; i++) { + if (oldHotbarIndex != activeHotbarIndex) { + int index = oldHotbarIndex*9 + i; + ItemStack stack = player.getInventory().getItem(i); + if (stack == null) { + stack = new ItemStack(Material.AIR); + } else { + stack = stack.clone(); + } + containerHotbarItems.set(new NamespacedKey("axiom", "slot_"+index), ItemStackDataType.INSTANCE, stack); + } + int index = activeHotbarIndex*9 + i; + containerHotbarItems.set(new NamespacedKey("axiom", "slot_"+index), ItemStackDataType.INSTANCE, hotbarItems[i].clone()); + if (player.getGameMode() == GameMode.CREATIVE) player.getInventory().setItem(i, hotbarItems[i]); + } + + container.set(AxiomConstants.HOTBAR_DATA, PersistentDataType.TAG_CONTAINER, containerHotbarItems); + container.set(AxiomConstants.ACTIVE_HOTBAR_INDEX, PersistentDataType.BYTE, (byte) activeHotbarIndex); + } + +} diff --git a/src/main/java/com/moulberry/axiom/packet/TeleportPacketListener.java b/src/main/java/com/moulberry/axiom/packet/TeleportPacketListener.java new file mode 100644 index 0000000..871cc2a --- /dev/null +++ b/src/main/java/com/moulberry/axiom/packet/TeleportPacketListener.java @@ -0,0 +1,44 @@ +package com.moulberry.axiom.packet; + +import com.moulberry.axiom.event.AxiomGameModeChangeEvent; +import com.moulberry.axiom.event.AxiomTeleportEvent; +import io.netty.buffer.Unpooled; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; +import org.bukkit.*; +import org.bukkit.entity.Player; +import org.bukkit.plugin.messaging.PluginMessageListener; +import org.jetbrains.annotations.NotNull; + +public class TeleportPacketListener implements PluginMessageListener { + + @Override + public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, @NotNull byte[] message) { + if (!player.hasPermission("axiom.*")) { + return; + } + + FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); + ResourceKey resourceKey = friendlyByteBuf.readResourceKey(Registries.DIMENSION); + double x = friendlyByteBuf.readDouble(); + double y = friendlyByteBuf.readDouble(); + double z = friendlyByteBuf.readDouble(); + float yRot = friendlyByteBuf.readFloat(); + float xRot = friendlyByteBuf.readFloat(); + + NamespacedKey namespacedKey = new NamespacedKey(resourceKey.location().getNamespace(), resourceKey.location().getPath()); + World world = Bukkit.getWorld(namespacedKey); + if (world == null) return; + + // Call event + AxiomTeleportEvent teleportEvent = new AxiomTeleportEvent(player, new Location(world, x, y, z, yRot, xRot)); + Bukkit.getPluginManager().callEvent(teleportEvent); + if (teleportEvent.isCancelled()) return; + + // Do teleport + player.teleport(new Location(world, x, y, z, yRot, xRot)); + } + +} diff --git a/src/main/resources/zstd_dictionaries/block_entities_v1.dict b/src/main/resources/zstd_dictionaries/block_entities_v1.dict new file mode 100644 index 0000000..f6d838a Binary files /dev/null and b/src/main/resources/zstd_dictionaries/block_entities_v1.dict differ