diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index eb74ffb..9b655ce 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ bom-newest = "1.37" cloud-paper = "2.0.0-20240516.054251-69" coreprotect = "22.4" paper = "1.20.2-R0.1-SNAPSHOT" -plotsquared = "7.3.9-20240513.192211-13" +plotsquared = "7.3.8" reflection-remapper = "0.1.2-20240315.033304-2" viaversion-api = "5.0.1" worldguard-bukkit = "7.0.9-SNAPSHOT" diff --git a/src/main/java/com/moulberry/axiom/AxiomPaper.java b/src/main/java/com/moulberry/axiom/AxiomPaper.java index 455d493..9f65715 100644 --- a/src/main/java/com/moulberry/axiom/AxiomPaper.java +++ b/src/main/java/com/moulberry/axiom/AxiomPaper.java @@ -9,25 +9,7 @@ import com.moulberry.axiom.event.AxiomModifyWorldEvent; import com.moulberry.axiom.integration.coreprotect.CoreProtectIntegration; import com.moulberry.axiom.integration.plotsquared.PlotSquaredIntegration; import com.moulberry.axiom.packet.*; -import com.moulberry.axiom.packet.impl.BlueprintRequestPacketListener; -import com.moulberry.axiom.packet.impl.DeleteEntityPacketListener; -import com.moulberry.axiom.packet.impl.HelloPacketListener; -import com.moulberry.axiom.packet.impl.ManipulateEntityPacketListener; -import com.moulberry.axiom.packet.impl.MarkerNbtRequestPacketListener; -import com.moulberry.axiom.packet.impl.RequestChunkDataPacketListener; -import com.moulberry.axiom.packet.impl.SetBlockBufferPacketListener; -import com.moulberry.axiom.packet.impl.SetBlockPacketListener; -import com.moulberry.axiom.packet.impl.SetEditorViewsPacketListener; -import com.moulberry.axiom.packet.impl.SetFlySpeedPacketListener; -import com.moulberry.axiom.packet.impl.SetGamemodePacketListener; -import com.moulberry.axiom.packet.impl.SetHotbarSlotPacketListener; -import com.moulberry.axiom.packet.impl.SetTimePacketListener; -import com.moulberry.axiom.packet.impl.SetWorldPropertyListener; -import com.moulberry.axiom.packet.impl.SpawnEntityPacketListener; -import com.moulberry.axiom.packet.impl.SwitchActiveHotbarPacketListener; -import com.moulberry.axiom.packet.impl.TeleportPacketListener; -import com.moulberry.axiom.packet.impl.UpdateAnnotationPacketListener; -import com.moulberry.axiom.packet.impl.UploadBlueprintPacketListener; +import com.moulberry.axiom.packet.impl.*; import com.moulberry.axiom.world_properties.server.ServerWorldPropertiesRegistry; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; @@ -44,6 +26,7 @@ import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.PacketFlow; import net.minecraft.network.protocol.common.ServerboundCustomPayloadPacket; import net.minecraft.server.MinecraftServer; +import net.minecraft.world.entity.EntityType; import net.minecraft.world.level.block.state.BlockState; import org.bukkit.*; import org.bukkit.command.CommandSender; @@ -66,6 +49,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.IntFunction; public class AxiomPaper extends JavaPlugin implements Listener { @@ -80,6 +64,9 @@ public class AxiomPaper extends JavaPlugin implements Listener { public IdMapper allowedBlockRegistry = null; private boolean logLargeBlockBufferChanges = false; + private int packetCollectionReadLimit = 1024; + private Set> whitelistedEntities = new HashSet<>(); + private Set> blacklistedEntities = new HashSet<>(); public Path blueprintFolder = null; public boolean allowAnnotations = false; @@ -99,9 +86,21 @@ public class AxiomPaper extends JavaPlugin implements Listener { this.getLogger().warning("Invalid value for unsupported-axiom-version, expected 'kick', 'warn' or 'ignore'"); } - boolean allowLargeChunkDataRequest = this.configuration.getBoolean("allow-large-chunk-data-request"); this.logLargeBlockBufferChanges = this.configuration.getBoolean("log-large-block-buffer-changes"); + if (this.configuration.getBoolean("allow-large-payload-for-all-packets")) { + packetCollectionReadLimit = Short.MAX_VALUE; + } + + this.whitelistedEntities.clear(); + this.blacklistedEntities.clear(); + for (String whitelistedEntity : this.configuration.getStringList("whitelist-entities")) { + EntityType.byString(whitelistedEntity).ifPresent(this.whitelistedEntities::add); + } + for (String blacklistedEntity : this.configuration.getStringList("blacklist-entities")) { + EntityType.byString(blacklistedEntity).ifPresent(this.blacklistedEntities::add); + } + List disallowedBlocks = this.configuration.getStringList("disallowed-blocks"); this.allowedBlockRegistry = DisallowedBlocks.createAllowedBlockRegistry(disallowedBlocks); @@ -130,27 +129,29 @@ public class AxiomPaper extends JavaPlugin implements Listener { Map largePayloadHandlers = new HashMap<>(); - registerPacketHandler("hello", new HelloPacketListener(this), msg, largePayloadHandlers); - registerPacketHandler("set_gamemode", new SetGamemodePacketListener(this), msg, largePayloadHandlers); - registerPacketHandler("set_fly_speed", new SetFlySpeedPacketListener(this), msg, largePayloadHandlers); - registerPacketHandler("set_world_time", new SetTimePacketListener(this), msg, largePayloadHandlers); - registerPacketHandler("set_world_property", new SetWorldPropertyListener(this), msg, largePayloadHandlers); - registerPacketHandler("set_block", new SetBlockPacketListener(this), msg, largePayloadHandlers); // set-single-block - registerPacketHandler("set_hotbar_slot", new SetHotbarSlotPacketListener(this), msg, largePayloadHandlers); - registerPacketHandler("switch_active_hotbar", new SwitchActiveHotbarPacketListener(this), msg, largePayloadHandlers); - registerPacketHandler("teleport", new TeleportPacketListener(this), msg, largePayloadHandlers); - registerPacketHandler("set_editor_views", new SetEditorViewsPacketListener(this), msg, largePayloadHandlers); - registerPacketHandler("request_chunk_data", new RequestChunkDataPacketListener(this, - !configuration.getBoolean("packet-handlers.request-chunk-data")), msg, largePayloadHandlers); - registerPacketHandler("spawn_entity", new SpawnEntityPacketListener(this), msg, largePayloadHandlers); - registerPacketHandler("manipulate_entity", new ManipulateEntityPacketListener(this), msg, largePayloadHandlers); - registerPacketHandler("delete_entity", new DeleteEntityPacketListener(this), msg, largePayloadHandlers); - registerPacketHandler("marker_nbt_request", new MarkerNbtRequestPacketListener(this), msg, largePayloadHandlers); - registerPacketHandler("request_blueprint", new BlueprintRequestPacketListener(this), msg, largePayloadHandlers); + registerPacketHandler("hello", new HelloPacketListener(this), msg, LargePayloadBehaviour.FORCE_SMALL, largePayloadHandlers); + registerPacketHandler("set_gamemode", new SetGamemodePacketListener(this), msg, LargePayloadBehaviour.DEFAULT, largePayloadHandlers); + registerPacketHandler("set_fly_speed", new SetFlySpeedPacketListener(this), msg, LargePayloadBehaviour.DEFAULT, largePayloadHandlers); + registerPacketHandler("set_world_time", new SetTimePacketListener(this), msg, LargePayloadBehaviour.DEFAULT, largePayloadHandlers); + registerPacketHandler("set_world_property", new SetWorldPropertyListener(this), msg, LargePayloadBehaviour.DEFAULT, largePayloadHandlers); + registerPacketHandler("set_block", new SetBlockPacketListener(this), msg, LargePayloadBehaviour.DEFAULT, largePayloadHandlers); // set-single-block + registerPacketHandler("set_hotbar_slot", new SetHotbarSlotPacketListener(this), msg, LargePayloadBehaviour.DEFAULT, largePayloadHandlers); + registerPacketHandler("switch_active_hotbar", new SwitchActiveHotbarPacketListener(this), msg, LargePayloadBehaviour.DEFAULT, largePayloadHandlers); + registerPacketHandler("teleport", new TeleportPacketListener(this), msg, LargePayloadBehaviour.DEFAULT, largePayloadHandlers); + registerPacketHandler("set_editor_views", new SetEditorViewsPacketListener(this), msg, LargePayloadBehaviour.DEFAULT, largePayloadHandlers); + registerPacketHandler("request_chunk_data", new RequestChunkDataPacketListener(this, !configuration.getBoolean("packet-handlers.request-chunk-data")), msg, + this.configuration.getBoolean("allow-large-chunk-data-request") ? LargePayloadBehaviour.FORCE_LARGE : LargePayloadBehaviour.DEFAULT, largePayloadHandlers); + registerPacketHandler("request_entity_data", new RequestEntityDataPacketListener(this, !configuration.getBoolean("packet-handlers.request-entity-data")), msg, + this.configuration.getBoolean("allow-large-chunk-data-request") ? LargePayloadBehaviour.FORCE_LARGE : LargePayloadBehaviour.DEFAULT, largePayloadHandlers); + registerPacketHandler("spawn_entity", new SpawnEntityPacketListener(this), msg, LargePayloadBehaviour.DEFAULT, largePayloadHandlers); + registerPacketHandler("manipulate_entity", new ManipulateEntityPacketListener(this), msg, LargePayloadBehaviour.DEFAULT, largePayloadHandlers); + registerPacketHandler("delete_entity", new DeleteEntityPacketListener(this), msg, LargePayloadBehaviour.DEFAULT, largePayloadHandlers); + registerPacketHandler("marker_nbt_request", new MarkerNbtRequestPacketListener(this), msg, LargePayloadBehaviour.DEFAULT, largePayloadHandlers); + registerPacketHandler("request_blueprint", new BlueprintRequestPacketListener(this), msg, LargePayloadBehaviour.DEFAULT, largePayloadHandlers); - registerPacketHandler("set_buffer", new SetBlockBufferPacketListener(this), msg, largePayloadHandlers); - registerPacketHandler("upload_blueprint", new UploadBlueprintPacketListener(this), msg, largePayloadHandlers); - registerPacketHandler("annotation_update", new UpdateAnnotationPacketListener(this), msg, largePayloadHandlers); + registerPacketHandler("set_buffer", new SetBlockBufferPacketListener(this), msg, LargePayloadBehaviour.FORCE_LARGE, largePayloadHandlers); + registerPacketHandler("upload_blueprint", new UploadBlueprintPacketListener(this), msg, LargePayloadBehaviour.FORCE_LARGE, largePayloadHandlers); + registerPacketHandler("annotation_update", new UpdateAnnotationPacketListener(this), msg, LargePayloadBehaviour.FORCE_LARGE, largePayloadHandlers); if (!largePayloadHandlers.isEmpty()) { ChannelInitializeListenerHolder.addListener(Key.key("axiom:handle_big_payload"), new ChannelInitializeListener() { @@ -300,25 +301,26 @@ public class AxiomPaper extends JavaPlugin implements Listener { } } - private void registerPacketHandler(String name, PacketHandler handler, Messenger messenger, Map largePayloadHandlers) { + private enum LargePayloadBehaviour { + DEFAULT, + FORCE_LARGE, + FORCE_SMALL + } + + private void registerPacketHandler(String name, PacketHandler handler, Messenger messenger, LargePayloadBehaviour behaviour, + Map largePayloadHandlers) { String configEntry = "packet-handlers." + name.replace("_", "-"); if (name.equals("set_block")) { configEntry = "packet-handlers.set-single-block"; } else if (name.equals("request_blueprint")) { configEntry = "packet-handlers.blueprint-request"; } - if (name.equals("request-chunk-data") || this.configuration.getBoolean(configEntry, true)) { - boolean isLargePayload = false; - - if (name.equals("hello")) { // Hello must use normal system, as non-Axiom players can't send large payloads - isLargePayload = false; - } else if (this.configuration.getBoolean("allow-large-payload-for-all-packets")) { - isLargePayload = true; - } else if (name.equals("set_buffer") || name.equals("upload_blueprint") || name.equals("annotation_update")) { - isLargePayload = true; - } else if (name.equals("request_chunk_data") && this.configuration.getBoolean("allow-large-chunk-data-request")) { - isLargePayload = true; - } + if (name.equals("request_chunk_data") || name.equals("request_entity_data") || this.configuration.getBoolean(configEntry, true)) { + boolean isLargePayload = switch (behaviour) { + case DEFAULT -> this.configuration.getBoolean("allow-large-payload-for-all-packets"); + case FORCE_LARGE -> true; + case FORCE_SMALL -> false; + }; if (isLargePayload) { largePayloadHandlers.put("axiom:"+name, handler); @@ -348,6 +350,10 @@ public class AxiomPaper extends JavaPlugin implements Listener { return allowedCapabilities; } + public IntFunction limitCollection(IntFunction applier) { + return FriendlyByteBuf.limitValue(applier, this.packetCollectionReadLimit); + } + public boolean logLargeBlockBufferChanges() { return this.logLargeBlockBufferChanges; } @@ -377,6 +383,19 @@ public class AxiomPaper extends JavaPlugin implements Listener { return activeAxiomPlayers.contains(player.getUniqueId()) && hasAxiomPermission(player, permission, strict); } + public boolean canEntityBeManipulated(EntityType entityType) { + if (entityType == EntityType.PLAYER) { + return false; + } + if (!this.whitelistedEntities.isEmpty() && !this.whitelistedEntities.contains(entityType)) { + return false; + } + if (this.blacklistedEntities.contains(entityType)) { + return false; + } + return true; + } + public @Nullable RateLimiter getBlockBufferRateLimiter(UUID uuid) { return this.playerBlockBufferRateLimiters.get(uuid); } diff --git a/src/main/java/com/moulberry/axiom/NbtSanitization.java b/src/main/java/com/moulberry/axiom/NbtSanitization.java index c24c4df..bc9d1d7 100644 --- a/src/main/java/com/moulberry/axiom/NbtSanitization.java +++ b/src/main/java/com/moulberry/axiom/NbtSanitization.java @@ -21,6 +21,15 @@ public class NbtSanitization { "Glowing", "Tags", "Passengers", + // armor stand + "ArmorItems", + "HandItems", + "Small", + "ShowArms", + "DisabledSlots", + "NoBasePlate", + "Marker", + "Pose", // marker "data", // display entity @@ -50,6 +59,10 @@ public class NbtSanitization { ); public static void sanitizeEntity(CompoundTag entityRoot) { + if (AxiomPaper.PLUGIN.configuration.getBoolean("disable-entity-sanitization")) { + return; + } + entityRoot.getAllKeys().retainAll(ALLOWED_KEYS); if (entityRoot.contains("Passengers", Tag.TAG_LIST)) { diff --git a/src/main/java/com/moulberry/axiom/blueprint/BlueprintHeader.java b/src/main/java/com/moulberry/axiom/blueprint/BlueprintHeader.java index 48936a7..e68d802 100644 --- a/src/main/java/com/moulberry/axiom/blueprint/BlueprintHeader.java +++ b/src/main/java/com/moulberry/axiom/blueprint/BlueprintHeader.java @@ -9,24 +9,25 @@ 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) { +public record BlueprintHeader(String name, String author, List tags, float thumbnailYaw, float thumbnailPitch, boolean lockedThumbnail, int blockCount, boolean containsAir) { - private static final int CURRENT_VERSION = 0; + private static final int CURRENT_VERSION = 1; public void write(FriendlyByteBuf friendlyByteBuf) { friendlyByteBuf.writeUtf(this.name); friendlyByteBuf.writeUtf(this.author); friendlyByteBuf.writeCollection(this.tags, FriendlyByteBuf::writeUtf); friendlyByteBuf.writeInt(this.blockCount); + friendlyByteBuf.writeBoolean(this.containsAir); } public static BlueprintHeader read(FriendlyByteBuf friendlyByteBuf) { String name = friendlyByteBuf.readUtf(); String author = friendlyByteBuf.readUtf(); - List tags = friendlyByteBuf.readCollection(FriendlyByteBuf.limitValue(ArrayList::new, 1000), - FriendlyByteBuf::readUtf); + List tags = friendlyByteBuf.readList(FriendlyByteBuf::readUtf); int blockCount = friendlyByteBuf.readInt(); - return new BlueprintHeader(name, author, tags, 0, 0, true, blockCount); + boolean containsAir = friendlyByteBuf.readBoolean(); + return new BlueprintHeader(name, author, tags, 0, 0, true, blockCount, containsAir); } public static BlueprintHeader load(CompoundTag tag) { @@ -37,13 +38,14 @@ public record BlueprintHeader(String name, String author, List tags, flo float thumbnailPitch = tag.contains("ThumbnailPitch", Tag.TAG_FLOAT) ? tag.getFloat("ThumbnailPitch") : 30; boolean lockedThumbnail = tag.getBoolean("LockedThumbnail"); int blockCount = tag.getInt("BlockCount"); + boolean containsAir = tag.getBoolean("ContainsAir"); 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); + return new BlueprintHeader(name, author, tags, thumbnailYaw, thumbnailPitch, lockedThumbnail, blockCount, containsAir); } public CompoundTag save(CompoundTag tag) { @@ -60,6 +62,7 @@ public record BlueprintHeader(String name, String author, List tags, flo tag.putFloat("ThumbnailPitch", this.thumbnailPitch); tag.putBoolean("LockedThumbnail", this.lockedThumbnail); tag.putInt("BlockCount", this.blockCount); + tag.putBoolean("ContainsAir", this.containsAir); return tag; } diff --git a/src/main/java/com/moulberry/axiom/blueprint/BlueprintIo.java b/src/main/java/com/moulberry/axiom/blueprint/BlueprintIo.java index d6352c4..b057e4a 100644 --- a/src/main/java/com/moulberry/axiom/blueprint/BlueprintIo.java +++ b/src/main/java/com/moulberry/axiom/blueprint/BlueprintIo.java @@ -23,6 +23,8 @@ import net.minecraft.world.level.chunk.PalettedContainer; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; public class BlueprintIo { @@ -111,7 +113,23 @@ public class BlueprintIo { } } - return new RawBlueprint(header, thumbnailBytes, blockMap, blockEntities); + ListTag entitiesTag = blockDataTag.getList("Entities", Tag.TAG_COMPOUND); + List entities = new ArrayList<>(); + for (Tag tag : entitiesTag) { + CompoundTag entityCompound = (CompoundTag) tag; + + // Data Fix + if (blueprintDataVersion != currentDataVersion) { + Dynamic dynamic = new Dynamic<>(NbtOps.INSTANCE, entityCompound); + Dynamic output = DataFixers.getDataFixer().update(References.ENTITY, dynamic, + blueprintDataVersion, currentDataVersion); + entityCompound = (CompoundTag) output.getValue(); + } + + entities.add(entityCompound); + } + + return new RawBlueprint(header, thumbnailBytes, blockMap, blockEntities, entities); } public static final Codec> BLOCK_STATE_CODEC = PalettedContainer.codecRW(Block.BLOCK_STATE_REGISTRY, BlockState.CODEC, @@ -137,41 +155,6 @@ public class BlueprintIo { 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); @@ -241,6 +224,11 @@ public class BlueprintIo { }); compound.put("BlockEntities", blockEntitiesTag); + // Write entities + ListTag entitiesTag = new ListTag(); + entitiesTag.addAll(rawBlueprint.entities()); + compound.put("Entities", entitiesTag); + baos.reset(); NbtIo.writeCompressed(compound, baos); dataOutputStream.writeInt(baos.size()); diff --git a/src/main/java/com/moulberry/axiom/blueprint/RawBlueprint.java b/src/main/java/com/moulberry/axiom/blueprint/RawBlueprint.java index 8093f3d..6033500 100644 --- a/src/main/java/com/moulberry/axiom/blueprint/RawBlueprint.java +++ b/src/main/java/com/moulberry/axiom/blueprint/RawBlueprint.java @@ -5,14 +5,18 @@ 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.nbt.CompoundTag; 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 java.util.ArrayList; +import java.util.List; + public record RawBlueprint(BlueprintHeader header, byte[] thumbnail, Long2ObjectMap> blocks, - Long2ObjectMap blockEntities) { + Long2ObjectMap blockEntities, List entities) { public static void writeHeader(FriendlyByteBuf friendlyByteBuf, RawBlueprint rawBlueprint) { rawBlueprint.header.write(friendlyByteBuf); @@ -23,7 +27,7 @@ public record RawBlueprint(BlueprintHeader header, byte[] thumbnail, Long2Object BlueprintHeader header = BlueprintHeader.read(friendlyByteBuf); byte[] thumbnail = friendlyByteBuf.readByteArray(); - return new RawBlueprint(header, thumbnail, null, null); + return new RawBlueprint(header, thumbnail, null, null, null); } public static void write(FriendlyByteBuf friendlyByteBuf, RawBlueprint rawBlueprint) { @@ -49,6 +53,11 @@ public record RawBlueprint(BlueprintHeader header, byte[] thumbnail, Long2Object friendlyByteBuf.writeLong(pos); rawBlueprint.blockEntities.get(pos).write(friendlyByteBuf); } + + friendlyByteBuf.writeVarInt(rawBlueprint.entities.size()); + for (CompoundTag entity : rawBlueprint.entities) { + friendlyByteBuf.writeNbt(entity); + } } public static RawBlueprint read(FriendlyByteBuf friendlyByteBuf) { @@ -62,7 +71,7 @@ public record RawBlueprint(BlueprintHeader header, byte[] thumbnail, Long2Object long pos = friendlyByteBuf.readLong(); PalettedContainer palettedContainer = new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, - Blocks.STRUCTURE_VOID.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES); + Blocks.STRUCTURE_VOID.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES); palettedContainer.read(friendlyByteBuf); blocks.put(pos, palettedContainer); @@ -78,7 +87,14 @@ public record RawBlueprint(BlueprintHeader header, byte[] thumbnail, Long2Object blockEntities.put(pos, compressedBlockEntity); } - return new RawBlueprint(header, thumbnail, blocks, blockEntities); + List entities = new ArrayList<>(); + + int entityCount = friendlyByteBuf.readVarInt(); + for (int i = 0; i < entityCount; i++) { + entities.add(friendlyByteBuf.readNbt()); + } + + return new RawBlueprint(header, thumbnail, blocks, blockEntities, entities); } } diff --git a/src/main/java/com/moulberry/axiom/packet/impl/DeleteEntityPacketListener.java b/src/main/java/com/moulberry/axiom/packet/impl/DeleteEntityPacketListener.java index 89bc0ab..77bfedc 100644 --- a/src/main/java/com/moulberry/axiom/packet/impl/DeleteEntityPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/impl/DeleteEntityPacketListener.java @@ -35,22 +35,17 @@ public class DeleteEntityPacketListener implements PacketHandler { return; } - List delete = friendlyByteBuf.readCollection(FriendlyByteBuf.limitValue(ArrayList::new, 1000), - FriendlyByteBuf::readUUID); + List delete = friendlyByteBuf.readCollection(this.plugin.limitCollection(ArrayList::new), FriendlyByteBuf::readUUID); ServerLevel serverLevel = ((CraftWorld)player.getWorld()).getHandle(); - List whitelistedEntities = this.plugin.configuration.getStringList("whitelist-entities"); - List blacklistedEntities = this.plugin.configuration.getStringList("blacklist-entities"); - for (UUID uuid : delete) { Entity entity = serverLevel.getEntity(uuid); if (entity == null || entity instanceof net.minecraft.world.entity.player.Player || entity.hasPassenger(e -> e instanceof net.minecraft.world.entity.player.Player)) continue; - String type = EntityType.getKey(entity.getType()).toString(); - - if (!whitelistedEntities.isEmpty() && !whitelistedEntities.contains(type)) continue; - if (blacklistedEntities.contains(type)) continue; + if (!this.plugin.canEntityBeManipulated(entity.getType())) { + continue; + } if (!Integration.canBreakBlock(player, player.getWorld().getBlockAt(entity.getBlockX(), entity.getBlockY(), entity.getBlockZ()))) { diff --git a/src/main/java/com/moulberry/axiom/packet/impl/HelloPacketListener.java b/src/main/java/com/moulberry/axiom/packet/impl/HelloPacketListener.java index db0fb0a..26b2c91 100644 --- a/src/main/java/com/moulberry/axiom/packet/impl/HelloPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/impl/HelloPacketListener.java @@ -136,6 +136,9 @@ public class HelloPacketListener implements PacketHandler { buf.writeVarInt(5); // Maximum Reach buf.writeVarInt(16); // Max editor views buf.writeBoolean(true); // Editable Views + buf.writeVarInt(0); // No custom data overrides + buf.writeVarInt(0); // No rotation overrides + buf.writeVarInt(1); // Blueprint version byte[] enableBytes = new byte[buf.writerIndex()]; buf.getBytes(0, enableBytes); diff --git a/src/main/java/com/moulberry/axiom/packet/impl/ManipulateEntityPacketListener.java b/src/main/java/com/moulberry/axiom/packet/impl/ManipulateEntityPacketListener.java index 2a43829..b6f4f3c 100644 --- a/src/main/java/com/moulberry/axiom/packet/impl/ManipulateEntityPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/impl/ManipulateEntityPacketListener.java @@ -49,7 +49,7 @@ public class ManipulateEntityPacketListener implements PacketHandler { public record ManipulateEntry(UUID uuid, @Nullable Set relativeMovementSet, @Nullable Vec3 position, float yaw, float pitch, CompoundTag merge, PassengerManipulation passengerManipulation, List passengers) { - public static ManipulateEntry read(FriendlyByteBuf friendlyByteBuf, Player player) { + public static ManipulateEntry read(FriendlyByteBuf friendlyByteBuf, Player player, AxiomPaper plugin) { UUID uuid = friendlyByteBuf.readUUID(); int flags = friendlyByteBuf.readByte(); @@ -69,8 +69,7 @@ public class ManipulateEntityPacketListener implements PacketHandler { PassengerManipulation passengerManipulation = friendlyByteBuf.readEnum(PassengerManipulation.class); List passengers = List.of(); if (passengerManipulation == PassengerManipulation.ADD_LIST || passengerManipulation == PassengerManipulation.REMOVE_LIST) { - passengers = friendlyByteBuf.readCollection(FriendlyByteBuf.limitValue(ArrayList::new, 1000), - FriendlyByteBuf::readUUID); + passengers = friendlyByteBuf.readCollection(plugin.limitCollection(ArrayList::new), FriendlyByteBuf::readUUID); } return new ManipulateEntry(uuid, relativeMovementSet, position, yaw, pitch, nbt, @@ -90,22 +89,18 @@ public class ManipulateEntityPacketListener implements PacketHandler { return; } - List entries = friendlyByteBuf.readCollection(FriendlyByteBuf.limitValue(ArrayList::new, 1000), - buf -> ManipulateEntry.read(buf, player)); + List entries = friendlyByteBuf.readCollection(this.plugin.limitCollection(ArrayList::new), + buf -> ManipulateEntry.read(buf, player, this.plugin)); ServerLevel serverLevel = ((CraftWorld)player.getWorld()).getHandle(); - List whitelistedEntities = this.plugin.configuration.getStringList("whitelist-entities"); - List blacklistedEntities = this.plugin.configuration.getStringList("blacklist-entities"); - for (ManipulateEntry entry : entries) { Entity entity = serverLevel.getEntity(entry.uuid); if (entity == null || entity instanceof net.minecraft.world.entity.player.Player || entity.hasPassenger(ManipulateEntityPacketListener::isPlayer)) continue; - String type = EntityType.getKey(entity.getType()).toString(); - - if (!whitelistedEntities.isEmpty() && !whitelistedEntities.contains(type)) continue; - if (blacklistedEntities.contains(type)) continue; + if (!this.plugin.canEntityBeManipulated(entity.getType())) { + continue; + } Vec3 position = entity.position(); BlockPos containing = BlockPos.containing(position.x, position.y, position.z); @@ -168,10 +163,9 @@ public class ManipulateEntityPacketListener implements PacketHandler { if (passenger == null || passenger.isPassenger() || passenger instanceof net.minecraft.world.entity.player.Player || passenger.hasPassenger(ManipulateEntityPacketListener::isPlayer)) continue; - String passengerType = EntityType.getKey(passenger.getType()).toString(); - - if (!whitelistedEntities.isEmpty() && !whitelistedEntities.contains(passengerType)) continue; - if (blacklistedEntities.contains(passengerType)) continue; + if (!this.plugin.canEntityBeManipulated(passenger.getType())) { + continue; + } // Prevent mounting loop if (passenger.getSelfAndPassengers().anyMatch(entity2 -> entity2 == entity)) { @@ -195,10 +189,9 @@ public class ManipulateEntityPacketListener implements PacketHandler { if (passenger == null || passenger == entity || passenger instanceof net.minecraft.world.entity.player.Player || passenger.hasPassenger(ManipulateEntityPacketListener::isPlayer)) continue; - String passengerType = EntityType.getKey(passenger.getType()).toString(); - - if (!whitelistedEntities.isEmpty() && !whitelistedEntities.contains(passengerType)) continue; - if (blacklistedEntities.contains(passengerType)) continue; + if (!this.plugin.canEntityBeManipulated(passenger.getType())) { + continue; + } Entity vehicle = passenger.getVehicle(); if (vehicle == entity) { diff --git a/src/main/java/com/moulberry/axiom/packet/impl/RequestChunkDataPacketListener.java b/src/main/java/com/moulberry/axiom/packet/impl/RequestChunkDataPacketListener.java index b4b589a..4354755 100644 --- a/src/main/java/com/moulberry/axiom/packet/impl/RequestChunkDataPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/impl/RequestChunkDataPacketListener.java @@ -68,7 +68,7 @@ public class RequestChunkDataPacketListener implements PacketHandler { ResourceKey worldKey = friendlyByteBuf.readResourceKey(Registries.DIMENSION); ServerLevel level = server.getLevel(worldKey); - if (level == null) { + if (level == null || level != player.serverLevel()) { sendEmptyResponse(player, id); return; } diff --git a/src/main/java/com/moulberry/axiom/packet/impl/RequestEntityDataPacketListener.java b/src/main/java/com/moulberry/axiom/packet/impl/RequestEntityDataPacketListener.java new file mode 100644 index 0000000..989a425 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/packet/impl/RequestEntityDataPacketListener.java @@ -0,0 +1,108 @@ +package com.moulberry.axiom.packet.impl; + +import com.moulberry.axiom.AxiomPaper; +import com.moulberry.axiom.VersionHelper; +import com.moulberry.axiom.integration.Integration; +import com.moulberry.axiom.packet.PacketHandler; +import io.netty.buffer.Unpooled; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.game.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.player.Player; +import org.bukkit.Location; +import org.bukkit.craftbukkit.v1_20_R1.entity.CraftPlayer; + +import java.util.*; + +public class RequestEntityDataPacketListener implements PacketHandler { + + public static final ResourceLocation RESPONSE_ID = VersionHelper.createResourceLocation("axiom:response_entity_data"); + + private final AxiomPaper plugin; + private final boolean forceFail; + public RequestEntityDataPacketListener(AxiomPaper plugin, boolean forceFail) { + this.plugin = plugin; + this.forceFail = forceFail; + } + + @Override + public void onReceive(org.bukkit.entity.Player bukkitPlayer, FriendlyByteBuf friendlyByteBuf) { + ServerPlayer player = ((CraftPlayer)bukkitPlayer).getHandle(); + long id = friendlyByteBuf.readLong(); + + if (this.forceFail || !this.plugin.canUseAxiom(bukkitPlayer, "axiom.entity.request_data") || this.plugin.isMismatchedDataVersion(bukkitPlayer.getUniqueId())) { + // We always send an 'empty' response in order to make the client happy + sendResponse(player, id, true, Map.of()); + return; + } + + if (!this.plugin.canModifyWorld(bukkitPlayer, bukkitPlayer.getWorld())) { + sendResponse(player, id, true, Map.of()); + return; + } + + List request = friendlyByteBuf.readCollection(this.plugin.limitCollection(ArrayList::new), buf -> buf.readUUID()); + ServerLevel serverLevel = player.serverLevel(); + + final int maxPacketSize = 0x100000; + int remainingBytes = maxPacketSize; + + Map entityData = new HashMap<>(); + + Set visitedEntities = new HashSet<>(); + + for (UUID uuid : request) { + if (!visitedEntities.add(uuid)) { + continue; + } + + Entity entity = serverLevel.getEntity(uuid); + if (entity == null || entity instanceof Player) { + continue; + } + + if (!this.plugin.canEntityBeManipulated(entity.getType())) { + continue; + } + + if (!Integration.canPlaceBlock(bukkitPlayer, new Location(bukkitPlayer.getWorld(), + entity.getBlockX(), entity.getBlockY(), entity.getBlockZ()))) { + continue; + } + + CompoundTag entityTag = new CompoundTag(); + if (entity.save(entityTag)) { + int size = entityTag.sizeInBytes(); + if (size >= maxPacketSize) { + sendResponse(player, id, false, Map.of(uuid, entityTag)); + continue; + } + + // Send partial packet if we've run out of available bytes + if (remainingBytes - size < 0) { + sendResponse(player, id, false, entityData); + entityData.clear(); + } + + entityData.put(uuid, entityTag); + remainingBytes -= size; + } + } + + sendResponse(player, id, true, entityData); + } + + private static void sendResponse(ServerPlayer player, long id, boolean finished, Map map) { + FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); + friendlyByteBuf.writeLong(id); + friendlyByteBuf.writeBoolean(finished); + friendlyByteBuf.writeMap(map, (buf, uuid) -> buf.writeUUID(uuid), (buf, nbt) -> buf.writeNbt(nbt)); + + player.connection.send(new ClientboundCustomPayloadPacket(RESPONSE_ID, friendlyByteBuf)); + } + +} diff --git a/src/main/java/com/moulberry/axiom/packet/impl/SetBlockPacketListener.java b/src/main/java/com/moulberry/axiom/packet/impl/SetBlockPacketListener.java index 4d5f73a..99259b4 100644 --- a/src/main/java/com/moulberry/axiom/packet/impl/SetBlockPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/impl/SetBlockPacketListener.java @@ -92,7 +92,7 @@ public class SetBlockPacketListener implements PacketHandler { } // Read packet - IntFunction> mapFunction = FriendlyByteBuf.limitValue(Maps::newLinkedHashMapWithExpectedSize, 512); + IntFunction> mapFunction = this.plugin.limitCollection(Maps::newLinkedHashMapWithExpectedSize); IdMapper registry = this.plugin.getBlockRegistry(bukkitPlayer.getUniqueId()); Map blocks = friendlyByteBuf.readMap(mapFunction, FriendlyByteBuf::readBlockPos, buf -> buf.readById(registry)); diff --git a/src/main/java/com/moulberry/axiom/packet/impl/SpawnEntityPacketListener.java b/src/main/java/com/moulberry/axiom/packet/impl/SpawnEntityPacketListener.java index 05d4171..b6b3837 100644 --- a/src/main/java/com/moulberry/axiom/packet/impl/SpawnEntityPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/impl/SpawnEntityPacketListener.java @@ -54,16 +54,13 @@ public class SpawnEntityPacketListener implements PacketHandler { return; } - List entries = friendlyByteBuf.readCollection(FriendlyByteBuf.limitValue(ArrayList::new, 1000), + List entries = friendlyByteBuf.readCollection(this.plugin.limitCollection(ArrayList::new), buf -> new SpawnEntry(buf.readUUID(), buf.readDouble(), buf.readDouble(), buf.readDouble(), buf.readFloat(), buf.readFloat(), buf.readNullable(FriendlyByteBuf::readUUID), UnknownVersionHelper.readTagUnknown(buf, player))); ServerLevel serverLevel = ((CraftWorld)player.getWorld()).getHandle(); - List whitelistedEntities = this.plugin.configuration.getStringList("whitelist-entities"); - List blacklistedEntities = this.plugin.configuration.getStringList("blacklist-entities"); - for (SpawnEntry entry : entries) { Vec3 position = new Vec3(entry.x, entry.y, entry.z); @@ -99,9 +96,9 @@ public class SpawnEntityPacketListener implements PacketHandler { AtomicBoolean useNewUuid = new AtomicBoolean(true); Entity spawned = EntityType.loadEntityRecursive(tag, serverLevel, entity -> { - String type = EntityType.getKey(entity.getType()).toString(); - if (!whitelistedEntities.isEmpty() && !whitelistedEntities.contains(type)) return null; - if (blacklistedEntities.contains(type)) return null; + if (!this.plugin.canEntityBeManipulated(entity.getType())) { + return null; + } if (useNewUuid.getAndSet(false)) { entity.setUUID(entry.newUuid); diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 2f8d2af..85d9fb1 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -14,7 +14,7 @@ allow-teleport-between-worlds: true # Whether to allow clients to save/load/share blueprints through the server blueprint-sharing: false -# Allow large chunk data requests, not recommended for public servers +# Allow large chunk & entity data requests, not recommended for public servers allow-large-chunk-data-request: false # Raise the size limit for *all* packets to 2mb instead of 32kb. NOT RECOMMENDED FOR PUBLIC SERVERS @@ -57,6 +57,9 @@ whitelist-entities: blacklist-entities: # - "minecraft:ender_dragon" +# Disables entity data sanitization. NOT RECOMMENDED FOR PUBLIC SERVERS +disable-entity-sanitization: false + # True allows players to see/manipulate marker entities send-markers: false @@ -95,6 +98,7 @@ packet-handlers: teleport: true set-editor-views: true request-chunk-data: true + request-entity-data: true set-buffer: true spawn-entity: true manipulate-entity: true diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 574246a..45e25b1 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -25,6 +25,7 @@ permissions: axiom.entity.spawn: true axiom.entity.manipulate: true axiom.entity.delete: true + axiom.entity.request_data: true axiom.blueprint.*: description: Allows use of all blueprint-related features default: op