A way for other plugins to exempt entities from cleanup

This commit is contained in:
NotAlexNoyle committed 2026-07-29 16:51:02 -04:00
1 parent 67cd86c532
commit 352621e2ba
4 files changed
+82 -2

No files matched your search

+39
View File
@@ -26,6 +26,41 @@ BadPylot, NotAlexNoyle.
- See how much time is left until entity cleanup with an in-game command (/cleanupin).
- Let other plugins exempt their own items and experience orbs from despawning and cleanup.
# Protecting another plugin's entities
Minigame objectives and similar plugin-owned items are ordinary dropped items, so entity cleanup and /clearentities delete them out from under the plugin that spawned them. To exempt one, tag it with the `nodespawn-og:protect` key:
```java
// Do this once, e.g. in a static field.
private static final NamespacedKey PROTECT = Objects.requireNonNull(NamespacedKey.fromString("nodespawn-og:protect"));
// Tag the entity as soon as it exists.
Item item = world.dropItem(location, itemStack);
item.getPersistentDataContainer().set(PROTECT, PersistentDataType.BYTE, (byte) 1);
```
## Tagging rules
- **Tag the entity, not the ItemStack.** NoDespawn-OG reads `Item#getPersistentDataContainer()`, which is the entity's container. A key written to the stack's `ItemMeta` is a different container and has no effect.
- **Use `PersistentDataType.BYTE` with the value `1`.** Any other value, type, or a missing key means unprotected.
- **Tag immediately after spawning.** `World#dropItem` returns the entity; tag it before returning control to the server so no despawn or cleanup pass can see it untagged.
- **Re-tag every new entity.** The tag lives on the entity, not the item. Once a player picks the item up the entity is gone, so a plugin that re-drops the item must tag the new entity too.
- **`ExperienceOrb` works the same way**, using the same key on the orb entity.
- **The tag persists.** It is stored in entity NBT, so it survives chunk unload and server restarts.
## Effect
A tagged entity is skipped by the periodic entity cleanup, by /clearentities, and by despawning: its `ItemDespawnEvent` is cancelled and it is never given despawn timing data. It lives until something removes it, so **the owning plugin is responsible for removing its own protected entities**. To give one back to NoDespawn-OG, remove the key (`container.remove(PROTECT)`) or set it to `(byte) 0`.
Death piles are unaffected by this key; they keep using their existing `disable-death-despawns` and death-pile-protection behavior.
# Permissions
Clear dropped entities with a command (/clearentities):
@@ -46,6 +81,10 @@ Running the plugin on your server requires [Utilities-OG](https://github.com/tru
# Changelog
*Version 4.1.1:*
- Items and experience orbs tagged `nodespawn-og:protect` by another plugin are now exempt from despawning, entity cleanup, and /clearentities.
*Version 4.1:*
- Death pile experience orbs now inherit the same pile ID, owner, and despawn timing as the items in their death pile.
+1 -1
View File
@@ -27,7 +27,7 @@ kotlin { jvmToolchain(17) }
/* ----------------------------- Metadata ------------------------------ */
group = "net.trueog.nodespawn-og" // Declare bundle identifier.
version = "4.1" // Declare plugin version (will be in .jar).
version = "4.1.1" // Declare plugin version (will be in .jar).
val apiVersion = "1.19" // Declare minecraft server target version.
@@ -36,6 +36,7 @@ public class EntityCleanupScheduler extends BukkitRunnable {
private final Set<Integer> warningTicks = new HashSet<>();
private final NamespacedKey keyDeath = new NamespacedKey(NoDespawnOG.getPlugin(), "nodespawn_death");
private final NamespacedKey keyExpireAt = new NamespacedKey(NoDespawnOG.getPlugin(), "nodespawn_expire_at");
private final NamespacedKey keyProtect = new NamespacedKey(NoDespawnOG.getPlugin(), "protect");
public EntityCleanupScheduler() {
@@ -325,6 +326,16 @@ public class EntityCleanupScheduler extends BukkitRunnable {
}
// Entities another plugin tagged nodespawn-og:protect are off limits to
// cleanup.
private boolean isProtectedEntity(Entity entity) {
final Byte protectFlag = entity.getPersistentDataContainer().get(keyProtect, PersistentDataType.BYTE);
return protectFlag != null && protectFlag.byteValue() == (byte) 1;
}
private void scheduleVanillaDespawnCheck(Entity entity, long now, long expireAt) {
if (!entity.isValid()) {
@@ -414,7 +425,7 @@ public class EntityCleanupScheduler extends BukkitRunnable {
}
if (!isDeathPileEntity(entity)) {
if (!isDeathPileEntity(entity) && !isProtectedEntity(entity)) {
entity.remove();
@@ -39,6 +39,10 @@ public class EventListener implements Listener {
private final NamespacedKey keyCreated = new NamespacedKey(NoDespawnOG.getPlugin(), "nodespawn_created");
private final NamespacedKey keyExpireAt = new NamespacedKey(NoDespawnOG.getPlugin(), "nodespawn_expire_at");
// Other plugins tag their own items/orbs with nodespawn-og:protect to opt them
// out of despawning and cleanup entirely (minigame objectives and the like).
private final NamespacedKey keyProtect = new NamespacedKey(NoDespawnOG.getPlugin(), "protect");
private static final int VANILLA_DESPAWN_TICKS = 6000;
private static final long TICK_MS = 50L;
@@ -66,6 +70,17 @@ public class EventListener implements Listener {
public void onItemDespawn(ItemDespawnEvent event) {
final Item item = event.getEntity();
if (isProtectedEntity(item)) {
event.setCancelled(true);
item.setTicksLived(1);
return;
}
final long expireAt = getExpireAt(item);
if (expireAt == 0L) {
@@ -212,6 +227,14 @@ public class EventListener implements Listener {
}
private boolean isProtectedEntity(Entity entity) {
final Byte flag = entity.getPersistentDataContainer().get(keyProtect, PersistentDataType.BYTE);
return Byte.valueOf((byte) 1).equals(flag);
}
private boolean isDeathPileItem(Item item) {
return isDeathPileEntity(item);
@@ -323,6 +346,13 @@ public class EventListener implements Listener {
private void ensureExpireData(Entity entity) {
// Protected entities are never given despawn data, so nothing ages them out.
if (isProtectedEntity(entity)) {
return;
}
final long now = System.currentTimeMillis();
long expireAt = getExpireAt(entity);
if (expireAt != 0L) {