From 0b14fddab091f560e193d14ef393034b53044598 Mon Sep 17 00:00:00 2001 From: Moulberry Date: Sun, 2 Jul 2023 18:23:44 +0800 Subject: [PATCH] Initial --- .editorconfig | 8 - build.gradle.kts | 93 +++++---- settings.gradle.kts | 2 +- .../java/com/moulberry/axiom/AxiomPaper.java | 192 ++++++++++++++++++ .../moulberry/axiom/buffer/BlockBuffer.java | 121 +++++++++++ .../axiom/packet/AxiomBigPayloadHandler.java | 65 ++++++ .../packet/SetBlockBufferPacketListener.java | 185 +++++++++++++++++ .../axiom/packet/SetBlockPacketListener.java | 152 ++++++++++++++ .../axiom/persistence/ItemStackDataType.java | 46 +++++ .../testplugin/PluginBrigadierCommand.java | 69 ------- .../paperweight/testplugin/TestPlugin.java | 75 ------- src/main/resources/plugin.yml | 5 +- 12 files changed, 813 insertions(+), 200 deletions(-) delete mode 100644 .editorconfig create mode 100644 src/main/java/com/moulberry/axiom/AxiomPaper.java create mode 100644 src/main/java/com/moulberry/axiom/buffer/BlockBuffer.java create mode 100644 src/main/java/com/moulberry/axiom/packet/AxiomBigPayloadHandler.java create mode 100644 src/main/java/com/moulberry/axiom/packet/SetBlockBufferPacketListener.java create mode 100644 src/main/java/com/moulberry/axiom/packet/SetBlockPacketListener.java create mode 100644 src/main/java/com/moulberry/axiom/persistence/ItemStackDataType.java delete mode 100644 src/main/java/io/papermc/paperweight/testplugin/PluginBrigadierCommand.java delete mode 100644 src/main/java/io/papermc/paperweight/testplugin/TestPlugin.java diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 76e671c..0000000 --- a/.editorconfig +++ /dev/null @@ -1,8 +0,0 @@ -root = true - -[*] -charset = utf-8 -indent_size = 2 -indent_style = space -insert_final_newline = true -max_line_length = off diff --git a/build.gradle.kts b/build.gradle.kts index 5909ff3..468881e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,59 +1,64 @@ plugins { - `java-library` - id("io.papermc.paperweight.userdev") version "1.5.5" - id("xyz.jpenilla.run-paper") version "2.1.0" // Adds runServer and runMojangMappedServer tasks for testing + `java-library` + id("io.papermc.paperweight.userdev") version "1.5.5" + id("xyz.jpenilla.run-paper") version "2.1.0" // Adds runServer and runMojangMappedServer tasks for testing + + // Shades and relocates dependencies into our plugin jar. See https://imperceptiblethoughts.com/shadow/introduction/ + id("com.github.johnrengelman.shadow") version "8.1.1" } -group = "io.papermc.paperweight" +group = "com.moulberry.com.moulberry.axiom" version = "1.0.0-SNAPSHOT" -description = "Test plugin for paperweight-userdev" +description = "Serverside component for Axiom on Paper" java { - // Configure the java toolchain. This allows gradle to auto-provision JDK 17 on systems that only have JDK 8 installed for example. - toolchain.languageVersion.set(JavaLanguageVersion.of(17)) + // Configure the java toolchain. This allows gradle to auto-provision JDK 17 on systems that only have JDK 8 installed for example. + toolchain.languageVersion.set(JavaLanguageVersion.of(17)) +} + +repositories { + maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") + maven("https://jitpack.io") } dependencies { - paperweight.paperDevBundle("1.20.1-R0.1-SNAPSHOT") - // paperweight.foliaDevBundle("1.20.1-R0.1-SNAPSHOT") - // paperweight.devBundle("com.example.paperfork", "1.20.1-R0.1-SNAPSHOT") + paperweight.paperDevBundle("1.20.1-R0.1-SNAPSHOT") + implementation("xyz.jpenilla:reflection-remapper:0.1.0-SNAPSHOT") } tasks { - // Configure reobfJar to run when invoking the build task - assemble { - dependsOn(reobfJar) - } - - compileJava { - options.encoding = Charsets.UTF_8.name() // We want UTF-8 for everything - - // Set the release flag. This configures what version bytecode the compiler will emit, as well as what JDK APIs are usable. - // See https://openjdk.java.net/jeps/247 for more information. - options.release.set(17) - } - javadoc { - options.encoding = Charsets.UTF_8.name() // We want UTF-8 for everything - } - processResources { - filteringCharset = Charsets.UTF_8.name() // We want UTF-8 for everything - val props = mapOf( - "name" to project.name, - "version" to project.version, - "description" to project.description, - "apiVersion" to "1.20" - ) - inputs.properties(props) - filesMatching("plugin.yml") { - expand(props) + // Configure reobfJar to run when invoking the build task + assemble { + dependsOn(reobfJar) } - } - /* - reobfJar { - // This is an example of how you might change the output location for reobfJar. It's recommended not to do this - // for a variety of reasons, however it's asked frequently enough that an example of how to do it is included here. - outputJar.set(layout.buildDirectory.file("libs/PaperweightTestPlugin-${project.version}.jar")) - } - */ + compileJava { + options.encoding = Charsets.UTF_8.name() // We want UTF-8 for everything + + // Set the release flag. This configures what version bytecode the compiler will emit, as well as what JDK APIs are usable. + // See https://openjdk.java.net/jeps/247 for more information. + options.release.set(17) + } + javadoc { + options.encoding = Charsets.UTF_8.name() // We want UTF-8 for everything + } + processResources { + filteringCharset = Charsets.UTF_8.name() // We want UTF-8 for everything + val props = mapOf( + "name" to project.name, + "version" to project.version, + "description" to project.description, + "apiVersion" to "1.20" + ) + inputs.properties(props) + filesMatching("plugin.yml") { + expand(props) + } + } + + shadowJar { + // helper function to relocate a package into our package + fun reloc(pkg: String) = relocate(pkg, "com.moulberry.axiom.dependency.$pkg") + reloc("xyz.jpenilla:reflection-remapper") + } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 14205e4..ca3aa0f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,4 +2,4 @@ plugins { id("org.gradle.toolchains.foojay-resolver-convention") version("0.5.0") } -rootProject.name = "paperweight-test-plugin" +rootProject.name = "AxiomPaper" diff --git a/src/main/java/com/moulberry/axiom/AxiomPaper.java b/src/main/java/com/moulberry/axiom/AxiomPaper.java new file mode 100644 index 0000000..2e0e2ae --- /dev/null +++ b/src/main/java/com/moulberry/axiom/AxiomPaper.java @@ -0,0 +1,192 @@ +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 io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.papermc.paper.network.ChannelInitializeListener; +import io.papermc.paper.network.ChannelInitializeListenerHolder; +import io.papermc.paper.network.ConnectionEvent; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import net.kyori.adventure.key.Key; +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.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +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.event.player.PlayerRegisterChannelEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.plugin.java.JavaPlugin; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Map; + +public class AxiomPaper extends JavaPlugin implements Listener { + + @Override + public void onEnable() { + Bukkit.getPluginManager().registerEvents(this, this); + + Bukkit.getMessenger().registerOutgoingPluginChannel(this, "axiom:enable"); + Bukkit.getMessenger().registerOutgoingPluginChannel(this, "axiom:initialize_hotbars"); + + Bukkit.getMessenger().registerIncomingPluginChannel(this, "axiom:set_gamemode", (channel, player, message) -> { + 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) -> { + 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) -> { + 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) -> { + 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) -> { + 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)); + } + }); + + SetBlockBufferPacketListener setBlockBufferPacketListener = new SetBlockBufferPacketListener(this); + + ChannelInitializeListenerHolder.addListener(Key.key("axiom:handle_big_payload"), new ChannelInitializeListener() { + @Override + public void afterInitChannel(@NonNull Channel channel) { + var packets = ConnectionProtocol.PLAY.getPacketsByIds(PacketFlow.SERVERBOUND); + int payloadId = -1; + for (Map.Entry>> entry : packets.entrySet()) { + if (entry.getValue() == ServerboundCustomPayloadPacket.class) { + payloadId = entry.getKey(); + break; + } + } + if (payloadId < 0) { + throw new RuntimeException("Failed ot find ServerboundCustomPayloadPacket id"); + } + + Connection connection = (Connection) channel.pipeline().get("packet_handler"); + channel.pipeline().addBefore("decoder", "axiom-big-payload-handler", + new AxiomBigPayloadHandler(payloadId, connection, setBlockBufferPacketListener)); + } + }); + + // Bukkit.getMessenger().registerIncomingPluginChannel(this, "axiom:set_block_buffer", new SetBlockBufferPacketListener(this)); + } + + private static final NamespacedKey ACTIVE_HOTBAR_INDEX = new NamespacedKey("axiom", "active_hotbar_index"); + private static final NamespacedKey HOTBAR_DATA = new NamespacedKey("axiom", "hotbar_data"); + + @EventHandler + public void onRegisterChannel(PlayerRegisterChannelEvent event) { + Player player = event.getPlayer(); + String channel = event.getChannel(); + + switch (channel) { + case "axiom:enable" -> { + FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); + friendlyByteBuf.writeBoolean(true); + friendlyByteBuf.writeByte(0); // todo: world properties + player.sendPluginMessage(this, "axiom:enable", friendlyByteBuf.array()); + } + case "axiom: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) { + FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); + friendlyByteBuf.writeByte((byte) activeHotbarIndex); + for (int i=0; i<9*9; i++) { + // Ignore selected hotbar + if (i / 9 == activeHotbarIndex) continue; + + ItemStack stack = hotbarItems.get(new NamespacedKey("axiom", "slot_"+i), ItemStackDataType.INSTANCE); + friendlyByteBuf.writeItem(CraftItemStack.asNMSCopy(stack)); + } + player.sendPluginMessage(this, "axiom:initialize_hotbars", friendlyByteBuf.array()); + } + } + default -> {} + } + } + + + +} diff --git a/src/main/java/com/moulberry/axiom/buffer/BlockBuffer.java b/src/main/java/com/moulberry/axiom/buffer/BlockBuffer.java new file mode 100644 index 0000000..6d402f5 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/buffer/BlockBuffer.java @@ -0,0 +1,121 @@ +package com.moulberry.axiom.buffer; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectSet; +import net.minecraft.core.BlockPos; +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.*; + +public class BlockBuffer { + + public static final BlockState EMPTY_STATE = Blocks.STRUCTURE_VOID.defaultBlockState(); + + private final Long2ObjectMap> values; + + private PalettedContainer last = null; + private long lastId = Long.MAX_VALUE; + private int count; + + public BlockBuffer() { + this.values = new Long2ObjectOpenHashMap<>(); + } + + public BlockBuffer(Long2ObjectMap> values) { + this.values = values; + } + + public int getCount() { + return this.count; + } + + public void clear() { + this.last = null; + this.lastId = Long.MAX_VALUE; + this.values.clear(); + } + + public BlockState get(int x, int y, int z) { + var container = this.getSectionForCoord(x, y, z); + if (container == null) { + return null; + } + + var state = container.get(x & 0xF, y & 0xF, z & 0xF); + if (state == EMPTY_STATE) { + return null; + } else { + return state; + } + } + + 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) { + var container = this.getSectionForCoord(x, y, z); + if (container == null) { + return null; + } + + var state = container.get(x & 0xF, y & 0xF, z & 0xF); + if (state == EMPTY_STATE) { + return null; + } else { + container.set(x & 0xF, y & 0xF, z & 0xF, EMPTY_STATE); + return state; + } + } + + public ObjectSet>> entrySet() { + return this.values.long2ObjectEntrySet(); + } + + public PalettedContainer getSectionForCoord(int x, int y, int z) { + long id = BlockPos.asLong(x >> 4, y >> 4, z >> 4); + + if (id != this.lastId) { + this.lastId = id; + this.last = this.values.get(id); + } + + return this.last; + } + + public PalettedContainer getOrCreateSectionForCoord(int x, int y, int z) { + long id = BlockPos.asLong(x >> 4, y >> 4, z >> 4); + return this.getOrCreateSection(id); + } + + public PalettedContainer getOrCreateSection(long id) { + if (this.last == null || id != this.lastId) { + this.lastId = id; + this.last = this.values.computeIfAbsent(id, k -> new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, + EMPTY_STATE, PalettedContainer.Strategy.SECTION_STATES)); + } + + return this.last; + } + +} diff --git a/src/main/java/com/moulberry/axiom/packet/AxiomBigPayloadHandler.java b/src/main/java/com/moulberry/axiom/packet/AxiomBigPayloadHandler.java new file mode 100644 index 0000000..064d592 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/packet/AxiomBigPayloadHandler.java @@ -0,0 +1,65 @@ +package com.moulberry.axiom.packet; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.papermc.paper.network.ConnectionEvent; +import net.minecraft.network.Connection; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; + +import java.util.List; + +public class AxiomBigPayloadHandler extends ByteToMessageDecoder { + + private static final ResourceLocation SET_BLOCK_BUFFER = new ResourceLocation("axiom", "set_block_buffer"); + private final int payloadId; + private final Connection connection; + private final SetBlockBufferPacketListener listener; + + public AxiomBigPayloadHandler(int payloadId, Connection connection, SetBlockBufferPacketListener listener) { + this.payloadId = payloadId; + this.connection = connection; + this.listener = listener; + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + try { + int i = in.readableBytes(); + if (i != 0) { + int readerIndex = in.readerIndex(); + FriendlyByteBuf buf = new FriendlyByteBuf(in); + int packetId = buf.readVarInt(); + + if (packetId == payloadId) { + ResourceLocation identifier = buf.readResourceLocation(); + if (identifier.equals(SET_BLOCK_BUFFER)) { + ServerPlayer player = connection.getPlayer(); + if (player != null) { + listener.onReceive(player, buf); + return; + } + } + } + in.readerIndex(readerIndex); + } + } catch (Exception e) { + e.printStackTrace(); + } + + ctx.fireChannelRead(in.retain()); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt == ConnectionEvent.COMPRESSION_THRESHOLD_SET || evt == ConnectionEvent.COMPRESSION_DISABLED) { + ctx.channel().pipeline().remove("axiom-big-payload-handler"); + ctx.channel().pipeline().addBefore("decoder", "axiom-big-payload-handler", + new AxiomBigPayloadHandler(payloadId, connection, listener)); + } + super.userEventTriggered(ctx, evt); + } + +} diff --git a/src/main/java/com/moulberry/axiom/packet/SetBlockBufferPacketListener.java b/src/main/java/com/moulberry/axiom/packet/SetBlockBufferPacketListener.java new file mode 100644 index 0000000..7a24ae6 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/packet/SetBlockBufferPacketListener.java @@ -0,0 +1,185 @@ +package com.moulberry.axiom.packet; + +import com.moulberry.axiom.AxiomPaper; +import com.moulberry.axiom.buffer.BlockBuffer; +import io.netty.buffer.Unpooled; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunk; +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.craftbukkit.v1_20_R1.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.bukkit.plugin.messaging.PluginMessageListener; +import org.jetbrains.annotations.NotNull; +import xyz.jpenilla.reflectionremapper.ReflectionRemapper; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Map; + +public class SetBlockBufferPacketListener { + + private final AxiomPaper plugin; + private final Method updateBlockEntityTicker; + + public SetBlockBufferPacketListener(AxiomPaper plugin) { + this.plugin = plugin; + + ReflectionRemapper reflectionRemapper = ReflectionRemapper.forReobfMappingsInPaperJar(); + String methodName = reflectionRemapper.remapMethodName(LevelChunk.class, "updateBlockEntityTicker", BlockEntity.class); + + try { + this.updateBlockEntityTicker = LevelChunk.class.getDeclaredMethod(methodName, BlockEntity.class); + this.updateBlockEntityTicker.setAccessible(true); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + public void onReceive(ServerPlayer player, FriendlyByteBuf friendlyByteBuf) { + ResourceKey worldKey = friendlyByteBuf.readResourceKey(Registries.DIMENSION); + BlockBuffer buffer = new BlockBuffer(); + + while (true) { + long index = friendlyByteBuf.readLong(); + if (index == Long.MAX_VALUE) break; + + PalettedContainer palettedContainer = buffer.getOrCreateSection(index); + palettedContainer.read(friendlyByteBuf); + } + + player.getServer().execute(() -> { + ServerLevel world = player.getServer().getLevel(worldKey); + if (world == null) return; + + BlockPos.MutableBlockPos blockPos = new BlockPos.MutableBlockPos(); + + var lightEngine = world.getChunkSource().getLightEngine(); + + BlockState emptyState = BlockBuffer.EMPTY_STATE; + + for (Long2ObjectMap.Entry> entry : buffer.entrySet()) { + int cx = BlockPos.getX(entry.getLongKey()); + int cy = BlockPos.getY(entry.getLongKey()); + int cz = BlockPos.getZ(entry.getLongKey()); + PalettedContainer container = entry.getValue(); + + if (cy < world.getMinSection() || cy >= world.getMaxSection()) { + continue; + } + + LevelChunk chunk = world.getChunk(cx, cz); + chunk.setUnsaved(true); + + LevelChunkSection section = chunk.getSection(world.getSectionIndexFromSectionY(cy)); + PalettedContainer sectionStates = section.getStates(); + boolean hasOnlyAir = section.hasOnlyAir(); + + Heightmap worldSurface = null; + Heightmap oceanFloor = null; + Heightmap motionBlocking = null; + Heightmap motionBlockingNoLeaves = null; + for (Map.Entry heightmap : chunk.getHeightmaps()) { + switch (heightmap.getKey()) { + case WORLD_SURFACE -> worldSurface = heightmap.getValue(); + case OCEAN_FLOOR -> oceanFloor = heightmap.getValue(); + case MOTION_BLOCKING -> motionBlocking = heightmap.getValue(); + case MOTION_BLOCKING_NO_LEAVES -> motionBlockingNoLeaves = heightmap.getValue(); + default -> {} + } + } + + sectionStates.acquire(); + try { + for (int x = 0; x < 16; x++) { + for (int y = 0; y < 16; y++) { + for (int z = 0; z < 16; z++) { + BlockState blockState = container.get(x, y, z); + if (blockState == emptyState) continue; + + int bx = cx*16 + x; + int by = cy*16 + y; + int bz = cz*16 + z; + + blockPos.set(bx, by, bz); + + if (hasOnlyAir && blockState.isAir()) { + continue; + } + + BlockState old = section.setBlockState(x, y, z, blockState, false); + if (blockState != old) { + Block block = blockState.getBlock(); + motionBlocking.update(x, by, z, blockState); + motionBlockingNoLeaves.update(x, by, z, blockState); + oceanFloor.update(x, by, z, blockState); + worldSurface.update(x, by, z, blockState); + + if (false) { // Full update + old.onRemove(world, blockPos, blockState, false); + + if (sectionStates.get(x, y, z).is(block)) { + blockState.onPlace(world, blockPos, old, false); + } + } + + 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); + + try { + this.updateBlockEntityTicker.invoke(chunk, blockEntity); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + } + } else if (oldHasBlockEntity) { + chunk.removeBlockEntity(blockPos); + } + + world.getChunkSource().blockChanged(blockPos); // todo: maybe simply resend chunk instead of this? + + if (LightEngine.hasDifferentLightProperties(chunk, blockPos, old, blockState)) { + lightEngine.checkBlock(blockPos); + } + } + } + } + } + } finally { + sectionStates.release(); + } + + boolean nowHasOnlyAir = section.hasOnlyAir(); + if (hasOnlyAir != nowHasOnlyAir) { + world.getChunkSource().getLightEngine().updateSectionStatus(SectionPos.of(cx, cy, cz), nowHasOnlyAir); + } + } + }); + } + +} diff --git a/src/main/java/com/moulberry/axiom/packet/SetBlockPacketListener.java b/src/main/java/com/moulberry/axiom/packet/SetBlockPacketListener.java new file mode 100644 index 0000000..bfdaa08 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/packet/SetBlockPacketListener.java @@ -0,0 +1,152 @@ +package com.moulberry.axiom.packet; + +import com.moulberry.axiom.AxiomPaper; +import io.netty.buffer.Unpooled; +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +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.craftbukkit.v1_20_R1.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.bukkit.plugin.messaging.PluginMessageListener; +import org.jetbrains.annotations.NotNull; +import xyz.jpenilla.reflectionremapper.ReflectionRemapper; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.logging.Level; + +public class SetBlockPacketListener implements PluginMessageListener { + + private final AxiomPaper plugin; + private final Method updateBlockEntityTicker; + + public SetBlockPacketListener(AxiomPaper plugin) { + this.plugin = plugin; + + ReflectionRemapper reflectionRemapper = ReflectionRemapper.forReobfMappingsInPaperJar(); + String methodName = reflectionRemapper.remapMethodName(LevelChunk.class, "updateBlockEntityTicker", BlockEntity.class); + + try { + this.updateBlockEntityTicker = LevelChunk.class.getDeclaredMethod(methodName, BlockEntity.class); + this.updateBlockEntityTicker.setAccessible(true); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + @Override + public void onPluginMessageReceived(@NotNull String channel, @NotNull Player bukkitPlayer, @NotNull byte[] message) { + FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); + BlockPos blockPos = friendlyByteBuf.readBlockPos(); + BlockState blockState = friendlyByteBuf.readById(Block.BLOCK_STATE_REGISTRY); + boolean updateNeighbors = friendlyByteBuf.readBoolean(); + int sequenceId = friendlyByteBuf.readInt(); + + ServerPlayer player = ((CraftPlayer)bukkitPlayer).getHandle(); + + if (updateNeighbors) { + player.level().setBlock(blockPos, blockState, 3); + } else { + int bx = blockPos.getX(); + int by = blockPos.getY(); + int bz = blockPos.getZ(); + int x = bx & 0xF; + int y = by & 0xF; + int z = bz & 0xF; + int cx = bx >> 4; + int cy = by >> 4; + int cz = bz >> 4; + + ServerLevel level = player.serverLevel(); + LevelChunk chunk = level.getChunk(cx, cz); + chunk.setUnsaved(true); + + LevelChunkSection section = chunk.getSection(level.getSectionIndexFromSectionY(cy)); + boolean hasOnlyAir = section.hasOnlyAir(); + + Heightmap worldSurface = null; + Heightmap oceanFloor = null; + Heightmap motionBlocking = null; + Heightmap motionBlockingNoLeaves = null; + for (Map.Entry heightmap : chunk.getHeightmaps()) { + switch (heightmap.getKey()) { + case WORLD_SURFACE -> worldSurface = heightmap.getValue(); + case OCEAN_FLOOR -> oceanFloor = heightmap.getValue(); + case MOTION_BLOCKING -> motionBlocking = heightmap.getValue(); + case MOTION_BLOCKING_NO_LEAVES -> motionBlockingNoLeaves = heightmap.getValue(); + default -> {} + } + } + + BlockState old = section.setBlockState(x, y, z, blockState, false); + if (blockState != old) { + Block block = blockState.getBlock(); + motionBlocking.update(x, by, z, blockState); + motionBlockingNoLeaves.update(x, by, z, blockState); + oceanFloor.update(x, by, z, blockState); + worldSurface.update(x, by, z, blockState); + + if (blockState.hasBlockEntity()) { + BlockEntity blockEntity = chunk.getBlockEntity(blockPos, LevelChunk.EntityCreationType.CHECK); + + 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 + // 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 { + // 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 (old.hasBlockEntity()) { + chunk.removeBlockEntity(blockPos); + } + + level.getChunkSource().blockChanged(blockPos); + if (LightEngine.hasDifferentLightProperties(chunk, blockPos, old, blockState)) { + level.getChunkSource().getLightEngine().checkBlock(blockPos); + } + } + + boolean nowHasOnlyAir = section.hasOnlyAir(); + if (hasOnlyAir != nowHasOnlyAir) { + level.getChunkSource().getLightEngine().updateSectionStatus(SectionPos.of(cx, cy, cz), nowHasOnlyAir); + } + } + + if (sequenceId >= 0) { + player.connection.ackBlockChangesUpTo(sequenceId); + } + } + +} diff --git a/src/main/java/com/moulberry/axiom/persistence/ItemStackDataType.java b/src/main/java/com/moulberry/axiom/persistence/ItemStackDataType.java new file mode 100644 index 0000000..bf37e0f --- /dev/null +++ b/src/main/java/com/moulberry/axiom/persistence/ItemStackDataType.java @@ -0,0 +1,46 @@ +package com.moulberry.axiom.persistence; + +import net.minecraft.nbt.CompoundTag; +import org.bukkit.craftbukkit.v1_20_R1.inventory.CraftItemStack; +import org.bukkit.craftbukkit.v1_20_R1.persistence.CraftPersistentDataContainer; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataAdapterContext; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.NotNull; + +public class ItemStackDataType implements PersistentDataType { + public static ItemStackDataType INSTANCE = new ItemStackDataType(); + private ItemStackDataType() { + } + + @Override + public @NotNull Class getPrimitiveType() { + return PersistentDataContainer.class; + } + + @Override + public @NotNull Class getComplexType() { + return ItemStack.class; + } + + @Override + public @NotNull PersistentDataContainer toPrimitive(@NotNull ItemStack complex, @NotNull PersistentDataAdapterContext context) { + net.minecraft.world.item.ItemStack nmsStack = CraftItemStack.asNMSCopy(complex); + if (nmsStack == null) nmsStack = net.minecraft.world.item.ItemStack.EMPTY; + CompoundTag tag = new CompoundTag(); + nmsStack.save(tag); + + PersistentDataContainer container = context.newPersistentDataContainer(); + ((CraftPersistentDataContainer)container).putAll(tag); + return container; + } + + @Override + public @NotNull ItemStack fromPrimitive(@NotNull PersistentDataContainer primitive, @NotNull PersistentDataAdapterContext context) { + CompoundTag tag = ((CraftPersistentDataContainer)primitive).toTagCompound(); + net.minecraft.world.item.ItemStack nmsStack = net.minecraft.world.item.ItemStack.of(tag); + + return CraftItemStack.asCraftMirror(nmsStack); + } +} diff --git a/src/main/java/io/papermc/paperweight/testplugin/PluginBrigadierCommand.java b/src/main/java/io/papermc/paperweight/testplugin/PluginBrigadierCommand.java deleted file mode 100644 index e2288f3..0000000 --- a/src/main/java/io/papermc/paperweight/testplugin/PluginBrigadierCommand.java +++ /dev/null @@ -1,69 +0,0 @@ -package io.papermc.paperweight.testplugin; - -import com.mojang.brigadier.CommandDispatcher; -import com.mojang.brigadier.ParseResults; -import com.mojang.brigadier.StringReader; -import com.mojang.brigadier.builder.LiteralArgumentBuilder; -import java.util.List; -import java.util.function.Consumer; -import com.mojang.brigadier.suggestion.Suggestion; -import net.minecraft.commands.CommandSourceStack; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; -import org.bukkit.command.PluginIdentifiableCommand; -import org.bukkit.craftbukkit.v1_20_R1.CraftServer; -import org.bukkit.craftbukkit.v1_20_R1.command.VanillaCommandWrapper; -import org.bukkit.plugin.Plugin; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.framework.qual.DefaultQualifier; - -@DefaultQualifier(NonNull.class) -final class PluginBrigadierCommand extends Command implements PluginIdentifiableCommand { - private final Consumer> command; - private final Plugin plugin; - - PluginBrigadierCommand( - final Plugin plugin, - final String name, - final Consumer> command - ) { - super(name); - this.plugin = plugin; - this.command = command; - } - - @Override - public boolean execute(final CommandSender sender, final String commandLabel, final String[] args) { - final String joined = String.join(" ", args); - final String argsString = joined.isBlank() ? "" : " " + joined; - ((CraftServer) Bukkit.getServer()).getServer().getCommands().performPrefixedCommand( - VanillaCommandWrapper.getListener(sender), - commandLabel + argsString, - commandLabel - ); - return true; - } - - @Override - public List tabComplete(final CommandSender sender, final String alias, final String[] args, final @Nullable Location location) { - final String joined = String.join(" ", args); - final String argsString = joined.isBlank() ? "" : joined; - final CommandDispatcher dispatcher = ((CraftServer) Bukkit.getServer()).getServer().getCommands().getDispatcher(); - final ParseResults results = dispatcher.parse(new StringReader(alias + " " + argsString), VanillaCommandWrapper.getListener(sender)); - return dispatcher.getCompletionSuggestions(results) - .thenApply(result -> result.getList().stream().map(Suggestion::getText).toList()) - .join(); - } - - @Override - public Plugin getPlugin() { - return this.plugin; - } - - Consumer> command() { - return this.command; - } -} diff --git a/src/main/java/io/papermc/paperweight/testplugin/TestPlugin.java b/src/main/java/io/papermc/paperweight/testplugin/TestPlugin.java deleted file mode 100644 index 6b5dea5..0000000 --- a/src/main/java/io/papermc/paperweight/testplugin/TestPlugin.java +++ /dev/null @@ -1,75 +0,0 @@ -package io.papermc.paperweight.testplugin; - -import com.destroystokyo.paper.brigadier.BukkitBrigadierCommandSource; -import com.destroystokyo.paper.event.brigadier.CommandRegisteredEvent; -import com.mojang.brigadier.Command; -import com.mojang.brigadier.builder.LiteralArgumentBuilder; -import com.mojang.brigadier.tree.LiteralCommandNode; -import java.util.Collection; -import java.util.function.Consumer; -import net.minecraft.ChatFormatting; -import net.minecraft.commands.CommandSourceStack; -import net.minecraft.commands.arguments.EntityArgument; -import net.minecraft.network.chat.ClickEvent; -import net.minecraft.network.chat.Component; -import net.minecraft.server.level.ServerPlayer; -import org.bukkit.craftbukkit.v1_20_R1.CraftServer; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.plugin.java.JavaPlugin; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.framework.qual.DefaultQualifier; - -import static net.kyori.adventure.text.Component.text; -import static net.kyori.adventure.text.format.NamedTextColor.BLUE; -import static net.minecraft.commands.Commands.argument; -import static net.minecraft.commands.Commands.literal; -import static net.minecraft.commands.arguments.EntityArgument.players; - -@DefaultQualifier(NonNull.class) -public final class TestPlugin extends JavaPlugin implements Listener { - @Override - public void onEnable() { - this.getServer().getPluginManager().registerEvents(this, this); - - this.registerPluginBrigadierCommand( - "paperweight", - literal -> literal.requires(stack -> stack.getBukkitSender().hasPermission("paperweight")) - .then(literal("hello") - .executes(ctx -> { - ctx.getSource().getBukkitSender().sendMessage(text("Hello!", BLUE)); - return Command.SINGLE_SUCCESS; - })) - .then(argument("players", players()) - .executes(ctx -> { - final Collection players = EntityArgument.getPlayers(ctx, "players"); - for (final ServerPlayer player : players) { - player.sendSystemMessage( - Component.literal("Hello from Paperweight test plugin!") - .withStyle(ChatFormatting.ITALIC, ChatFormatting.GREEN) - .withStyle(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/paperweight @a"))) - ); - } - return players.size(); - })) - ); - } - - private PluginBrigadierCommand registerPluginBrigadierCommand(final String label, final Consumer> command) { - final PluginBrigadierCommand pluginBrigadierCommand = new PluginBrigadierCommand(this, label, command); - this.getServer().getCommandMap().register(this.getName(), pluginBrigadierCommand); - ((CraftServer) this.getServer()).syncCommands(); - return pluginBrigadierCommand; - } - - @SuppressWarnings({"rawtypes", "unchecked"}) - @EventHandler - public void onCommandRegistered(final CommandRegisteredEvent event) { - if (!(event.getCommand() instanceof PluginBrigadierCommand pluginBrigadierCommand)) { - return; - } - final LiteralArgumentBuilder node = literal(event.getCommandLabel()); - pluginBrigadierCommand.command().accept(node); - event.setLiteral((LiteralCommandNode) node.build()); - } -} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 4c7bf57..c5d9a7e 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,8 +1,7 @@ name: $name version: $version -main: io.papermc.paperweight.testplugin.TestPlugin +main: com.moulberry.axiom.AxiomPaper description: $description -load: STARTUP authors: - - Author + - Moulberry api-version: "$apiVersion"