From b0daeaf6c3d088e797e53ccf4faaea46cf9fbd9a Mon Sep 17 00:00:00 2001 From: Moulberry Date: Thu, 22 Feb 2024 18:18:53 +0800 Subject: [PATCH] Implement Server Blueprints --- .../java/com/moulberry/axiom/AxiomPaper.java | 21 +- .../axiom/blueprint/BlockEntityMap.java | 25 ++ .../axiom/blueprint/BlueprintHeader.java | 65 +++++ .../axiom/blueprint/BlueprintIo.java | 249 ++++++++++++++++++ .../moulberry/axiom/blueprint/DFUHelper.java | 45 ++++ .../axiom/blueprint/RawBlueprint.java | 84 ++++++ .../blueprint/ServerBlueprintManager.java | 107 ++++++++ .../blueprint/ServerBlueprintRegistry.java | 32 +++ .../moulberry/axiom/marker/MarkerData.java | 6 +- .../axiom/packet/AxiomBigPayloadHandler.java | 32 ++- .../BlueprintRequestPacketListener.java | 62 +++++ .../axiom/packet/HelloPacketListener.java | 5 + .../packet/SetBlockBufferPacketListener.java | 6 +- .../packet/UploadBlueprintPacketListener.java | 83 ++++++ src/main/resources/config.yml | 4 + 15 files changed, 808 insertions(+), 18 deletions(-) create mode 100644 src/main/java/com/moulberry/axiom/blueprint/BlockEntityMap.java create mode 100644 src/main/java/com/moulberry/axiom/blueprint/BlueprintHeader.java create mode 100644 src/main/java/com/moulberry/axiom/blueprint/BlueprintIo.java create mode 100644 src/main/java/com/moulberry/axiom/blueprint/DFUHelper.java create mode 100644 src/main/java/com/moulberry/axiom/blueprint/RawBlueprint.java create mode 100644 src/main/java/com/moulberry/axiom/blueprint/ServerBlueprintManager.java create mode 100644 src/main/java/com/moulberry/axiom/blueprint/ServerBlueprintRegistry.java create mode 100644 src/main/java/com/moulberry/axiom/packet/BlueprintRequestPacketListener.java create mode 100644 src/main/java/com/moulberry/axiom/packet/UploadBlueprintPacketListener.java diff --git a/src/main/java/com/moulberry/axiom/AxiomPaper.java b/src/main/java/com/moulberry/axiom/AxiomPaper.java index 401fc30..b5e26ab 100644 --- a/src/main/java/com/moulberry/axiom/AxiomPaper.java +++ b/src/main/java/com/moulberry/axiom/AxiomPaper.java @@ -1,6 +1,7 @@ package com.moulberry.axiom; import com.google.common.util.concurrent.RateLimiter; +import com.moulberry.axiom.blueprint.ServerBlueprintManager; import com.moulberry.axiom.buffer.CompressedBlockEntity; import com.moulberry.axiom.event.AxiomCreateWorldPropertiesEvent; import com.moulberry.axiom.event.AxiomModifyWorldEvent; @@ -35,6 +36,9 @@ import org.bukkit.plugin.messaging.Messenger; import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.Nullable; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -50,6 +54,8 @@ public class AxiomPaper extends JavaPlugin implements Listener { public IdMapper allowedBlockRegistry = null; private boolean logLargeBlockBufferChanges = false; + public Path blueprintFolder = null; + @Override public void onEnable() { PLUGIN = this; @@ -132,9 +138,13 @@ public class AxiomPaper extends JavaPlugin implements Listener { if (configuration.getBoolean("packet-handlers.marker-nbt-request")) { msg.registerIncomingPluginChannel(this, "axiom:marker_nbt_request", new MarkerNbtRequestPacketListener(this)); } + if (configuration.getBoolean("packet-handlers.blueprint-request")) { + msg.registerIncomingPluginChannel(this, "axiom:request_blueprint", new BlueprintRequestPacketListener(this)); + } if (configuration.getBoolean("packet-handlers.set-buffer")) { SetBlockBufferPacketListener setBlockBufferPacketListener = new SetBlockBufferPacketListener(this); + UploadBlueprintPacketListener uploadBlueprintPacketListener = new UploadBlueprintPacketListener(this); ChannelInitializeListenerHolder.addListener(Key.key("axiom:handle_big_payload"), new ChannelInitializeListener() { @Override @@ -153,11 +163,20 @@ public class AxiomPaper extends JavaPlugin implements Listener { Connection connection = (Connection) channel.pipeline().get("packet_handler"); channel.pipeline().addBefore("decoder", "axiom-big-payload-handler", - new AxiomBigPayloadHandler(payloadId, connection, setBlockBufferPacketListener)); + new AxiomBigPayloadHandler(payloadId, connection, setBlockBufferPacketListener, + uploadBlueprintPacketListener)); } }); } + if (this.configuration.getBoolean("blueprint-sharing")) { + this.blueprintFolder = this.getDataFolder().toPath().resolve("blueprints"); + try { + Files.createDirectories(this.blueprintFolder); + } catch (IOException ignored) {} + ServerBlueprintManager.initialize(this.blueprintFolder); + } + Bukkit.getScheduler().scheduleSyncRepeatingTask(this, () -> { HashSet stillActiveAxiomPlayers = new HashSet<>(); diff --git a/src/main/java/com/moulberry/axiom/blueprint/BlockEntityMap.java b/src/main/java/com/moulberry/axiom/blueprint/BlockEntityMap.java new file mode 100644 index 0000000..8e72087 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/blueprint/BlockEntityMap.java @@ -0,0 +1,25 @@ +package com.moulberry.axiom.blueprint; + +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntityType; + +import java.util.HashMap; +import java.util.Map; + +public class BlockEntityMap { + + private static final Map> blockBlockEntityTypeMap = new HashMap<>(); + static { + for (BlockEntityType blockEntityType : BuiltInRegistries.BLOCK_ENTITY_TYPE) { + for (Block validBlock : blockEntityType.validBlocks) { + blockBlockEntityTypeMap.put(validBlock, blockEntityType); + } + } + } + + public static BlockEntityType get(Block block) { + return blockBlockEntityTypeMap.get(block); + } + +} diff --git a/src/main/java/com/moulberry/axiom/blueprint/BlueprintHeader.java b/src/main/java/com/moulberry/axiom/blueprint/BlueprintHeader.java new file mode 100644 index 0000000..cb4e126 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/blueprint/BlueprintHeader.java @@ -0,0 +1,65 @@ +package com.moulberry.axiom.blueprint; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.FriendlyByteBuf; + +import java.util.ArrayList; +import java.util.List; + +public record BlueprintHeader(String name, String author, List tags, float thumbnailYaw, float thumbnailPitch, boolean lockedThumbnail, int blockCount) { + + private static final int CURRENT_VERSION = 0; + + public void write(FriendlyByteBuf friendlyByteBuf) { + friendlyByteBuf.writeUtf(this.name); + friendlyByteBuf.writeUtf(this.author); + friendlyByteBuf.writeCollection(this.tags, FriendlyByteBuf::writeUtf); + friendlyByteBuf.writeInt(this.blockCount); + } + + public static BlueprintHeader read(FriendlyByteBuf friendlyByteBuf) { + String name = friendlyByteBuf.readUtf(); + String author = friendlyByteBuf.readUtf(); + List tags = friendlyByteBuf.readList(FriendlyByteBuf::readUtf); + int blockCount = friendlyByteBuf.readInt(); + return new BlueprintHeader(name, author, tags, 0, 0, true, blockCount); + } + + public static BlueprintHeader load(CompoundTag tag) { + long version = tag.getLong("Version"); + String name = tag.getString("Name"); + String author = tag.getString("Author"); + float thumbnailYaw = tag.contains("ThumbnailYaw", Tag.TAG_FLOAT) ? tag.getFloat("ThumbnailYaw") : 135; + float thumbnailPitch = tag.contains("ThumbnailPitch", Tag.TAG_FLOAT) ? tag.getFloat("ThumbnailPitch") : 30; + boolean lockedThumbnail = tag.getBoolean("LockedThumbnail"); + int blockCount = tag.getInt("BlockCount"); + + List tags = new ArrayList<>(); + for (Tag string : tag.getList("Tags", Tag.TAG_STRING)) { + tags.add(string.getAsString()); + } + + return new BlueprintHeader(name, author, tags, thumbnailYaw, thumbnailPitch, lockedThumbnail, blockCount); + } + + public CompoundTag save(CompoundTag tag) { + ListTag listTag = new ListTag(); + for (String string : this.tags) { + listTag.add(StringTag.valueOf(string)); + } + + tag.putLong("Version", CURRENT_VERSION); + tag.putString("Name", this.name); + tag.putString("Author", this.author); + tag.put("Tags", listTag); + tag.putFloat("ThumbnailYaw", this.thumbnailYaw); + tag.putFloat("ThumbnailPitch", this.thumbnailPitch); + tag.putBoolean("LockedThumbnail", this.lockedThumbnail); + tag.putInt("BlockCount", this.blockCount); + return tag; + } + +} diff --git a/src/main/java/com/moulberry/axiom/blueprint/BlueprintIo.java b/src/main/java/com/moulberry/axiom/blueprint/BlueprintIo.java new file mode 100644 index 0000000..0a160f7 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/blueprint/BlueprintIo.java @@ -0,0 +1,249 @@ +package com.moulberry.axiom.blueprint; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.Dynamic; +import com.moulberry.axiom.buffer.CompressedBlockEntity; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.*; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.util.datafix.fixes.References; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.PalettedContainer; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; + +public class BlueprintIo { + + private static final int MAGIC = 0xAE5BB36; + + private static final IOException NOT_VALID_BLUEPRINT = new IOException("Not a valid Blueprint"); + public static BlueprintHeader readHeader(InputStream inputStream) throws IOException { + if (inputStream.available() < 4) throw NOT_VALID_BLUEPRINT; + DataInputStream dataInputStream = new DataInputStream(inputStream); + + int magic = dataInputStream.readInt(); + if (magic != MAGIC) throw NOT_VALID_BLUEPRINT; + + dataInputStream.readInt(); // Ignore header length + CompoundTag headerTag = NbtIo.read(dataInputStream); + return BlueprintHeader.load(headerTag); + } + + public static RawBlueprint readRawBlueprint(InputStream inputStream) throws IOException { + if (inputStream.available() < 4) throw NOT_VALID_BLUEPRINT; + DataInputStream dataInputStream = new DataInputStream(inputStream); + + int magic = dataInputStream.readInt(); + if (magic != MAGIC) throw NOT_VALID_BLUEPRINT; + + // Header + dataInputStream.readInt(); // Ignore header length + CompoundTag headerTag = NbtIo.read(dataInputStream); + BlueprintHeader header = BlueprintHeader.load(headerTag); + + // Thumbnail + int thumbnailLength = dataInputStream.readInt(); + byte[] thumbnailBytes = dataInputStream.readNBytes(thumbnailLength); + if (thumbnailBytes.length < thumbnailLength) throw NOT_VALID_BLUEPRINT; + + int currentDataVersion = SharedConstants.getCurrentVersion().getDataVersion().getVersion(); + + // Block data + dataInputStream.readInt(); // Ignore block data length + CompoundTag blockDataTag = NbtIo.readCompressed(dataInputStream); + int blueprintDataVersion = blockDataTag.getInt("DataVersion"); + if (blueprintDataVersion == 0) blueprintDataVersion = currentDataVersion; + + ListTag listTag = blockDataTag.getList("BlockRegion", Tag.TAG_COMPOUND); + Long2ObjectMap> blockMap = readBlocks(listTag, blueprintDataVersion); + + // Block Entities + ListTag blockEntitiesTag = blockDataTag.getList("BlockEntities", Tag.TAG_COMPOUND); + Long2ObjectMap blockEntities = new Long2ObjectOpenHashMap<>(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + for (Tag tag : blockEntitiesTag) { + CompoundTag blockEntityCompound = (CompoundTag) tag; + + // Data Fix + if (blueprintDataVersion != currentDataVersion) { + Dynamic dynamic = new Dynamic<>(NbtOps.INSTANCE, blockEntityCompound); + Dynamic output = DataFixers.getDataFixer().update(References.BLOCK_ENTITY, dynamic, + blueprintDataVersion, currentDataVersion); + blockEntityCompound = (CompoundTag) output.getValue(); + } + + BlockPos blockPos = BlockEntity.getPosFromTag(blockEntityCompound); + long pos = blockPos.asLong(); + + String id = blockEntityCompound.getString("id"); + BlockEntityType type = BuiltInRegistries.BLOCK_ENTITY_TYPE.get(new ResourceLocation(id)); + + if (type != null) { + PalettedContainer container = blockMap.get(BlockPos.asLong( + blockPos.getX() >> 4, + blockPos.getY() >> 4, + blockPos.getZ() >> 4 + )); + + BlockState blockState = container.get(blockPos.getX() & 0xF, blockPos.getY() & 0xF, blockPos.getZ() & 0xF); + if (type.isValid(blockState)) { + CompoundTag newTag = blockEntityCompound.copy(); + newTag.remove("x"); + newTag.remove("y"); + newTag.remove("z"); + newTag.remove("id"); + CompressedBlockEntity compressedBlockEntity = CompressedBlockEntity.compress(newTag, baos); + blockEntities.put(pos, compressedBlockEntity); + } + } + } + + return new RawBlueprint(header, thumbnailBytes, blockMap, blockEntities); + } + + public static final Codec> BLOCK_STATE_CODEC = PalettedContainer.codecRW(Block.BLOCK_STATE_REGISTRY, BlockState.CODEC, + PalettedContainer.Strategy.SECTION_STATES, Blocks.STRUCTURE_VOID.defaultBlockState()); + + public static Long2ObjectMap> readBlocks(ListTag list, int dataVersion) { + Long2ObjectMap> map = new Long2ObjectOpenHashMap<>(); + + for (Tag tag : list) { + if (tag instanceof CompoundTag compoundTag) { + int cx = compoundTag.getInt("X"); + int cy = compoundTag.getInt("Y"); + int cz = compoundTag.getInt("Z"); + + CompoundTag blockStates = compoundTag.getCompound("BlockStates"); + blockStates = DFUHelper.updatePalettedContainer(blockStates, dataVersion); + PalettedContainer container = BLOCK_STATE_CODEC.parse(NbtOps.INSTANCE, blockStates) + .getOrThrow(false, err -> {}); + map.put(BlockPos.asLong(cx, cy, cz), container); + } + } + + return map; + } + + public static void writeHeader(Path inPath, Path outPath, BlueprintHeader newHeader) throws IOException { + byte[] thumbnailAndBlockBytes; + try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(inPath))) { + if (inputStream.available() < 4) throw NOT_VALID_BLUEPRINT; + DataInputStream dataInputStream = new DataInputStream(inputStream); + + int magic = dataInputStream.readInt(); + if (magic != MAGIC) throw NOT_VALID_BLUEPRINT; + + // Header + int headerLength = dataInputStream.readInt(); // Ignore header length + if (dataInputStream.skip(headerLength) < headerLength) throw NOT_VALID_BLUEPRINT; + + thumbnailAndBlockBytes = dataInputStream.readAllBytes(); + } + + try (OutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(outPath))) { + DataOutputStream dataOutputStream = new DataOutputStream(outputStream); + + dataOutputStream.writeInt(MAGIC); + + // Write header + CompoundTag headerTag = newHeader.save(new CompoundTag()); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (DataOutputStream os = new DataOutputStream(baos)) { + NbtIo.write(headerTag, os); + } + dataOutputStream.writeInt(baos.size()); + baos.writeTo(dataOutputStream); + + // Copy remaining bytes + dataOutputStream.write(thumbnailAndBlockBytes); + } + } + + public static void writeRaw(OutputStream outputStream, RawBlueprint rawBlueprint) throws IOException { + DataOutputStream dataOutputStream = new DataOutputStream(outputStream); + + dataOutputStream.writeInt(MAGIC); + + // Write header + CompoundTag headerTag = rawBlueprint.header().save(new CompoundTag()); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (DataOutputStream os = new DataOutputStream(baos)) { + NbtIo.write(headerTag, os); + } + dataOutputStream.writeInt(baos.size()); + baos.writeTo(dataOutputStream); + + // Write thumbnail + dataOutputStream.writeInt(rawBlueprint.thumbnail().length); + dataOutputStream.write(rawBlueprint.thumbnail()); + + // Write block data + CompoundTag compound = new CompoundTag(); + ListTag savedBlockRegions = new ListTag(); + + for (Long2ObjectMap.Entry> entry : rawBlueprint.blocks().long2ObjectEntrySet()) { + long pos = entry.getLongKey(); + PalettedContainer container = entry.getValue(); + + int cx = BlockPos.getX(pos); + int cy = BlockPos.getY(pos); + int cz = BlockPos.getZ(pos); + + CompoundTag tag = new CompoundTag(); + tag.putInt("X", cx); + tag.putInt("Y", cy); + tag.putInt("Z", cz); + Tag encoded = BlueprintIo.BLOCK_STATE_CODEC.encodeStart(NbtOps.INSTANCE, container) + .getOrThrow(false, err -> {}); + tag.put("BlockStates", encoded); + savedBlockRegions.add(tag); + } + + compound.putInt("DataVersion", SharedConstants.getCurrentVersion().getDataVersion().getVersion()); + compound.put("BlockRegion", savedBlockRegions); + + // Write Block Entities + ListTag blockEntitiesTag = new ListTag(); + rawBlueprint.blockEntities().forEach((pos, compressedBlockEntity) -> { + int x = BlockPos.getX(pos); + int y = BlockPos.getY(pos); + int z = BlockPos.getZ(pos); + + PalettedContainer container = rawBlueprint.blocks().get(BlockPos.asLong(x >> 4,y >> 4, z >> 4)); + BlockState blockState = container.get(x & 0xF, y & 0xF, z & 0xF); + + BlockEntityType type = BlockEntityMap.get(blockState.getBlock()); + if (type == null) return; + + ResourceLocation resourceLocation = BlockEntityType.getKey(type); + + if (resourceLocation != null) { + CompoundTag tag = compressedBlockEntity.decompress(); + tag.putInt("x", x); + tag.putInt("y", y); + tag.putInt("z", z); + tag.putString("id", resourceLocation.toString()); + blockEntitiesTag.add(tag); + } + }); + compound.put("BlockEntities", blockEntitiesTag); + + baos.reset(); + NbtIo.writeCompressed(compound, baos); + dataOutputStream.writeInt(baos.size()); + baos.writeTo(dataOutputStream); + } + +} diff --git a/src/main/java/com/moulberry/axiom/blueprint/DFUHelper.java b/src/main/java/com/moulberry/axiom/blueprint/DFUHelper.java new file mode 100644 index 0000000..6f2561e --- /dev/null +++ b/src/main/java/com/moulberry/axiom/blueprint/DFUHelper.java @@ -0,0 +1,45 @@ +package com.moulberry.axiom.blueprint; + +import com.mojang.serialization.Dynamic; +import net.minecraft.SharedConstants; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.Tag; +import net.minecraft.util.datafix.DataFixers; +import net.minecraft.util.datafix.fixes.References; + +public class DFUHelper { + + private static final int DATA_VERSION = SharedConstants.getCurrentVersion().getDataVersion().getVersion(); + + public static CompoundTag updatePalettedContainer(CompoundTag tag, int fromVersion) { + if (!hasExpectedPaletteTag(tag)) { + return tag; + } + + if (fromVersion == DATA_VERSION) return tag; + + tag = tag.copy(); + + ListTag newPalette = new ListTag(); + for (Tag entry : tag.getList("palette", Tag.TAG_COMPOUND)) { + Dynamic dynamic = new Dynamic<>(NbtOps.INSTANCE, entry); + Dynamic output = DataFixers.getDataFixer().update(References.BLOCK_STATE, dynamic, fromVersion, DATA_VERSION); + newPalette.add(output.getValue()); + } + + tag.put("palette", newPalette); + return tag; + } + + private static boolean hasExpectedPaletteTag(CompoundTag tag) { + if (!tag.contains("palette", Tag.TAG_LIST)) return false; + + ListTag listTag = (ListTag) tag.get("palette"); + if (listTag == null) return false; + + return listTag.isEmpty() || listTag.getElementType() == Tag.TAG_COMPOUND; + } + +} diff --git a/src/main/java/com/moulberry/axiom/blueprint/RawBlueprint.java b/src/main/java/com/moulberry/axiom/blueprint/RawBlueprint.java new file mode 100644 index 0000000..8093f3d --- /dev/null +++ b/src/main/java/com/moulberry/axiom/blueprint/RawBlueprint.java @@ -0,0 +1,84 @@ +package com.moulberry.axiom.blueprint; + +import com.moulberry.axiom.buffer.CompressedBlockEntity; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.longs.LongSet; +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; + +public record RawBlueprint(BlueprintHeader header, byte[] thumbnail, Long2ObjectMap> blocks, + Long2ObjectMap blockEntities) { + + public static void writeHeader(FriendlyByteBuf friendlyByteBuf, RawBlueprint rawBlueprint) { + rawBlueprint.header.write(friendlyByteBuf); + friendlyByteBuf.writeByteArray(rawBlueprint.thumbnail); + } + + public static RawBlueprint readHeader(FriendlyByteBuf friendlyByteBuf) { + BlueprintHeader header = BlueprintHeader.read(friendlyByteBuf); + byte[] thumbnail = friendlyByteBuf.readByteArray(); + + return new RawBlueprint(header, thumbnail, null, null); + } + + public static void write(FriendlyByteBuf friendlyByteBuf, RawBlueprint rawBlueprint) { + rawBlueprint.header.write(friendlyByteBuf); + friendlyByteBuf.writeByteArray(rawBlueprint.thumbnail); + + LongSet chunkKeys = rawBlueprint.blocks.keySet(); + friendlyByteBuf.writeVarInt(chunkKeys.size()); + + LongIterator longIterator = chunkKeys.longIterator(); + while (longIterator.hasNext()) { + long pos = longIterator.nextLong(); + friendlyByteBuf.writeLong(pos); + rawBlueprint.blocks.get(pos).write(friendlyByteBuf); + } + + LongSet blockEntityKeys = rawBlueprint.blockEntities.keySet(); + friendlyByteBuf.writeVarInt(blockEntityKeys.size()); + + longIterator = blockEntityKeys.longIterator(); + while (longIterator.hasNext()) { + long pos = longIterator.nextLong(); + friendlyByteBuf.writeLong(pos); + rawBlueprint.blockEntities.get(pos).write(friendlyByteBuf); + } + } + + public static RawBlueprint read(FriendlyByteBuf friendlyByteBuf) { + BlueprintHeader header = BlueprintHeader.read(friendlyByteBuf); + byte[] thumbnail = friendlyByteBuf.readByteArray(); + + Long2ObjectMap> blocks = new Long2ObjectOpenHashMap<>(); + + int chunkCount = friendlyByteBuf.readVarInt(); + for (int i = 0; i < chunkCount; i++) { + long pos = friendlyByteBuf.readLong(); + + PalettedContainer palettedContainer = new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, + Blocks.STRUCTURE_VOID.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES); + palettedContainer.read(friendlyByteBuf); + + blocks.put(pos, palettedContainer); + } + + Long2ObjectMap blockEntities = new Long2ObjectOpenHashMap<>(); + + int blockEntityCount = friendlyByteBuf.readVarInt(); + for (int i = 0; i < blockEntityCount; i++) { + long pos = friendlyByteBuf.readLong(); + + CompressedBlockEntity compressedBlockEntity = CompressedBlockEntity.read(friendlyByteBuf); + blockEntities.put(pos, compressedBlockEntity); + } + + return new RawBlueprint(header, thumbnail, blocks, blockEntities); + } + +} diff --git a/src/main/java/com/moulberry/axiom/blueprint/ServerBlueprintManager.java b/src/main/java/com/moulberry/axiom/blueprint/ServerBlueprintManager.java new file mode 100644 index 0000000..dd61107 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/blueprint/ServerBlueprintManager.java @@ -0,0 +1,107 @@ +package com.moulberry.axiom.blueprint; + +import com.moulberry.axiom.AxiomPaper; +import com.moulberry.axiom.packet.CustomByteArrayPayload; +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import org.bukkit.craftbukkit.v1_20_R1.entity.CraftPlayer; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; + +public class ServerBlueprintManager { + + private static ServerBlueprintRegistry registry = null; + + public static void initialize(Path blueprintDirectory) { + Map map = new HashMap<>(); + loadRegistryFromFolder(map, blueprintDirectory, "/"); + registry = new ServerBlueprintRegistry(map); + } + + private static final int MAX_SIZE = 1000000; + private static final ResourceLocation PACKET_BLUEPRINT_MANIFEST_IDENTIFIER = new ResourceLocation("axiom:blueprint_manifest"); + + public static void sendManifest(List serverPlayers) { + if (registry != null) { + List sendTo = new ArrayList<>(); + + for (ServerPlayer serverPlayer : serverPlayers) { + CraftPlayer craftPlayer = serverPlayer.getBukkitEntity(); + if (AxiomPaper.PLUGIN.canUseAxiom(craftPlayer) && + craftPlayer.getListeningPluginChannels().contains("axiom:blueprint_manifest")) { + sendTo.add(serverPlayer); + } + } + + if (sendTo.isEmpty()) return; + + FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + buf.writeBoolean(true); // replace + + for (Map.Entry entry : registry.blueprints().entrySet()) { + buf.writeUtf(entry.getKey()); + RawBlueprint.writeHeader(buf, entry.getValue()); + + if (buf.writerIndex() > MAX_SIZE) { + // Finish and send current packet + buf.writeUtf(""); + byte[] bytes = new byte[buf.writerIndex()]; + buf.getBytes(0, bytes); + var payload = new CustomByteArrayPayload(PACKET_BLUEPRINT_MANIFEST_IDENTIFIER, bytes); + for (ServerPlayer serverPlayer : sendTo) { + serverPlayer.connection.send(new ClientboundCustomPayloadPacket(payload)); + } + + // Continue + buf = new FriendlyByteBuf(Unpooled.buffer()); + buf.writeBoolean(false); // don't replace + } + } + + buf.writeUtf(""); + byte[] bytes = new byte[buf.writerIndex()]; + buf.getBytes(0, bytes); + var payload = new CustomByteArrayPayload(PACKET_BLUEPRINT_MANIFEST_IDENTIFIER, bytes); + for (ServerPlayer serverPlayer : sendTo) { + serverPlayer.connection.send(new ClientboundCustomPayloadPacket(payload)); + } + } + } + + public static ServerBlueprintRegistry getRegistry() { + return registry; + } + + private static void loadRegistryFromFolder(Map map, Path folder, String location) { + if (!Files.isDirectory(folder)) { + return; + } + + try (DirectoryStream directoryStream = Files.newDirectoryStream(folder)) { + for (Path path : directoryStream) { + String filename = path.getFileName().toString(); + if (filename.endsWith(".bp")) { + try { + RawBlueprint rawBlueprint = BlueprintIo.readRawBlueprint(new BufferedInputStream(Files.newInputStream(path))); + String newLocation = location + filename.substring(0, filename.length()-3); + map.put(newLocation, rawBlueprint); + } catch (Exception e) { + e.printStackTrace(); + } + } else if (Files.isDirectory(path)) { + String newLocation = location + filename + "/"; + loadRegistryFromFolder(map, path, newLocation); + } + } + } catch (IOException ignored) {} + } + +} diff --git a/src/main/java/com/moulberry/axiom/blueprint/ServerBlueprintRegistry.java b/src/main/java/com/moulberry/axiom/blueprint/ServerBlueprintRegistry.java new file mode 100644 index 0000000..ecfd63f --- /dev/null +++ b/src/main/java/com/moulberry/axiom/blueprint/ServerBlueprintRegistry.java @@ -0,0 +1,32 @@ +package com.moulberry.axiom.blueprint; + +import net.minecraft.network.FriendlyByteBuf; + +import java.util.HashMap; +import java.util.Map; + +public record ServerBlueprintRegistry(Map blueprints) { + + public void writeManifest(FriendlyByteBuf friendlyByteBuf) { + for (Map.Entry entry : this.blueprints.entrySet()) { + friendlyByteBuf.writeUtf(entry.getKey()); + RawBlueprint.writeHeader(friendlyByteBuf, entry.getValue()); + } + friendlyByteBuf.writeUtf(""); + } + + public static ServerBlueprintRegistry readManifest(FriendlyByteBuf friendlyByteBuf) { + Map blueprints = new HashMap<>(); + + while (true) { + String path = friendlyByteBuf.readUtf(); + if (path.isEmpty()) { + return new ServerBlueprintRegistry(blueprints); + } + + blueprints.put(path, RawBlueprint.readHeader(friendlyByteBuf)); + } + + } + +} diff --git a/src/main/java/com/moulberry/axiom/marker/MarkerData.java b/src/main/java/com/moulberry/axiom/marker/MarkerData.java index 93d8db6..b31b3e4 100644 --- a/src/main/java/com/moulberry/axiom/marker/MarkerData.java +++ b/src/main/java/com/moulberry/axiom/marker/MarkerData.java @@ -19,7 +19,7 @@ public record MarkerData(UUID uuid, Vec3 position, @Nullable String name, @Nulla int lineArgb, float lineThickness, int faceArgb) { public static MarkerData read(FriendlyByteBuf friendlyByteBuf) { UUID uuid = friendlyByteBuf.readUUID(); - Vec3 position = friendlyByteBuf.readVec3(); + Vec3 position = new Vec3(friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble()); String name = friendlyByteBuf.readNullable(FriendlyByteBuf::readUtf); Vec3 minRegion = null; @@ -31,8 +31,8 @@ public record MarkerData(UUID uuid, Vec3 position, @Nullable String name, @Nulla byte flags = friendlyByteBuf.readByte(); if (flags != 0) { - minRegion = friendlyByteBuf.readVec3(); - maxRegion = friendlyByteBuf.readVec3(); + minRegion = new Vec3(friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble()); + maxRegion = new Vec3(friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble()); if ((flags & 2) != 0) { lineArgb = friendlyByteBuf.readInt(); diff --git a/src/main/java/com/moulberry/axiom/packet/AxiomBigPayloadHandler.java b/src/main/java/com/moulberry/axiom/packet/AxiomBigPayloadHandler.java index 0b8761f..8a36a8a 100644 --- a/src/main/java/com/moulberry/axiom/packet/AxiomBigPayloadHandler.java +++ b/src/main/java/com/moulberry/axiom/packet/AxiomBigPayloadHandler.java @@ -1,5 +1,6 @@ package com.moulberry.axiom.packet; +import com.moulberry.axiom.AxiomPaper; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; @@ -14,14 +15,18 @@ import java.util.List; public class AxiomBigPayloadHandler extends ByteToMessageDecoder { private static final ResourceLocation SET_BUFFER = new ResourceLocation("axiom", "set_buffer"); + private static final ResourceLocation UPLOAD_BLUEPRINT = new ResourceLocation("axiom", "upload_blueprint"); private final int payloadId; private final Connection connection; - private final SetBlockBufferPacketListener listener; + private final SetBlockBufferPacketListener setBlockBuffer; + private final UploadBlueprintPacketListener uploadBlueprint; - public AxiomBigPayloadHandler(int payloadId, Connection connection, SetBlockBufferPacketListener listener) { + public AxiomBigPayloadHandler(int payloadId, Connection connection, SetBlockBufferPacketListener setBlockBuffer, + UploadBlueprintPacketListener uploadBlueprint) { this.payloadId = payloadId; this.connection = connection; - this.listener = listener; + this.setBlockBuffer = setBlockBuffer; + this.uploadBlueprint = uploadBlueprint; } @Override @@ -44,12 +49,19 @@ public class AxiomBigPayloadHandler extends ByteToMessageDecoder { ResourceLocation identifier = buf.readResourceLocation(); if (identifier.equals(SET_BUFFER)) { ServerPlayer player = connection.getPlayer(); - if (player != null && player.getBukkitEntity().hasPermission("axiom.*")) { - if (listener.onReceive(player, buf)) { - success = true; - in.skipBytes(in.readableBytes()); - return; - } + if (AxiomPaper.PLUGIN.canUseAxiom(player.getBukkitEntity())) { + setBlockBuffer.onReceive(player, buf); + success = true; + in.skipBytes(in.readableBytes()); + return; + } + } else if (identifier.equals(UPLOAD_BLUEPRINT)) { + ServerPlayer player = connection.getPlayer(); + if (AxiomPaper.PLUGIN.canUseAxiom(player.getBukkitEntity())) { + uploadBlueprint.onReceive(player, buf); + success = true; + in.skipBytes(in.readableBytes()); + return; } } } @@ -74,7 +86,7 @@ public class AxiomBigPayloadHandler extends ByteToMessageDecoder { 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)); + new AxiomBigPayloadHandler(payloadId, connection, setBlockBuffer, uploadBlueprint)); } super.userEventTriggered(ctx, evt); } diff --git a/src/main/java/com/moulberry/axiom/packet/BlueprintRequestPacketListener.java b/src/main/java/com/moulberry/axiom/packet/BlueprintRequestPacketListener.java new file mode 100644 index 0000000..9012b6a --- /dev/null +++ b/src/main/java/com/moulberry/axiom/packet/BlueprintRequestPacketListener.java @@ -0,0 +1,62 @@ +package com.moulberry.axiom.packet; + +import com.moulberry.axiom.AxiomPaper; +import com.moulberry.axiom.blueprint.RawBlueprint; +import com.moulberry.axiom.blueprint.ServerBlueprintManager; +import com.moulberry.axiom.blueprint.ServerBlueprintRegistry; +import com.moulberry.axiom.marker.MarkerData; +import io.netty.buffer.Unpooled; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.Marker; +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; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public class BlueprintRequestPacketListener implements PluginMessageListener { + + private final AxiomPaper plugin; + public BlueprintRequestPacketListener(AxiomPaper plugin) { + this.plugin = plugin; + } + + private static final ResourceLocation RESPONSE_PACKET_IDENTIFIER = new ResourceLocation("axiom:response_blueprint"); + + @Override + public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, @NotNull byte[] message) { + if (!this.plugin.canUseAxiom(player)) { + return; + } + + FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); + String path = friendlyByteBuf.readUtf(); + + ServerBlueprintRegistry registry = ServerBlueprintManager.getRegistry(); + if (registry == null) { + return; + } + + RawBlueprint rawBlueprint = registry.blueprints().get(path); + if (rawBlueprint != null) { + FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + + buf.writeUtf(path); + RawBlueprint.write(buf, rawBlueprint); + + byte[] bytes = new byte[buf.writerIndex()]; + buf.getBytes(0, bytes); + var payload = new CustomByteArrayPayload(RESPONSE_PACKET_IDENTIFIER, bytes); + ((CraftPlayer)player).getHandle().connection.send(new ClientboundCustomPayloadPacket(payload)); + } + } + +} diff --git a/src/main/java/com/moulberry/axiom/packet/HelloPacketListener.java b/src/main/java/com/moulberry/axiom/packet/HelloPacketListener.java index 3dfbacd..1e16f0b 100644 --- a/src/main/java/com/moulberry/axiom/packet/HelloPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/HelloPacketListener.java @@ -5,6 +5,7 @@ import com.moulberry.axiom.AxiomConstants; import com.moulberry.axiom.AxiomPaper; import com.moulberry.axiom.View; import com.moulberry.axiom.WorldExtension; +import com.moulberry.axiom.blueprint.ServerBlueprintManager; import com.moulberry.axiom.event.AxiomHandshakeEvent; import com.moulberry.axiom.persistence.ItemStackDataType; import com.moulberry.axiom.persistence.UUIDDataType; @@ -17,6 +18,7 @@ import net.minecraft.network.FriendlyByteBuf; import org.bukkit.Bukkit; import org.bukkit.NamespacedKey; import org.bukkit.World; +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.inventory.ItemStack; @@ -25,6 +27,7 @@ import org.bukkit.persistence.PersistentDataType; import org.bukkit.plugin.messaging.PluginMessageListener; import org.jetbrains.annotations.NotNull; +import java.util.List; import java.util.Set; import java.util.UUID; @@ -170,6 +173,8 @@ public class HelloPacketListener implements PluginMessageListener { } WorldExtension.onPlayerJoin(world, player); + + ServerBlueprintManager.sendManifest(List.of(((CraftPlayer)player).getHandle())); } } diff --git a/src/main/java/com/moulberry/axiom/packet/SetBlockBufferPacketListener.java b/src/main/java/com/moulberry/axiom/packet/SetBlockBufferPacketListener.java index 6e33f29..a15c22d 100644 --- a/src/main/java/com/moulberry/axiom/packet/SetBlockBufferPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/SetBlockBufferPacketListener.java @@ -68,9 +68,9 @@ public class SetBlockBufferPacketListener { } } - public boolean onReceive(ServerPlayer player, FriendlyByteBuf friendlyByteBuf) { + public void onReceive(ServerPlayer player, FriendlyByteBuf friendlyByteBuf) { MinecraftServer server = player.getServer(); - if (server == null) return false; + if (server == null) return; ResourceKey worldKey = friendlyByteBuf.readResourceKey(Registries.DIMENSION); friendlyByteBuf.readUUID(); // Discard, we don't need to associate buffers @@ -116,8 +116,6 @@ public class SetBlockBufferPacketListener { } else { throw new RuntimeException("Unknown buffer type: " + type); } - - return true; } private void applyBlockBuffer(ServerPlayer player, MinecraftServer server, BlockBuffer buffer, ResourceKey worldKey) { diff --git a/src/main/java/com/moulberry/axiom/packet/UploadBlueprintPacketListener.java b/src/main/java/com/moulberry/axiom/packet/UploadBlueprintPacketListener.java new file mode 100644 index 0000000..7ac878d --- /dev/null +++ b/src/main/java/com/moulberry/axiom/packet/UploadBlueprintPacketListener.java @@ -0,0 +1,83 @@ +package com.moulberry.axiom.packet; + +import com.moulberry.axiom.AxiomPaper; +import com.moulberry.axiom.blueprint.BlueprintIo; +import com.moulberry.axiom.blueprint.RawBlueprint; +import com.moulberry.axiom.blueprint.ServerBlueprintManager; +import com.moulberry.axiom.blueprint.ServerBlueprintRegistry; +import com.moulberry.axiom.marker.MarkerData; +import io.netty.buffer.Unpooled; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.Marker; +import org.bukkit.craftbukkit.v1_20_R1.CraftServer; +import org.bukkit.craftbukkit.v1_20_R1.CraftWorld; +import org.bukkit.entity.Player; +import org.bukkit.plugin.messaging.PluginMessageListener; +import org.jetbrains.annotations.NotNull; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.UUID; + +public class UploadBlueprintPacketListener { + + private final AxiomPaper plugin; + public UploadBlueprintPacketListener(AxiomPaper plugin) { + this.plugin = plugin; + } + + public void onReceive(ServerPlayer serverPlayer, FriendlyByteBuf friendlyByteBuf) { + if (!this.plugin.canUseAxiom(serverPlayer.getBukkitEntity())) { + return; + } + + ServerBlueprintRegistry registry = ServerBlueprintManager.getRegistry(); + if (registry == null || this.plugin.blueprintFolder == null) { + return; + } + + String pathStr = friendlyByteBuf.readUtf(); + RawBlueprint rawBlueprint = RawBlueprint.read(friendlyByteBuf); + + pathStr = pathStr.replace("\\", "/"); + + if (!pathStr.endsWith(".bp") || pathStr.contains("..") || !pathStr.startsWith("/")) { + return; + } + + pathStr = pathStr.substring(1); + + Path relative = Path.of(pathStr).normalize(); + if (relative.isAbsolute()) { + return; + } + + Path path = this.plugin.blueprintFolder.resolve(relative); + + // Write file + try { + Files.createDirectories(path.getParent()); + } catch (IOException e) { + return; + } + try (OutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(path))) { + BlueprintIo.writeRaw(outputStream, rawBlueprint); + } catch (IOException e) { + return; + } + + // Update registry + registry.blueprints().put("/" + pathStr.substring(0, pathStr.length()-3), rawBlueprint); + + // Resend manifest + ServerBlueprintManager.sendManifest(serverPlayer.getServer().getPlayerList().getPlayers()); + } + +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index f5c0b38..1c97379 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -11,6 +11,9 @@ max-chunk-load-distance: 128 # Whether players are allowed to teleport between worlds using views allow-teleport-between-worlds: true +# Whether to allow clients to save/load/share blueprints through the server +blueprint-sharing: false + # Action to take when a user with an incompatible Minecraft version or Axiom version joins # Valid actions are 'kick', 'warn' and 'ignore' # 'warn' will give the player a warning and disable Axiom @@ -76,3 +79,4 @@ packet-handlers: manipulate-entity: true delete-entity: true marker-nbt-request: true + blueprint-request: true