diff --git a/src/main/java/com/moulberry/axiom/AxiomPaper.java b/src/main/java/com/moulberry/axiom/AxiomPaper.java index 817a488..fe23a99 100644 --- a/src/main/java/com/moulberry/axiom/AxiomPaper.java +++ b/src/main/java/com/moulberry/axiom/AxiomPaper.java @@ -84,6 +84,8 @@ public class AxiomPaper extends JavaPlugin implements Listener { msg.registerOutgoingPluginChannel(this, "axiom:set_world_property"); msg.registerOutgoingPluginChannel(this, "axiom:ack_world_properties"); msg.registerOutgoingPluginChannel(this, "axiom:restrictions"); + msg.registerOutgoingPluginChannel(this, "axiom:marker_data"); + msg.registerOutgoingPluginChannel(this, "axiom:marker_nbt_response"); if (configuration.getBoolean("packet-handlers.hello")) { msg.registerIncomingPluginChannel(this, "axiom:hello", new HelloPacketListener(this)); @@ -127,6 +129,9 @@ public class AxiomPaper extends JavaPlugin implements Listener { if (configuration.getBoolean("packet-handlers.delete-entity")) { msg.registerIncomingPluginChannel(this, "axiom:delete_entity", new DeleteEntityPacketListener(this)); } + if (configuration.getBoolean("packet-handlers.marker-nbt-request")) { + msg.registerIncomingPluginChannel(this, "axiom:marker_nbt_request", new MarkerNbtRequestPacketListener(this)); + } if (configuration.getBoolean("packet-handlers.set-buffer")) { SetBlockBufferPacketListener setBlockBufferPacketListener = new SetBlockBufferPacketListener(this); @@ -238,11 +243,12 @@ public class AxiomPaper extends JavaPlugin implements Listener { playerRestrictions.keySet().retainAll(stillActiveAxiomPlayers); }, 20, 20); + boolean sendMarkers = configuration.getBoolean("send-markers"); int maxChunkRelightsPerTick = configuration.getInt("max-chunk-relights-per-tick"); int maxChunkSendsPerTick = configuration.getInt("max-chunk-sends-per-tick"); Bukkit.getScheduler().scheduleSyncRepeatingTask(this, () -> { - WorldExtension.tick(MinecraftServer.getServer(), maxChunkRelightsPerTick, maxChunkSendsPerTick); + WorldExtension.tick(MinecraftServer.getServer(), sendMarkers, maxChunkRelightsPerTick, maxChunkSendsPerTick); }, 1, 1); } @@ -292,17 +298,22 @@ public class AxiomPaper extends JavaPlugin implements Listener { @EventHandler public void onFailMove(PlayerFailMoveEvent event) { - if (event.getPlayer().hasPermission("axiom.*")) { - if (event.getFailReason() == PlayerFailMoveEvent.FailReason.MOVED_TOO_QUICKLY) { - event.setAllowed(true); // Support for arcball camera - } else if (event.getPlayer().isFlying()) { - event.setAllowed(true); // Support for noclip - } + if (!this.activeAxiomPlayers.contains(event.getPlayer().getUniqueId())) { + return; + } + if (event.getFailReason() == PlayerFailMoveEvent.FailReason.MOVED_TOO_QUICKLY) { + event.setAllowed(true); // Support for arcball camera + } else if (event.getPlayer().isFlying()) { + event.setAllowed(true); // Support for noclip } } @EventHandler public void onChangedWorld(PlayerChangedWorldEvent event) { + if (!this.activeAxiomPlayers.contains(event.getPlayer().getUniqueId())) { + return; + } + World world = event.getPlayer().getWorld(); ServerWorldPropertiesRegistry properties = getOrCreateWorldProperties(world); @@ -312,6 +323,8 @@ public class AxiomPaper extends JavaPlugin implements Listener { } else { properties.registerFor(this, event.getPlayer()); } + + WorldExtension.onPlayerJoin(world, event.getPlayer()); } @EventHandler diff --git a/src/main/java/com/moulberry/axiom/NbtSanitization.java b/src/main/java/com/moulberry/axiom/NbtSanitization.java new file mode 100644 index 0000000..c24c4df --- /dev/null +++ b/src/main/java/com/moulberry/axiom/NbtSanitization.java @@ -0,0 +1,63 @@ +package com.moulberry.axiom; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; + +import java.util.Set; + +public class NbtSanitization { + + private static final Set ALLOWED_KEYS = Set.of( + "id", // entity id + // generic + "Pos", + "Rotation", + "Invulnerable", + "CustomName", + "CustomNameVisible", + "Silent", + "NoGravity", + "Glowing", + "Tags", + "Passengers", + // marker + "data", + // display entity + "transformation", + "interpolation_duration", + "start_interpolation", + "teleport_duration", + "billboard", + "view_range", + "shadow_radius", + "shadow_strength", + "width", + "height", + "glow_color_override", + "brightness", + "line_width", + "text_opacity", + "background", + "shadow", + "see_through", + "default_background", + "alignment", + "text", + "block_state", + "item", + "item_display" + ); + + public static void sanitizeEntity(CompoundTag entityRoot) { + entityRoot.getAllKeys().retainAll(ALLOWED_KEYS); + + if (entityRoot.contains("Passengers", Tag.TAG_LIST)) { + ListTag listTag = entityRoot.getList("Passengers", Tag.TAG_COMPOUND); + for (Tag tag : listTag) { + sanitizeEntity((CompoundTag) tag); + } + } + } + +} diff --git a/src/main/java/com/moulberry/axiom/WorldExtension.java b/src/main/java/com/moulberry/axiom/WorldExtension.java index e18c2b8..2152bb2 100644 --- a/src/main/java/com/moulberry/axiom/WorldExtension.java +++ b/src/main/java/com/moulberry/axiom/WorldExtension.java @@ -1,15 +1,24 @@ package com.moulberry.axiom; +import com.moulberry.axiom.marker.MarkerData; +import io.netty.buffer.Unpooled; import it.unimi.dsi.fastutil.longs.*; +import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket; import net.minecraft.resources.ResourceKey; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ChunkMap; 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 net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; import net.minecraft.world.level.chunk.LevelChunk; +import org.bukkit.World; +import org.bukkit.craftbukkit.v1_20_R3.CraftWorld; +import org.bukkit.craftbukkit.v1_20_R3.entity.CraftPlayer; +import org.bukkit.entity.Player; import java.util.*; @@ -23,15 +32,16 @@ public class WorldExtension { return extension; } - public static void tick(MinecraftServer server, int maxChunkRelightsPerTick, int maxChunkSendsPerTick) { + public static void onPlayerJoin(World world, Player player) { + ServerLevel level = ((CraftWorld)world).getHandle(); + get(level).onPlayerJoin(player); + } + + public static void tick(MinecraftServer server, boolean sendMarkers, int maxChunkRelightsPerTick, int maxChunkSendsPerTick) { extensions.keySet().retainAll(server.levelKeys()); for (ServerLevel level : server.getAllLevels()) { - WorldExtension extension = extensions.get(level.dimension()); - if (extension != null) { - extension.level = level; - extension.tick(maxChunkRelightsPerTick, maxChunkSendsPerTick); - } + get(level).tick(sendMarkers, maxChunkRelightsPerTick, maxChunkSendsPerTick); } } @@ -39,6 +49,7 @@ public class WorldExtension { private final LongSet pendingChunksToSend = new LongOpenHashSet(); private final LongSet pendingChunksToLight = new LongOpenHashSet(); + private final Map previousMarkerData = new HashMap<>(); public void sendChunk(int cx, int cz) { this.pendingChunksToSend.add(ChunkPos.asLong(cx, cz)); @@ -48,7 +59,66 @@ public class WorldExtension { this.pendingChunksToLight.add(ChunkPos.asLong(cx, cz)); } - public void tick(int maxChunkRelightsPerTick, int maxChunkSendsPerTick) { + public void onPlayerJoin(Player player) { + if (!this.previousMarkerData.isEmpty()) { + List markerData = new ArrayList<>(this.previousMarkerData.values()); + + FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + buf.writeCollection(markerData, MarkerData::write); + buf.writeCollection(Set.of(), FriendlyByteBuf::writeUUID); + byte[] bytes = new byte[buf.writerIndex()]; + buf.getBytes(0, bytes); + + player.sendPluginMessage(AxiomPaper.PLUGIN, "axiom:marker_data", bytes); + } + } + + public void tick(boolean sendMarkers, int maxChunkRelightsPerTick, int maxChunkSendsPerTick) { + if (sendMarkers) { + this.tickMarkers(); + } + this.tickChunkRelight(maxChunkRelightsPerTick, maxChunkSendsPerTick); + } + + private void tickMarkers() { + List changedData = new ArrayList<>(); + + Set allMarkers = new HashSet<>(); + + for (Entity entity : this.level.getEntities().getAll()) { + if (entity instanceof Marker marker) { + MarkerData currentData = MarkerData.createFrom(marker); + + MarkerData previousData = this.previousMarkerData.get(marker.getUUID()); + if (!Objects.equals(currentData, previousData)) { + this.previousMarkerData.put(marker.getUUID(), currentData); + changedData.add(currentData); + } + + allMarkers.add(marker.getUUID()); + } + } + + Set oldUuids = new HashSet<>(this.previousMarkerData.keySet()); + oldUuids.removeAll(allMarkers); + this.previousMarkerData.keySet().removeAll(oldUuids); + + if (!changedData.isEmpty() || !oldUuids.isEmpty()) { + FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + buf.writeCollection(changedData, MarkerData::write); + buf.writeCollection(oldUuids, FriendlyByteBuf::writeUUID); + byte[] bytes = new byte[buf.writerIndex()]; + buf.getBytes(0, bytes); + + for (ServerPlayer player : this.level.players()) { + if (AxiomPaper.PLUGIN.activeAxiomPlayers.contains(player.getUUID())) { + player.getBukkitEntity().sendPluginMessage(AxiomPaper.PLUGIN, "axiom:marker_data", bytes); + } + } + } + } + + private void tickChunkRelight(int maxChunkRelightsPerTick, int maxChunkSendsPerTick) { ChunkMap chunkMap = this.level.getChunkSource().chunkMap; boolean sendAll = maxChunkSendsPerTick <= 0; diff --git a/src/main/java/com/moulberry/axiom/marker/MarkerData.java b/src/main/java/com/moulberry/axiom/marker/MarkerData.java new file mode 100644 index 0000000..e86411f --- /dev/null +++ b/src/main/java/com/moulberry/axiom/marker/MarkerData.java @@ -0,0 +1,105 @@ +package com.moulberry.axiom.marker; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.Marker; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; +import xyz.jpenilla.reflectionremapper.ReflectionRemapper; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.UUID; + +public record MarkerData(UUID uuid, Vec3 position, @Nullable String name, @Nullable Vec3 minRegion, @Nullable Vec3 maxRegion) { + public static MarkerData read(FriendlyByteBuf friendlyByteBuf) { + UUID uuid = friendlyByteBuf.readUUID(); + Vec3 position = new Vec3(friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble()); + String name = friendlyByteBuf.readNullable(FriendlyByteBuf::readUtf); + + Vec3 minRegion = null; + Vec3 maxRegion = null; + if (friendlyByteBuf.readBoolean()) { + minRegion = new Vec3(friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble()); + maxRegion = new Vec3(friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble()); + } + + return new MarkerData(uuid, position, name, minRegion, maxRegion); + } + + public static void write(FriendlyByteBuf friendlyByteBuf, MarkerData markerData) { + friendlyByteBuf.writeUUID(markerData.uuid); + friendlyByteBuf.writeDouble(markerData.position.x); + friendlyByteBuf.writeDouble(markerData.position.y); + friendlyByteBuf.writeDouble(markerData.position.z); + friendlyByteBuf.writeNullable(markerData.name, FriendlyByteBuf::writeUtf); + + if (markerData.minRegion != null && markerData.maxRegion != null) { + friendlyByteBuf.writeBoolean(true); + friendlyByteBuf.writeDouble(markerData.minRegion.x); + friendlyByteBuf.writeDouble(markerData.minRegion.y); + friendlyByteBuf.writeDouble(markerData.minRegion.z); + friendlyByteBuf.writeDouble(markerData.maxRegion.x); + friendlyByteBuf.writeDouble(markerData.maxRegion.y); + friendlyByteBuf.writeDouble(markerData.maxRegion.z); + } else { + friendlyByteBuf.writeBoolean(false); + } + } + + private static final Field dataField; + static { + ReflectionRemapper reflectionRemapper = ReflectionRemapper.forReobfMappingsInPaperJar(); + String fieldName = reflectionRemapper.remapFieldName(Marker.class, "data"); + + try { + dataField = Marker.class.getDeclaredField(fieldName); + dataField.setAccessible(true); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + public static CompoundTag getData(Marker marker) { + try { + return (CompoundTag) dataField.get(marker); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + public static MarkerData createFrom(Marker marker) { + Vec3 position = marker.position(); + CompoundTag data = getData(marker); + + String name = data.getString("name").trim(); + if (name.isEmpty()) name = null; + + Vec3 minRegion = null; + Vec3 maxRegion = null; + if (data.contains("min", Tag.TAG_LIST) && data.contains("max", Tag.TAG_LIST)) { + ListTag min = data.getList("min", Tag.TAG_DOUBLE); + ListTag max = data.getList("max", Tag.TAG_DOUBLE); + + if (min.size() == 3 && max.size() == 3) { + double minX = min.getDouble(0); + double minY = min.getDouble(1); + double minZ = min.getDouble(2); + double maxX = max.getDouble(0); + double maxY = max.getDouble(1); + double maxZ = max.getDouble(2); + minRegion = new Vec3(minX, minY, minZ); + maxRegion = new Vec3(maxX, maxY, maxZ); + } + + } + + return new MarkerData(marker.getUUID(), position, name, minRegion, maxRegion); + } +} diff --git a/src/main/java/com/moulberry/axiom/packet/HelloPacketListener.java b/src/main/java/com/moulberry/axiom/packet/HelloPacketListener.java index 0344e6b..6843a54 100644 --- a/src/main/java/com/moulberry/axiom/packet/HelloPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/HelloPacketListener.java @@ -4,6 +4,7 @@ import com.google.common.util.concurrent.RateLimiter; import com.moulberry.axiom.AxiomConstants; import com.moulberry.axiom.AxiomPaper; import com.moulberry.axiom.View; +import com.moulberry.axiom.WorldExtension; import com.moulberry.axiom.event.AxiomHandshakeEvent; import com.moulberry.axiom.persistence.ItemStackDataType; import com.moulberry.axiom.persistence.UUIDDataType; @@ -167,6 +168,8 @@ public class HelloPacketListener implements PluginMessageListener { } else { properties.registerFor(plugin, player); } + + WorldExtension.onPlayerJoin(world, player); } } diff --git a/src/main/java/com/moulberry/axiom/packet/ManipulateEntityPacketListener.java b/src/main/java/com/moulberry/axiom/packet/ManipulateEntityPacketListener.java index dc31da8..0cb5769 100644 --- a/src/main/java/com/moulberry/axiom/packet/ManipulateEntityPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/ManipulateEntityPacketListener.java @@ -1,6 +1,7 @@ package com.moulberry.axiom.packet; import com.moulberry.axiom.AxiomPaper; +import com.moulberry.axiom.NbtSanitization; import io.netty.buffer.Unpooled; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; @@ -104,6 +105,8 @@ public class ManipulateEntityPacketListener implements PluginMessageListener { if (blacklistedEntities.contains(type)) continue; if (entry.merge != null && !entry.merge.isEmpty()) { + NbtSanitization.sanitizeEntity(entry.merge); + CompoundTag compoundTag = entity.saveWithoutId(new CompoundTag()); compoundTag = merge(compoundTag, entry.merge); entity.load(compoundTag); diff --git a/src/main/java/com/moulberry/axiom/packet/MarkerNbtRequestPacketListener.java b/src/main/java/com/moulberry/axiom/packet/MarkerNbtRequestPacketListener.java new file mode 100644 index 0000000..55f9b16 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/packet/MarkerNbtRequestPacketListener.java @@ -0,0 +1,66 @@ +package com.moulberry.axiom.packet; + +import com.moulberry.axiom.AxiomPaper; +import com.moulberry.axiom.event.AxiomTimeChangeEvent; +import com.moulberry.axiom.integration.plotsquared.PlotSquaredIntegration; +import com.moulberry.axiom.marker.MarkerData; +import io.netty.buffer.Unpooled; +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.Marker; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.Level; +import org.bukkit.Bukkit; +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 java.util.Set; +import java.util.UUID; + +public class MarkerNbtRequestPacketListener implements PluginMessageListener { + + private final AxiomPaper plugin; + public MarkerNbtRequestPacketListener(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.manipulate")) { + return; + } + + if (!this.plugin.canModifyWorld(player, player.getWorld())) { + return; + } + + FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); + UUID uuid = friendlyByteBuf.readUUID(); + + ServerLevel serverLevel = ((CraftWorld)player.getWorld()).getHandle(); + + Entity entity = serverLevel.getEntity(uuid); + if (entity instanceof Marker marker) { + CompoundTag data = MarkerData.getData(marker); + + FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + buf.writeUUID(uuid); + buf.writeNbt(data); + byte[] bytes = new byte[buf.writerIndex()]; + buf.getBytes(0, bytes); + + player.sendPluginMessage(AxiomPaper.PLUGIN, "axiom:marker_nbt_response", bytes); + } + } + +} diff --git a/src/main/java/com/moulberry/axiom/packet/SpawnEntityPacketListener.java b/src/main/java/com/moulberry/axiom/packet/SpawnEntityPacketListener.java index 967707e..ea6f385 100644 --- a/src/main/java/com/moulberry/axiom/packet/SpawnEntityPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/SpawnEntityPacketListener.java @@ -1,6 +1,7 @@ package com.moulberry.axiom.packet; import com.moulberry.axiom.AxiomPaper; +import com.moulberry.axiom.NbtSanitization; import com.moulberry.axiom.event.AxiomTeleportEvent; import com.moulberry.axiom.event.AxiomUnknownTeleportEvent; import io.netty.buffer.Unpooled; @@ -81,9 +82,11 @@ public class SpawnEntityPacketListener implements PluginMessageListener { continue; } + if (serverLevel.getEntity(entry.newUuid) != null) continue; + CompoundTag tag = entry.tag == null ? new CompoundTag() : entry.tag; - if (serverLevel.getEntity(entry.newUuid) != null) continue; + NbtSanitization.sanitizeEntity(tag); if (entry.copyFrom != null) { Entity entityCopyFrom = serverLevel.getEntity(entry.copyFrom); diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 7e87583..f5c0b38 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -19,7 +19,6 @@ incompatible-data-version: "warn" unsupported-axiom-version: "warn" client-doesnt-support-restrictions: "ignore" - # Maximum packet size. Must not be less than 32767 max-block-buffer-packet-size: 0x100000 @@ -51,6 +50,9 @@ whitelist-entities: blacklist-entities: # - "minecraft:ender_dragon" +# True allows players to see/manipulate marker entities +send-markers: false + # Disallowed blocks disallowed-blocks: # - "minecraft:wheat" @@ -73,3 +75,4 @@ packet-handlers: spawn-entity: true manipulate-entity: true delete-entity: true + marker-nbt-request: true