diff --git a/src/main/java/com/moulberry/axiom/AxiomPaper.java b/src/main/java/com/moulberry/axiom/AxiomPaper.java index 7e18301..0e960c0 100644 --- a/src/main/java/com/moulberry/axiom/AxiomPaper.java +++ b/src/main/java/com/moulberry/axiom/AxiomPaper.java @@ -124,6 +124,15 @@ public class AxiomPaper extends JavaPlugin implements Listener { if (configuration.getBoolean("packet-handlers.request-chunk-data")) { msg.registerIncomingPluginChannel(this, "axiom:request_chunk_data", new RequestChunkDataPacketListener(this)); } + if (configuration.getBoolean("packet-handlers.spawn-entity")) { + msg.registerIncomingPluginChannel(this, "axiom:spawn_entity", new SpawnEntityPacketListener(this)); + } + if (configuration.getBoolean("packet-handlers.manipulate-entity")) { + msg.registerIncomingPluginChannel(this, "axiom:manipulate_entity", new ManipulateEntityPacketListener(this)); + } + if (configuration.getBoolean("packet-handlers.delete-entity")) { + msg.registerIncomingPluginChannel(this, "axiom:delete_entity", new DeleteEntityPacketListener(this)); + } if (configuration.getBoolean("packet-handlers.set-buffer")) { SetBlockBufferPacketListener setBlockBufferPacketListener = new SetBlockBufferPacketListener(this); diff --git a/src/main/java/com/moulberry/axiom/packet/DeleteEntityPacketListener.java b/src/main/java/com/moulberry/axiom/packet/DeleteEntityPacketListener.java new file mode 100644 index 0000000..2fe253c --- /dev/null +++ b/src/main/java/com/moulberry/axiom/packet/DeleteEntityPacketListener.java @@ -0,0 +1,68 @@ +package com.moulberry.axiom.packet; + +import com.moulberry.axiom.AxiomPaper; +import io.netty.buffer.Unpooled; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.decoration.HangingEntity; +import net.minecraft.world.entity.decoration.ItemFrame; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.phys.Vec3; +import org.bukkit.craftbukkit.v1_20_R3.CraftWorld; +import org.bukkit.entity.Player; +import org.bukkit.plugin.messaging.PluginMessageListener; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.UUID; + +public class DeleteEntityPacketListener implements PluginMessageListener { + + private final AxiomPaper plugin; + public DeleteEntityPacketListener(AxiomPaper plugin) { + this.plugin = plugin; + } + + @Override + public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, @NotNull byte[] message) { + if (!this.plugin.canUseAxiom(player)) { + return; + } + + if (!player.hasPermission("axiom.entity.*") && !player.hasPermission("axiom.entity.delete")) { + return; + } + + if (!this.plugin.canModifyWorld(player, player.getWorld())) { + return; + } + + FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); + List delete = friendlyByteBuf.readList(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; + + entity.remove(Entity.RemovalReason.DISCARDED); + } + } + +} diff --git a/src/main/java/com/moulberry/axiom/packet/ManipulateEntityPacketListener.java b/src/main/java/com/moulberry/axiom/packet/ManipulateEntityPacketListener.java new file mode 100644 index 0000000..89da687 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/packet/ManipulateEntityPacketListener.java @@ -0,0 +1,201 @@ +package com.moulberry.axiom.packet; + +import com.moulberry.axiom.AxiomPaper; +import io.netty.buffer.Unpooled; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.RelativeMovement; +import net.minecraft.world.entity.decoration.HangingEntity; +import net.minecraft.world.entity.decoration.ItemFrame; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.phys.Vec3; +import org.bukkit.craftbukkit.v1_20_R3.CraftWorld; +import org.bukkit.entity.Player; +import org.bukkit.plugin.messaging.PluginMessageListener; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Set; +import java.util.UUID; + +public class ManipulateEntityPacketListener implements PluginMessageListener { + + private final AxiomPaper plugin; + public ManipulateEntityPacketListener(AxiomPaper plugin) { + this.plugin = plugin; + } + + public enum PassengerManipulation { + NONE, + REMOVE_ALL, + ADD_LIST, + REMOVE_LIST + } + + 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) { + UUID uuid = friendlyByteBuf.readUUID(); + + int flags = friendlyByteBuf.readByte(); + Set relativeMovementSet = null; + Vec3 position = null; + float yaw = 0; + float pitch = 0; + if (flags >= 0) { + relativeMovementSet = RelativeMovement.unpack(flags); + position = new Vec3(friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble()); + yaw = friendlyByteBuf.readFloat(); + pitch = friendlyByteBuf.readFloat(); + } + + CompoundTag nbt = friendlyByteBuf.readNbt(); + + PassengerManipulation passengerManipulation = friendlyByteBuf.readEnum(PassengerManipulation.class); + List passengers = List.of(); + if (passengerManipulation == PassengerManipulation.ADD_LIST || passengerManipulation == PassengerManipulation.REMOVE_LIST) { + passengers = friendlyByteBuf.readList(FriendlyByteBuf::readUUID); + } + + return new ManipulateEntry(uuid, relativeMovementSet, position, yaw, pitch, nbt, + passengerManipulation, passengers); + } + } + + private static final Rotation[] ROTATION_VALUES = Rotation.values(); + + @Override + public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, @NotNull byte[] message) { + if (!this.plugin.canUseAxiom(player)) { + return; + } + + if (!player.hasPermission("axiom.entity.*") && !player.hasPermission("axiom.entity.manipulate")) { + return; + } + + if (!this.plugin.canModifyWorld(player, player.getWorld())) { + return; + } + + FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); + List entries = friendlyByteBuf.readList(ManipulateEntry::read); + + 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 (entry.merge != null && !entry.merge.isEmpty()) { + CompoundTag compoundTag = entity.saveWithoutId(new CompoundTag()); + compoundTag = merge(compoundTag, entry.merge); + entity.load(compoundTag); + } + + Vec3 entryPos = entry.position(); + if (entryPos != null && entry.relativeMovementSet != null) { + double newX = entry.relativeMovementSet.contains(RelativeMovement.X) ? entity.position().x + entryPos.x : entryPos.x; + double newY = entry.relativeMovementSet.contains(RelativeMovement.Y) ? entity.position().y + entryPos.y : entryPos.y; + double newZ = entry.relativeMovementSet.contains(RelativeMovement.Z) ? entity.position().z + entryPos.z : entryPos.z; + float newYaw = entry.relativeMovementSet.contains(RelativeMovement.Y_ROT) ? entity.getYRot() + entry.yaw : entry.yaw; + float newPitch = entry.relativeMovementSet.contains(RelativeMovement.X_ROT) ? entity.getXRot() + entry.pitch : entry.pitch; + + if (entity instanceof HangingEntity hangingEntity) { + float changedYaw = newYaw - entity.getYRot(); + int rotations = Math.round(changedYaw / 90); + hangingEntity.rotate(ROTATION_VALUES[rotations & 3]); + + if (entity instanceof ItemFrame itemFrame && itemFrame.getDirection().getAxis() == Direction.Axis.Y) { + itemFrame.setRotation(itemFrame.getRotation() - Math.round(changedYaw / 45)); + } + } + + entity.teleportTo(serverLevel, newX, newY, newZ, Set.of(), newYaw, newPitch); + entity.setYHeadRot(newYaw); + } + + switch (entry.passengerManipulation) { + case NONE -> {} + case REMOVE_ALL -> entity.ejectPassengers(); + case ADD_LIST -> { + for (UUID passengerUuid : entry.passengers) { + Entity passenger = serverLevel.getEntity(passengerUuid); + + 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; + + // Prevent mounting loop + if (passenger.getSelfAndPassengers().anyMatch(entity2 -> entity2 == entity)) { + continue; + } + + passenger.startRiding(entity, true); + } + } + case REMOVE_LIST -> { + for (UUID passengerUuid : entry.passengers) { + Entity passenger = serverLevel.getEntity(passengerUuid); + 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; + + Entity vehicle = passenger.getVehicle(); + if (vehicle == entity) { + passenger.stopRiding(); + } + } + } + } + } + } + + private static CompoundTag merge(CompoundTag left, CompoundTag right) { + for (String key : right.getAllKeys()) { + Tag tag = right.get(key); + if (tag instanceof CompoundTag compound) { + if (compound.isEmpty()) { + left.remove(key); + } else if (left.contains(key, Tag.TAG_COMPOUND)) { + CompoundTag child = left.getCompound(key); + merge(child, compound); + } else { + left.put(key, tag.copy()); + } + } else { + left.put(key, tag.copy()); + } + } + return left; + } + + private static boolean isPlayer(Entity entity) { + return entity instanceof net.minecraft.world.entity.player.Player; + } + +} diff --git a/src/main/java/com/moulberry/axiom/packet/SpawnEntityPacketListener.java b/src/main/java/com/moulberry/axiom/packet/SpawnEntityPacketListener.java new file mode 100644 index 0000000..967707e --- /dev/null +++ b/src/main/java/com/moulberry/axiom/packet/SpawnEntityPacketListener.java @@ -0,0 +1,130 @@ +package com.moulberry.axiom.packet; + +import com.moulberry.axiom.AxiomPaper; +import com.moulberry.axiom.event.AxiomTeleportEvent; +import com.moulberry.axiom.event.AxiomUnknownTeleportEvent; +import io.netty.buffer.Unpooled; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.decoration.HangingEntity; +import net.minecraft.world.entity.decoration.ItemFrame; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.phys.Vec3; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.craftbukkit.v1_20_R3.CraftWorld; +import org.bukkit.craftbukkit.v1_20_R3.entity.CraftPlayer; +import org.bukkit.craftbukkit.v1_20_R3.util.CraftNamespacedKey; +import org.bukkit.entity.Player; +import org.bukkit.plugin.messaging.PluginMessageListener; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.UUID; + +public class SpawnEntityPacketListener implements PluginMessageListener { + + private final AxiomPaper plugin; + public SpawnEntityPacketListener(AxiomPaper plugin) { + this.plugin = plugin; + } + + private record SpawnEntry(UUID newUuid, double x, double y, double z, float yaw, float pitch, + @Nullable UUID copyFrom, CompoundTag tag) { + public SpawnEntry(FriendlyByteBuf friendlyByteBuf) { + this(friendlyByteBuf.readUUID(), friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble(), + friendlyByteBuf.readDouble(), friendlyByteBuf.readFloat(), friendlyByteBuf.readFloat(), + friendlyByteBuf.readNullable(FriendlyByteBuf::readUUID), friendlyByteBuf.readNbt()); + } + } + + private static final Rotation[] ROTATION_VALUES = Rotation.values(); + + @Override + public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, @NotNull byte[] message) { + if (!this.plugin.canUseAxiom(player)) { + return; + } + + if (!player.hasPermission("axiom.entity.*") && !player.hasPermission("axiom.entity.spawn")) { + return; + } + + if (!this.plugin.canModifyWorld(player, player.getWorld())) { + return; + } + + FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); + List entries = friendlyByteBuf.readList(SpawnEntry::new); + + 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); + + BlockPos blockPos = BlockPos.containing(position); + if (!Level.isInSpawnableBounds(blockPos)) { + continue; + } + + CompoundTag tag = entry.tag == null ? new CompoundTag() : entry.tag; + + if (serverLevel.getEntity(entry.newUuid) != null) continue; + + if (entry.copyFrom != null) { + Entity entityCopyFrom = serverLevel.getEntity(entry.copyFrom); + if (entityCopyFrom != null) { + CompoundTag compoundTag = new CompoundTag(); + if (entityCopyFrom.saveAsPassenger(compoundTag)) { + compoundTag.remove("Dimension"); + tag = tag.merge(compoundTag); + } + } + } + + if (!tag.contains("id")) continue; + + 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; + + entity.setUUID(entry.newUuid); + + if (entity instanceof HangingEntity hangingEntity) { + float changedYaw = entry.yaw - entity.getYRot(); + int rotations = Math.round(changedYaw / 90); + hangingEntity.rotate(ROTATION_VALUES[rotations & 3]); + + if (entity instanceof ItemFrame itemFrame && itemFrame.getDirection().getAxis() == Direction.Axis.Y) { + itemFrame.setRotation(itemFrame.getRotation() - Math.round(changedYaw / 45)); + } + } + + entity.moveTo(position.x, position.y, position.z, entry.yaw, entry.pitch); + entity.setYHeadRot(entity.getYRot()); + + return entity; + }); + + if (spawned != null) { + serverLevel.tryAddFreshEntityWithPassengers(spawned); + } + } + } + +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index edf4bc7..bf42371 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -34,6 +34,20 @@ block-buffer-rate-limit: 0 # Log large block buffer changes log-large-block-buffer-changes: false +# Whitelist entities that can be spawned/manipulated/deleted by the client +whitelist-entities: + - "minecraft:item_display" + - "minecraft:block_display" + - "minecraft:text_display" + - "minecraft:painting" + - "minecraft:armor_stand" + - "minecraft:item_frame" + - "minecraft:glow_item_frame" + +# Blacklist entities that can be spawned/manipulated/deleted by the client +blacklist-entities: +# - "minecraft:ender_dragon" + # Disallowed blocks disallowed-blocks: # - "minecraft:wheat" @@ -53,3 +67,6 @@ packet-handlers: set-editor-views: true request-chunk-data: true set-buffer: true + spawn-entity: true + manipulate-entity: true + delete-entity: true diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 67339da..f9b3b63 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -7,6 +7,15 @@ authors: api-version: "$apiVersion" permissions: axiom.*: - description: Allows use of all Axiom features + description: Allows use of all default Axiom features default: op + axiom.entity.*: + description: Allows use of all entity-related features (spawning, manipulating, deleting) + default: op + axiom.entity.spawn: + description: Allows entity spawning + axiom.entity.manipulate: + description: Allows entity manipulation + axiom.entity.delete: + description: Allows entity deletion