From 993f29e7361b9d98d365c8ab1c12bf18943ebfe5 Mon Sep 17 00:00:00 2001 From: tmpod Date: Fri, 19 Sep 2025 17:39:51 +0100 Subject: [PATCH 1/2] Add a new event for when an entity's scoreboard tags are changed This adds a new event, `ScoreboardTagsChangeEvent`, which is called when an entity gets their scoreboard tags changed, through plugins calling `Entity#addScoreboardTag`, `/tag`, `/data`, etc. It is fired before the change happens and cannot be cancelled. It contains the affected entity, which change it is happening (add, remove or replace), and which tags are part of the change. --- .../entity/ScoreboardTagsChangeEvent.java | 67 +++++++++++++++++++ .../craftbukkit/event/CraftEventFactory.java | 14 +++- 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 paper-api/src/main/java/io/papermc/paper/event/entity/ScoreboardTagsChangeEvent.java diff --git a/paper-api/src/main/java/io/papermc/paper/event/entity/ScoreboardTagsChangeEvent.java b/paper-api/src/main/java/io/papermc/paper/event/entity/ScoreboardTagsChangeEvent.java new file mode 100644 index 000000000000..11ed48079d16 --- /dev/null +++ b/paper-api/src/main/java/io/papermc/paper/event/entity/ScoreboardTagsChangeEvent.java @@ -0,0 +1,67 @@ +package io.papermc.paper.event.entity; + +import org.bukkit.entity.Entity; +import org.bukkit.event.HandlerList; +import org.bukkit.event.entity.EntityEvent; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; +import java.util.Collections; +import java.util.List; + +/** + * Called right before the scoreboard tags for an entity are changed. + */ +@NullMarked +public class ScoreboardTagsChangeEvent extends EntityEvent { + + private static final HandlerList HANDLER_LIST = new HandlerList(); + + private final List tags; + private final Change change; + + @ApiStatus.Internal + public ScoreboardTagsChangeEvent(final Entity entity, final List tags, final Change change) { + super(entity); + this.tags = Collections.unmodifiableList(tags); + this.change = change; + } + + /** + * Get the tags that are being added/removed/set. + * + * @return Tags being added/removed/set. Unmodifiable. + */ + public List getTags() { + return this.tags; + } + + /** + * Gets the type of change happening to the entity's tags. + * + * @return Type of change happening. + */ + public Change getChange() { + return this.change; + } + + @Override + public HandlerList getHandlers() { + return HANDLER_LIST; + } + + public static HandlerList getHandlerList() { + return HANDLER_LIST; + } + + /** + * Represents the possible changes made to the entity's tags. + */ + public enum Change { + /** The tags were added to the entity. */ + ADD, + /** The tags were removed from the entity. */ + REMOVE, + /** The tags replaced the entity's existing tags. */ + SET, + } +} diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java index 9878d6842ec7..a7128296194f 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java @@ -18,12 +18,12 @@ import io.papermc.paper.connection.HorriblePlayerLoginEventHack; import io.papermc.paper.connection.PlayerConnection; import io.papermc.paper.event.connection.PlayerConnectionValidateLoginEvent; +import io.papermc.paper.event.entity.ScoreboardTagsChangeEvent; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.network.Connection; import net.minecraft.network.chat.Component; import net.minecraft.network.protocol.game.ServerboundContainerClosePacket; -import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; @@ -2115,4 +2115,16 @@ public static Component handleLoginResult(PlayerList.LoginResult result, PlayerC return disconnectReason; } + + // Paper start - add ScoreboardTagsChangeEvent + public static ScoreboardTagsChangeEvent callScoreboardTagsChangeEvent(Entity entity, List tags, ScoreboardTagsChangeEvent.Change change) { + final var event = new ScoreboardTagsChangeEvent(entity.getBukkitEntity(), tags, change); + Bukkit.getPluginManager().callEvent(event); + return event; + } + + public static ScoreboardTagsChangeEvent callScoreboardTagsChangeEvent(Entity entity, String tag, ScoreboardTagsChangeEvent.Change change) { + return callScoreboardTagsChangeEvent(entity, List.of(tag), change); + } + // Paper end - add ScoreboardTagsChangeEvent } From ba5904f3ed3badf45a3fe8509924e3580bfa4159 Mon Sep 17 00:00:00 2001 From: tmpod Date: Fri, 19 Sep 2025 17:41:11 +0100 Subject: [PATCH 2/2] Implement triggers for the new ScoreboardTagsChangeEvent --- .../0003-Entity-Activation-Range-2.0.patch | 12 +++---- .../0016-Moonrise-optimisation-patches.patch | 18 +++++------ ...033-Optimise-EntityScheduler-ticking.patch | 4 +-- .../data/EntityDataAccessor.java.patch | 32 +++++++++++++++++++ .../minecraft/world/entity/Entity.java.patch | 14 +++++--- 5 files changed, 59 insertions(+), 21 deletions(-) create mode 100644 paper-server/patches/sources/net/minecraft/server/commands/data/EntityDataAccessor.java.patch diff --git a/paper-server/patches/features/0003-Entity-Activation-Range-2.0.patch b/paper-server/patches/features/0003-Entity-Activation-Range-2.0.patch index d5d1e0363ceb..d37b095202c5 100644 --- a/paper-server/patches/features/0003-Entity-Activation-Range-2.0.patch +++ b/paper-server/patches/features/0003-Entity-Activation-Range-2.0.patch @@ -484,7 +484,7 @@ index c70a58f5f633fa8e255f74c42f5e87c96b7b013a..ec20a5a6d7c8f65abda528fec36bec7b public void tick() { super.tick(); diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java -index b9f0745cdc42a093b69f46e353b99adf0c5aac56..a5d65e1b31e2e43cf039b8a19286a5324a739bbe 100644 +index a5c1c1f88f2e1930f46c18654a384b906ca66515..f9fea0ab2c7c5093ce7838073020f9985d6e58d0 100644 --- a/net/minecraft/world/entity/Entity.java +++ b/net/minecraft/world/entity/Entity.java @@ -409,6 +409,15 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess @@ -517,7 +517,7 @@ index b9f0745cdc42a093b69f46e353b99adf0c5aac56..a5d65e1b31e2e43cf039b8a19286a532 SynchedEntityData.Builder builder = new SynchedEntityData.Builder(this); builder.define(DATA_SHARED_FLAGS_ID, (byte)0); builder.define(DATA_AIR_SUPPLY_ID, this.getMaxAirSupply()); -@@ -984,6 +1000,10 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess +@@ -986,6 +1002,10 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess this.setPos(this.getX() + movement.x, this.getY() + movement.y, this.getZ() + movement.z); } else { if (type == MoverType.PISTON) { @@ -528,7 +528,7 @@ index b9f0745cdc42a093b69f46e353b99adf0c5aac56..a5d65e1b31e2e43cf039b8a19286a532 movement = this.limitPistonMovement(movement); if (movement.equals(Vec3.ZERO)) { return; -@@ -997,6 +1017,13 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess +@@ -999,6 +1019,13 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess this.stuckSpeedMultiplier = Vec3.ZERO; this.setDeltaMovement(Vec3.ZERO); } @@ -543,7 +543,7 @@ index b9f0745cdc42a093b69f46e353b99adf0c5aac56..a5d65e1b31e2e43cf039b8a19286a532 movement = this.maybeBackOffFromEdge(movement, type); Vec3 vec3 = this.collide(movement); diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java -index 8dd8a3d2a862998a920f226b2a6d9a877aac70a8..0c52e315557e1dae6a852694786e72241fff1e29 100644 +index bcdd4ad0482297c9ea376e3632722570e2dd4c23..0268e02d2ef2cb3d699644a804e23a6da4521f4c 100644 --- a/net/minecraft/world/entity/LivingEntity.java +++ b/net/minecraft/world/entity/LivingEntity.java @@ -3215,6 +3215,14 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin @@ -562,7 +562,7 @@ index 8dd8a3d2a862998a920f226b2a6d9a877aac70a8..0c52e315557e1dae6a852694786e7224 public void tick() { super.tick(); diff --git a/net/minecraft/world/entity/Mob.java b/net/minecraft/world/entity/Mob.java -index c737fd87e804129fac95be7d19b4aafab38d8b94..21c6d4746c6a905ec312dc1e35535cbd13868322 100644 +index 63f0b0a39e51b1cd30c2d131414d9512886a664f..e0b3cb2b2694768803ed347a1026b881fd624951 100644 --- a/net/minecraft/world/entity/Mob.java +++ b/net/minecraft/world/entity/Mob.java @@ -207,6 +207,19 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab @@ -838,7 +838,7 @@ index 52acc72841f0c6980f5f3f8ef21d0b29dd472ce3..41a6ec508a10a49a37539d2f10171d15 + } diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java -index 06069d3ac598f5f12feab038de4f1199794298f6..980eaba27ce2616c1573a4760cf4acc2dd251190 100644 +index 27dc02fd57dfe8942dc835d610b922dd7e66f309..a3926741a46756d8f7fdeb934685ba36122add76 100644 --- a/net/minecraft/world/level/Level.java +++ b/net/minecraft/world/level/Level.java @@ -143,6 +143,12 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl diff --git a/paper-server/patches/features/0016-Moonrise-optimisation-patches.patch b/paper-server/patches/features/0016-Moonrise-optimisation-patches.patch index e64c79dfb5ac..3484c6034ada 100644 --- a/paper-server/patches/features/0016-Moonrise-optimisation-patches.patch +++ b/paper-server/patches/features/0016-Moonrise-optimisation-patches.patch @@ -28728,7 +28728,7 @@ index 8cc5c0716392ba06501542ff5cbe71ee43979e5d..09fd99c9cbd23b5f3c899bfb00c9b896 + // Paper end - block counting } diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java -index 17d9e4699e8e2ce934c25d34d7269bdda5c87d00..295d40341df325b3fb6f14164283863a5a96b8bf 100644 +index 990305f720cf0817a5b369c6454cd2c61b423bc7..b4392f9914d0a104d9e933c06ac72e5363510184 100644 --- a/net/minecraft/world/entity/Entity.java +++ b/net/minecraft/world/entity/Entity.java @@ -147,7 +147,7 @@ import net.minecraft.world.waypoints.WaypointTransmitter; @@ -28979,7 +28979,7 @@ index 17d9e4699e8e2ce934c25d34d7269bdda5c87d00..295d40341df325b3fb6f14164283863a public Entity(EntityType entityType, Level level) { this.type = entityType; -@@ -1378,35 +1484,77 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess +@@ -1380,35 +1486,77 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess return distance; } @@ -29081,7 +29081,7 @@ index 17d9e4699e8e2ce934c25d34d7269bdda5c87d00..295d40341df325b3fb6f14164283863a } private static float[] collectCandidateStepUpHeights(AABB box, List colliders, float deltaY, float maxUpStep) { -@@ -2680,21 +2828,110 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess +@@ -2682,21 +2830,110 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess } public boolean isInWall() { @@ -29203,7 +29203,7 @@ index 17d9e4699e8e2ce934c25d34d7269bdda5c87d00..295d40341df325b3fb6f14164283863a } public InteractionResult interact(Player player, InteractionHand hand) { -@@ -4290,15 +4527,17 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess +@@ -4292,15 +4529,17 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess } public Iterable getIndirectPassengers() { @@ -29229,7 +29229,7 @@ index 17d9e4699e8e2ce934c25d34d7269bdda5c87d00..295d40341df325b3fb6f14164283863a } public int countPlayerPassengers() { -@@ -4441,77 +4680,136 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess +@@ -4443,77 +4682,136 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess return Mth.lerp(partialTick, this.yRotO, this.yRot); } @@ -29420,7 +29420,7 @@ index 17d9e4699e8e2ce934c25d34d7269bdda5c87d00..295d40341df325b3fb6f14164283863a public boolean touchingUnloadedChunk() { AABB aabb = this.getBoundingBox().inflate(1.0); -@@ -4666,6 +4964,15 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess +@@ -4668,6 +4966,15 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess } public final void setPosRaw(double x, double y, double z, boolean forceBoundingBoxUpdate) { @@ -29436,7 +29436,7 @@ index 17d9e4699e8e2ce934c25d34d7269bdda5c87d00..295d40341df325b3fb6f14164283863a if (!checkPosition(this, x, y, z)) { return; } -@@ -4817,6 +5124,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess +@@ -4819,6 +5126,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess @Override public final void setRemoved(Entity.RemovalReason removalReason, @Nullable org.bukkit.event.entity.EntityRemoveEvent.Cause cause) { // CraftBukkit - add Bukkit remove cause @@ -29449,7 +29449,7 @@ index 17d9e4699e8e2ce934c25d34d7269bdda5c87d00..295d40341df325b3fb6f14164283863a org.bukkit.craftbukkit.event.CraftEventFactory.callEntityRemoveEvent(this, cause); // CraftBukkit final boolean alreadyRemoved = this.removalReason != null; // Paper - Folia schedulers if (this.removalReason == null) { -@@ -4827,7 +5140,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess +@@ -4829,7 +5142,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess this.stopRiding(); } @@ -29458,7 +29458,7 @@ index 17d9e4699e8e2ce934c25d34d7269bdda5c87d00..295d40341df325b3fb6f14164283863a this.levelCallback.onRemove(removalReason); this.onRemoval(removalReason); // Paper start - Folia schedulers -@@ -4861,7 +5174,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess +@@ -4863,7 +5176,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess public boolean shouldBeSaved() { return (this.removalReason == null || this.removalReason.shouldSave()) && !this.isPassenger() diff --git a/paper-server/patches/features/0033-Optimise-EntityScheduler-ticking.patch b/paper-server/patches/features/0033-Optimise-EntityScheduler-ticking.patch index 351ea5724239..d3940b5d682e 100644 --- a/paper-server/patches/features/0033-Optimise-EntityScheduler-ticking.patch +++ b/paper-server/patches/features/0033-Optimise-EntityScheduler-ticking.patch @@ -67,10 +67,10 @@ index fd93b0c887420789276ebf92023d3e02b99cc1c9..9243bb11e3f968d0bf0eb2e3dc9295c0 io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.DIALOG_CLICK_MANAGER.handleQueue(this.tickCount); // Paper profilerFiller.push("commandFunctions"); diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java -index 295d40341df325b3fb6f14164283863a5a96b8bf..9a102b2c58446bd0aac5bd7f00e647f0270e7983 100644 +index b4392f9914d0a104d9e933c06ac72e5363510184..8e92d02c5a4b2040e9e351454d817409bfa1840b 100644 --- a/net/minecraft/world/entity/Entity.java +++ b/net/minecraft/world/entity/Entity.java -@@ -5164,6 +5164,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess +@@ -5166,6 +5166,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess this.getBukkitEntity().taskScheduler.retire(); } // Paper end - Folia schedulers diff --git a/paper-server/patches/sources/net/minecraft/server/commands/data/EntityDataAccessor.java.patch b/paper-server/patches/sources/net/minecraft/server/commands/data/EntityDataAccessor.java.patch new file mode 100644 index 000000000000..21dc83ec0c33 --- /dev/null +++ b/paper-server/patches/sources/net/minecraft/server/commands/data/EntityDataAccessor.java.patch @@ -0,0 +1,32 @@ +--- a/net/minecraft/server/commands/data/EntityDataAccessor.java ++++ b/net/minecraft/server/commands/data/EntityDataAccessor.java +@@ -4,7 +_,9 @@ + import com.mojang.brigadier.context.CommandContext; + import com.mojang.brigadier.exceptions.CommandSyntaxException; + import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; ++import com.mojang.serialization.Codec; + import com.mojang.logging.LogUtils; ++import java.util.List; + import java.util.Locale; + import java.util.UUID; + import java.util.function.Function; +@@ -39,6 +_,7 @@ + return builder.then(Commands.literal("entity").then(action.apply(Commands.argument(argumentName, EntityArgument.entity())))); + } + }; ++ private static final Codec> TAG_LIST_CODEC = Codec.STRING.sizeLimitedListOf(1024); // Paper - add ScoreboardTagsChangeEvent + private final Entity entity; + + public EntityDataAccessor(Entity entity) { +@@ -54,6 +_,11 @@ + + try (ProblemReporter.ScopedCollector scopedCollector = new ProblemReporter.ScopedCollector(this.entity.problemPath(), LOGGER)) { + this.entity.load(TagValueInput.create(scopedCollector, this.entity.registryAccess(), other)); ++ // Paper start - add ScoreboardTagsChangeEvent ++ other.read("Tags", TAG_LIST_CODEC).ifPresent(tags -> { ++ org.bukkit.craftbukkit.event.CraftEventFactory.callScoreboardTagsChangeEvent(this.entity, tags, io.papermc.paper.event.entity.ScoreboardTagsChangeEvent.Change.SET); ++ }); ++ // Paper end - add ScoreboardTagsChangeEvent + this.entity.setUUID(uuid); + } + } diff --git a/paper-server/patches/sources/net/minecraft/world/entity/Entity.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/Entity.java.patch index fca7e199321f..936a118c4d38 100644 --- a/paper-server/patches/sources/net/minecraft/world/entity/Entity.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/entity/Entity.java.patch @@ -197,16 +197,18 @@ public boolean isSpectator() { return false; } -@@ -362,7 +_,7 @@ +@@ -362,20 +_,28 @@ } public boolean addTag(String tag) { - return this.tags.size() < 1024 && this.tags.add(tag); ++ org.bukkit.craftbukkit.event.CraftEventFactory.callScoreboardTagsChangeEvent(this, tag, io.papermc.paper.event.entity.ScoreboardTagsChangeEvent.Change.ADD); // Paper - add ScoreboardTagsChangeEvent + return this.tags.add(tag); // Paper - fully limit tag size - replace set impl } public boolean removeTag(String tag) { -@@ -370,12 +_,18 @@ ++ org.bukkit.craftbukkit.event.CraftEventFactory.callScoreboardTagsChangeEvent(this, tag, io.papermc.paper.event.entity.ScoreboardTagsChangeEvent.Change.REMOVE); // Paper - add ScoreboardTagsChangeEvent + return this.tags.remove(tag); } public void kill(ServerLevel level) { @@ -848,7 +850,7 @@ Vec2 vec2 = input.read("Rotation", Vec2.CODEC).orElse(Vec2.ZERO); this.setDeltaMovement(Math.abs(vec31.x) > 10.0 ? 0.0 : vec31.x, Math.abs(vec31.y) > 10.0 ? 0.0 : vec31.y, Math.abs(vec31.z) > 10.0 ? 0.0 : vec31.z); this.hasImpulse = true; -@@ -1988,7 +_,20 @@ +@@ -1988,10 +_,23 @@ this.setNoGravity(input.getBooleanOr("NoGravity", false)); this.setGlowingTag(input.getBooleanOr("Glowing", false)); this.setTicksFrozen(input.getIntOr("TicksFrozen", 0)); @@ -869,7 +871,11 @@ + // Paper end this.customData = input.read("data", CustomData.CODEC).orElse(CustomData.EMPTY); this.tags.clear(); - input.read("Tags", TAG_LIST_CODEC).ifPresent(this.tags::addAll); +- input.read("Tags", TAG_LIST_CODEC).ifPresent(this.tags::addAll); ++ input.read("Tags", TAG_LIST_CODEC).ifPresent(tags::addAll); + this.readAdditionalSaveData(input); + if (this.repositionEntityAfterLoad()) { + this.reapplyPosition(); @@ -1999,6 +_,59 @@ } else { throw new IllegalStateException("Entity has invalid rotation");