From be637390c1fecb7088d4f56ee051e13834c914c1 Mon Sep 17 00:00:00 2001 From: JRoy <10731363+JRoy@users.noreply.github.com> Date: Sat, 4 Apr 2026 22:39:22 -0700 Subject: [PATCH 1/5] Add disable-item-pickup-while-vanished config option Prevents vanished players from picking up items, which could reveal their presence. Defaults to true. Follows the same pattern as the existing disable-item-pickup-while-afk option. Fixes #6270 --- .../essentials/EssentialsPlayerListener.java | 16 ++++++++++------ .../java/com/earth2me/essentials/ISettings.java | 2 ++ .../java/com/earth2me/essentials/Settings.java | 11 +++++++++++ Essentials/src/main/resources/config.yml | 4 ++++ 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java b/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java index c1fb775d7b4..0a390227be7 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java +++ b/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java @@ -1213,10 +1213,12 @@ private final class PickupListenerPre1_12 implements Listener { public void onPlayerPickupItem(final org.bukkit.event.player.PlayerPickupItemEvent event) { if (event.getItem().hasMetadata(Commandfireball.FIREBALL_META_KEY)) { event.setCancelled(true); - } else if (ess.getSettings().getDisableItemPickupWhileAfk()) { - if (ess.getUser(event.getPlayer()).isAfk()) { - event.setCancelled(true); - } + return; + } + final User user = ess.getUser(event.getPlayer()); + if ((ess.getSettings().getDisableItemPickupWhileAfk() && user.isAfk()) + || (ess.getSettings().getDisableItemPickupWhileVanished() && user.isVanished())) { + event.setCancelled(true); } } } @@ -1224,8 +1226,10 @@ public void onPlayerPickupItem(final org.bukkit.event.player.PlayerPickupItemEve private final class PickupListener1_12 implements Listener { @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true) public void onPlayerPickupItem(final org.bukkit.event.entity.EntityPickupItemEvent event) { - if (ess.getSettings().getDisableItemPickupWhileAfk() && event.getEntity() instanceof Player) { - if (ess.getUser((Player) event.getEntity()).isAfk()) { + if (event.getEntity() instanceof Player) { + final User user = ess.getUser((Player) event.getEntity()); + if ((ess.getSettings().getDisableItemPickupWhileAfk() && user.isAfk()) + || (ess.getSettings().getDisableItemPickupWhileVanished() && user.isVanished())) { event.setCancelled(true); } } diff --git a/Essentials/src/main/java/com/earth2me/essentials/ISettings.java b/Essentials/src/main/java/com/earth2me/essentials/ISettings.java index 0ba2d7d9c14..aa6d88bdaf9 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/ISettings.java +++ b/Essentials/src/main/java/com/earth2me/essentials/ISettings.java @@ -259,6 +259,8 @@ public interface ISettings extends IConf { boolean getDisableItemPickupWhileAfk(); + boolean getDisableItemPickupWhileVanished(); + EventPriority getRespawnPriority(); EventPriority getSpawnJoinPriority(); diff --git a/Essentials/src/main/java/com/earth2me/essentials/Settings.java b/Essentials/src/main/java/com/earth2me/essentials/Settings.java index 8d41da405e7..1951f581c8a 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/Settings.java +++ b/Essentials/src/main/java/com/earth2me/essentials/Settings.java @@ -115,6 +115,7 @@ public class Settings implements net.ess3.api.ISettings { private Set noGodWorlds = new HashSet<>(); private boolean registerBackInListener; private boolean disableItemPickupWhileAfk; + private boolean disableItemPickupWhileVanished; private long teleportInvulnerabilityTime; private boolean teleportInvulnerability; private long loginAttackDelay; @@ -803,6 +804,7 @@ public void reloadConfig() { teleportInvulnerabilityTime = _getTeleportInvulnerability(); teleportInvulnerability = _isTeleportInvulnerability(); disableItemPickupWhileAfk = _getDisableItemPickupWhileAfk(); + disableItemPickupWhileVanished = _getDisableItemPickupWhileVanished(); registerBackInListener = _registerBackInListener(); cancelAfkOnInteract = _cancelAfkOnInteract(); cancelAfkOnMove = _cancelAfkOnMove(); @@ -1438,6 +1440,15 @@ private boolean _getDisableItemPickupWhileAfk() { return config.getBoolean("disable-item-pickup-while-afk", false); } + @Override + public boolean getDisableItemPickupWhileVanished() { + return disableItemPickupWhileVanished; + } + + private boolean _getDisableItemPickupWhileVanished() { + return config.getBoolean("disable-item-pickup-while-vanished", true); + } + private EventPriority getPriority(final String priority) { if ("none".equals(priority)) { return null; diff --git a/Essentials/src/main/resources/config.yml b/Essentials/src/main/resources/config.yml index 5c87444d9fb..38f59b4a052 100644 --- a/Essentials/src/main/resources/config.yml +++ b/Essentials/src/main/resources/config.yml @@ -516,6 +516,10 @@ freeze-afk-players: false # Enable this if you want to prevent people from idling in mob traps. disable-item-pickup-while-afk: false +# When a player is vanished, should they be able to pick up items? +# Picking up items while vanished can reveal the player's presence. +disable-item-pickup-while-vanished: true + # This setting controls if a player is marked as active upon interaction. cancel-afk-on-interact: true From b39ac8272f6df483ea74882b1b6b1470a45d0f68 Mon Sep 17 00:00:00 2001 From: JRoy <10731363+JRoy@users.noreply.github.com> Date: Sun, 5 Apr 2026 12:35:18 -0700 Subject: [PATCH 2/5] Create cached permission system --- .../java/com/earth2me/essentials/IUser.java | 2 ++ .../java/com/earth2me/essentials/User.java | 31 +++++++++++++++++++ .../essentials/perm/IPermissionsHandler.java | 4 +++ .../essentials/perm/PermissionsHandler.java | 5 +++ .../perm/impl/LuckPermsHandler.java | 31 +++++++++++++++++++ 5 files changed, 73 insertions(+) diff --git a/Essentials/src/main/java/com/earth2me/essentials/IUser.java b/Essentials/src/main/java/com/earth2me/essentials/IUser.java index a701eb5610f..bb427dcba16 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/IUser.java +++ b/Essentials/src/main/java/com/earth2me/essentials/IUser.java @@ -32,6 +32,8 @@ public interface IUser { boolean isAuthorized(String node); + boolean isAuthorizedCached(String node); + boolean isAuthorized(IEssentialsCommand cmd); boolean isAuthorized(IEssentialsCommand cmd, String permissionPrefix); diff --git a/Essentials/src/main/java/com/earth2me/essentials/User.java b/Essentials/src/main/java/com/earth2me/essentials/User.java index 36a2df5ffc1..67801e80615 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/User.java +++ b/Essentials/src/main/java/com/earth2me/essentials/User.java @@ -147,6 +147,19 @@ public boolean isAuthorized(final String node) { return result; } + @Override + public boolean isAuthorizedCached(final String node) { + if (Essentials.TESTING) { + return false; + } + + final boolean result = isAuthorizedCachedCheck(node); + if (ess.getSettings().isDebug()) { + ess.getLogger().log(Level.INFO, "checking if " + base.getName() + " has " + node + " (cached) - " + result); + } + return result; + } + @Override public boolean isPermissionSet(final String node) { if (Essentials.TESTING) { @@ -186,6 +199,24 @@ private boolean isAuthorizedCheck(final String node) { } } + private boolean isAuthorizedCachedCheck(final String node) { + if (base instanceof OfflinePlayerStub) { + return false; + } + + try { + return ess.getPermissionsHandler().hasPermissionCached(base, node); + } catch (final Exception ex) { + if (ess.getSettings().isDebug()) { + ess.getLogger().log(Level.SEVERE, "Permission System Error: " + ess.getPermissionsHandler().getName() + " returned: " + ex.getMessage(), ex); + } else { + ess.getLogger().log(Level.SEVERE, "Permission System Error: " + ess.getPermissionsHandler().getName() + " returned: " + ex.getMessage()); + } + + return false; + } + } + private boolean isPermSetCheck(final String node) { if (base instanceof OfflinePlayerStub) { return false; diff --git a/Essentials/src/main/java/com/earth2me/essentials/perm/IPermissionsHandler.java b/Essentials/src/main/java/com/earth2me/essentials/perm/IPermissionsHandler.java index 76d47a2e160..65406313eb1 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/perm/IPermissionsHandler.java +++ b/Essentials/src/main/java/com/earth2me/essentials/perm/IPermissionsHandler.java @@ -28,6 +28,10 @@ public interface IPermissionsHandler { boolean hasPermission(Player base, String node); + default boolean hasPermissionCached(Player base, String node) { + return hasPermission(base, node); + } + // Does not check for * permissions boolean isPermissionSet(Player base, String node); diff --git a/Essentials/src/main/java/com/earth2me/essentials/perm/PermissionsHandler.java b/Essentials/src/main/java/com/earth2me/essentials/perm/PermissionsHandler.java index 0cbad14775b..37b3767bae0 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/perm/PermissionsHandler.java +++ b/Essentials/src/main/java/com/earth2me/essentials/perm/PermissionsHandler.java @@ -105,6 +105,11 @@ public boolean hasPermission(final Player base, final String node) { return handler.hasPermission(base, node); } + @Override + public boolean hasPermissionCached(final Player base, final String node) { + return handler.hasPermissionCached(base, node); + } + @Override public boolean isPermissionSet(final Player base, final String node) { return handler.isPermissionSet(base, node); diff --git a/Essentials/src/main/java/com/earth2me/essentials/perm/impl/LuckPermsHandler.java b/Essentials/src/main/java/com/earth2me/essentials/perm/impl/LuckPermsHandler.java index 1dc9418e75b..dca4c425af0 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/perm/impl/LuckPermsHandler.java +++ b/Essentials/src/main/java/com/earth2me/essentials/perm/impl/LuckPermsHandler.java @@ -7,17 +7,22 @@ import net.luckperms.api.context.ContextConsumer; import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.ImmutableContextSet; +import net.luckperms.api.event.EventSubscription; +import net.luckperms.api.event.user.UserDataRecalculateEvent; import net.luckperms.api.model.group.Group; import net.luckperms.api.query.QueryOptions; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.plugin.RegisteredServiceProvider; +import java.util.logging.Level; import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.function.Supplier; @@ -25,6 +30,8 @@ public class LuckPermsHandler extends ModernVaultHandler { private LuckPerms luckPerms; private Essentials ess; private CombinedCalculator calculator; + private final Map> permissionCache = new ConcurrentHashMap<>(); + private EventSubscription recalculateSubscription; @Override public void registerContext(final String context, final Function> calculator, final Supplier> suggestions) { @@ -41,6 +48,25 @@ public void unregisterContexts() { this.luckPerms.getContextManager().unregisterCalculator(this.calculator); this.calculator = null; } + if (this.recalculateSubscription != null) { + this.recalculateSubscription.close(); + this.recalculateSubscription = null; + } + this.permissionCache.clear(); + } + + @Override + public boolean hasPermissionCached(final Player base, final String node) { + final UUID uuid = base.getUniqueId(); + final Map userCache = permissionCache.computeIfAbsent(uuid, k -> new ConcurrentHashMap<>()); + return userCache.computeIfAbsent(node, k -> hasPermission(base, node)); + } + + public void invalidateCache(final UUID uuid) { + if (ess.getSettings().isDebug()) { + ess.getLogger().log(Level.INFO, "Invalidating permission cache for " + uuid); + } + permissionCache.remove(uuid); } @Override @@ -77,6 +103,11 @@ public boolean tryProvider(Essentials ess) { if (provider != null) { this.luckPerms = provider.getProvider(); this.ess = ess; + this.recalculateSubscription = this.luckPerms.getEventBus().subscribe( + ess, + UserDataRecalculateEvent.class, + event -> invalidateCache(event.getUser().getUniqueId()) + ); } return luckPerms != null && super.tryProvider(ess); } From 562e3fc9099b01b7ea30f085be2585e2f09f75e3 Mon Sep 17 00:00:00 2001 From: JRoy <10731363+JRoy@users.noreply.github.com> Date: Sun, 5 Apr 2026 12:35:35 -0700 Subject: [PATCH 3/5] Move essentials.afk.auto to cached check --- Essentials/src/main/java/com/earth2me/essentials/User.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Essentials/src/main/java/com/earth2me/essentials/User.java b/Essentials/src/main/java/com/earth2me/essentials/User.java index 67801e80615..9b039bf21e6 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/User.java +++ b/Essentials/src/main/java/com/earth2me/essentials/User.java @@ -903,7 +903,7 @@ public void checkActivity() { } } final long autoafk = ess.getSettings().getAutoAfk(); - if (!isAfk() && autoafk > 0 && lastActivity + autoafk * 1000 < System.currentTimeMillis() && isAuthorized("essentials.afk.auto")) { + if (!isAfk() && autoafk > 0 && lastActivity + autoafk * 1000 < System.currentTimeMillis() && isAuthorizedCached("essentials.afk.auto")) { setAfk(true, AfkStatusChangeEvent.Cause.ACTIVITY); if (isAfk() && !isHidden()) { setDisplayNick(); From d4d180442315b6b5035d94e41d0f7698380eb052 Mon Sep 17 00:00:00 2001 From: JRoy <10731363+JRoy@users.noreply.github.com> Date: Sun, 5 Apr 2026 12:51:11 -0700 Subject: [PATCH 4/5] pickup as perm --- .../earth2me/essentials/EssentialsPlayerListener.java | 4 ++-- .../main/java/com/earth2me/essentials/ISettings.java | 2 -- .../main/java/com/earth2me/essentials/Settings.java | 11 ----------- Essentials/src/main/resources/config.yml | 4 ---- Essentials/src/main/resources/plugin.yml | 3 +++ 5 files changed, 5 insertions(+), 19 deletions(-) diff --git a/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java b/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java index 019fed7a463..6345a7c6a56 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java +++ b/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java @@ -1234,7 +1234,7 @@ public void onPlayerPickupItem(final org.bukkit.event.player.PlayerPickupItemEve } final User user = ess.getUser(event.getPlayer()); if ((ess.getSettings().getDisableItemPickupWhileAfk() && user.isAfk()) - || (ess.getSettings().getDisableItemPickupWhileVanished() && user.isVanished())) { + || (user.isVanished() && !user.isAuthorizedCached("essentials.vanish.pickup"))) { event.setCancelled(true); } } @@ -1246,7 +1246,7 @@ public void onPlayerPickupItem(final org.bukkit.event.entity.EntityPickupItemEve if (event.getEntity() instanceof Player) { final User user = ess.getUser((Player) event.getEntity()); if ((ess.getSettings().getDisableItemPickupWhileAfk() && user.isAfk()) - || (ess.getSettings().getDisableItemPickupWhileVanished() && user.isVanished())) { + || (user.isVanished() && !user.isAuthorizedCached("essentials.vanish.pickup"))) { event.setCancelled(true); } } diff --git a/Essentials/src/main/java/com/earth2me/essentials/ISettings.java b/Essentials/src/main/java/com/earth2me/essentials/ISettings.java index 706fb8648ac..857dd0ce226 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/ISettings.java +++ b/Essentials/src/main/java/com/earth2me/essentials/ISettings.java @@ -261,8 +261,6 @@ public interface ISettings extends IConf { boolean getDisableItemPickupWhileAfk(); - boolean getDisableItemPickupWhileVanished(); - EventPriority getRespawnPriority(); EventPriority getSpawnJoinPriority(); diff --git a/Essentials/src/main/java/com/earth2me/essentials/Settings.java b/Essentials/src/main/java/com/earth2me/essentials/Settings.java index 59c6666fb21..136a448ba5d 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/Settings.java +++ b/Essentials/src/main/java/com/earth2me/essentials/Settings.java @@ -115,7 +115,6 @@ public class Settings implements net.ess3.api.ISettings { private Set noGodWorlds = new HashSet<>(); private boolean registerBackInListener; private boolean disableItemPickupWhileAfk; - private boolean disableItemPickupWhileVanished; private long teleportInvulnerabilityTime; private boolean teleportInvulnerability; private long loginAttackDelay; @@ -804,7 +803,6 @@ public void reloadConfig() { teleportInvulnerabilityTime = _getTeleportInvulnerability(); teleportInvulnerability = _isTeleportInvulnerability(); disableItemPickupWhileAfk = _getDisableItemPickupWhileAfk(); - disableItemPickupWhileVanished = _getDisableItemPickupWhileVanished(); registerBackInListener = _registerBackInListener(); cancelAfkOnInteract = _cancelAfkOnInteract(); cancelAfkOnMove = _cancelAfkOnMove(); @@ -1445,15 +1443,6 @@ private boolean _getDisableItemPickupWhileAfk() { return config.getBoolean("disable-item-pickup-while-afk", false); } - @Override - public boolean getDisableItemPickupWhileVanished() { - return disableItemPickupWhileVanished; - } - - private boolean _getDisableItemPickupWhileVanished() { - return config.getBoolean("disable-item-pickup-while-vanished", true); - } - private EventPriority getPriority(final String priority) { if ("none".equals(priority)) { return null; diff --git a/Essentials/src/main/resources/config.yml b/Essentials/src/main/resources/config.yml index 8b0b5d3b020..beb886dd867 100644 --- a/Essentials/src/main/resources/config.yml +++ b/Essentials/src/main/resources/config.yml @@ -516,10 +516,6 @@ freeze-afk-players: false # Enable this if you want to prevent people from idling in mob traps. disable-item-pickup-while-afk: false -# When a player is vanished, should they be able to pick up items? -# Picking up items while vanished can reveal the player's presence. -disable-item-pickup-while-vanished: true - # This setting controls if a player is marked as active upon interaction. cancel-afk-on-interact: true diff --git a/Essentials/src/main/resources/plugin.yml b/Essentials/src/main/resources/plugin.yml index 4c578304f3e..b18e7f06f6c 100644 --- a/Essentials/src/main/resources/plugin.yml +++ b/Essentials/src/main/resources/plugin.yml @@ -1374,6 +1374,9 @@ permissions: description: Applies invisibility effects to the player when they are in vanish mode essentials.vanish.interact: description: Allows the bearer to interact with players in vanish mode + essentials.vanish.pickup: + description: Allows the bearer to pick up items while in vanish mode + default: false essentials.version: description: Allows access to the /version command essentials.warp: From 7ea24720b133d9da0d86e49b0dea2e17c6b73267 Mon Sep 17 00:00:00 2001 From: JRoy <10731363+JRoy@users.noreply.github.com> Date: Sun, 5 Apr 2026 13:02:22 -0700 Subject: [PATCH 5/5] invalidate during command list send --- .../com/earth2me/essentials/EssentialsPlayerListener.java | 4 ++++ .../com/earth2me/essentials/perm/IPermissionsHandler.java | 3 +++ .../com/earth2me/essentials/perm/PermissionsHandler.java | 5 +++++ .../com/earth2me/essentials/perm/impl/LuckPermsHandler.java | 5 +++++ 4 files changed, 17 insertions(+) diff --git a/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java b/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java index 6345a7c6a56..c0c46d909dc 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java +++ b/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java @@ -1265,6 +1265,10 @@ public void onGameEvent(final org.bukkit.event.block.BlockReceiveGameEvent event private final class CommandSendFilter implements CommandSendListenerProvider.Filter { @Override public Predicate apply(Player player) { + // There is no event for op status changes, but the command list is resent when + // a player is opped/deopped, so we invalidate cached permissions here. + ess.getPermissionsHandler().invalidatePermissionCache(player.getUniqueId()); + final User user = ess.getUser(player); final Set checked = new HashSet<>(); final Set toRemove = new HashSet<>(); diff --git a/Essentials/src/main/java/com/earth2me/essentials/perm/IPermissionsHandler.java b/Essentials/src/main/java/com/earth2me/essentials/perm/IPermissionsHandler.java index 65406313eb1..6b440918050 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/perm/IPermissionsHandler.java +++ b/Essentials/src/main/java/com/earth2me/essentials/perm/IPermissionsHandler.java @@ -32,6 +32,9 @@ default boolean hasPermissionCached(Player base, String node) { return hasPermission(base, node); } + default void invalidatePermissionCache(UUID uuid) { + } + // Does not check for * permissions boolean isPermissionSet(Player base, String node); diff --git a/Essentials/src/main/java/com/earth2me/essentials/perm/PermissionsHandler.java b/Essentials/src/main/java/com/earth2me/essentials/perm/PermissionsHandler.java index 37b3767bae0..b5c15cb051a 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/perm/PermissionsHandler.java +++ b/Essentials/src/main/java/com/earth2me/essentials/perm/PermissionsHandler.java @@ -110,6 +110,11 @@ public boolean hasPermissionCached(final Player base, final String node) { return handler.hasPermissionCached(base, node); } + @Override + public void invalidatePermissionCache(final UUID uuid) { + handler.invalidatePermissionCache(uuid); + } + @Override public boolean isPermissionSet(final Player base, final String node) { return handler.isPermissionSet(base, node); diff --git a/Essentials/src/main/java/com/earth2me/essentials/perm/impl/LuckPermsHandler.java b/Essentials/src/main/java/com/earth2me/essentials/perm/impl/LuckPermsHandler.java index dca4c425af0..a3d1d0143ca 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/perm/impl/LuckPermsHandler.java +++ b/Essentials/src/main/java/com/earth2me/essentials/perm/impl/LuckPermsHandler.java @@ -62,6 +62,11 @@ public boolean hasPermissionCached(final Player base, final String node) { return userCache.computeIfAbsent(node, k -> hasPermission(base, node)); } + @Override + public void invalidatePermissionCache(final UUID uuid) { + invalidateCache(uuid); + } + public void invalidateCache(final UUID uuid) { if (ess.getSettings().isDebug()) { ess.getLogger().log(Level.INFO, "Invalidating permission cache for " + uuid);