From cd127d229b09fd2630dd5e4c8fd17dd03e83fe5f Mon Sep 17 00:00:00 2001 From: rockyhawk64 Date: Sat, 11 Oct 2025 23:53:03 +1100 Subject: [PATCH] add permission observer feature --- .../builder/dialog/ActionBuilder.java | 2 +- .../builder/dialog/DialogPanelBuilder.java | 2 +- .../builder/floodgate/CustomForm.java | 2 +- .../builder/floodgate/SimpleForm.java | 2 +- .../builder/inventory/PanelFactory.java | 2 +- .../builder/logic/ComparisonNode.java | 4 ++- .../builder/logic/ConditionNode.java | 3 +- .../builder/logic/LogicalNode.java | 7 +++-- .../commandpanels/builder/logic/NotNode.java | 5 ++-- .../commands/requirements/ConditionTag.java | 6 ++-- .../commands/tags/RefreshPanelTag.java | 4 +-- .../commandpanels/session/Panel.java | 29 ++++++++++++++----- .../inventory/InventoryPanelUpdater.java | 19 ++++++++++++ .../inventory/listeners/ClickEvents.java | 11 +++++++ 14 files changed, 74 insertions(+), 24 deletions(-) diff --git a/src/me/rockyhawk/commandpanels/builder/dialog/ActionBuilder.java b/src/me/rockyhawk/commandpanels/builder/dialog/ActionBuilder.java index 7b6a0de..5a4b873 100644 --- a/src/me/rockyhawk/commandpanels/builder/dialog/ActionBuilder.java +++ b/src/me/rockyhawk/commandpanels/builder/dialog/ActionBuilder.java @@ -33,7 +33,7 @@ public class ActionBuilder implements Listener { if (!button.getConditions().trim().isEmpty()) { ConditionNode conditionNode = new ConditionParser().parse(button.getConditions()); - if (!conditionNode.evaluate(player, ctx)) return null; + if (!conditionNode.evaluate(player, panel, ctx)) return null; } Component name = ctx.text.parseTextToComponent(player, button.getName()); diff --git a/src/me/rockyhawk/commandpanels/builder/dialog/DialogPanelBuilder.java b/src/me/rockyhawk/commandpanels/builder/dialog/DialogPanelBuilder.java index ba6321d..c2f6631 100644 --- a/src/me/rockyhawk/commandpanels/builder/dialog/DialogPanelBuilder.java +++ b/src/me/rockyhawk/commandpanels/builder/dialog/DialogPanelBuilder.java @@ -61,7 +61,7 @@ public class DialogPanelBuilder extends PanelBuilder { // Check conditions for which component to use if (!comp.getConditions().trim().isEmpty()) { ConditionNode conditionNode = new ConditionParser().parse(comp.getConditions()); - boolean result = conditionNode.evaluate(player, ctx); + boolean result = conditionNode.evaluate(player, panel, ctx); if (!result) continue; } diff --git a/src/me/rockyhawk/commandpanels/builder/floodgate/CustomForm.java b/src/me/rockyhawk/commandpanels/builder/floodgate/CustomForm.java index aabd322..977bc9d 100644 --- a/src/me/rockyhawk/commandpanels/builder/floodgate/CustomForm.java +++ b/src/me/rockyhawk/commandpanels/builder/floodgate/CustomForm.java @@ -50,7 +50,7 @@ public class CustomForm { // Evaluate conditions if (!comp.getConditions().trim().isEmpty()) { ConditionNode conditionNode = new ConditionParser().parse(comp.getConditions()); - if (!conditionNode.evaluate(player, ctx)) continue; + if (!conditionNode.evaluate(player, panel, ctx)) continue; } // Create the component diff --git a/src/me/rockyhawk/commandpanels/builder/floodgate/SimpleForm.java b/src/me/rockyhawk/commandpanels/builder/floodgate/SimpleForm.java index e48b4fd..61946c8 100644 --- a/src/me/rockyhawk/commandpanels/builder/floodgate/SimpleForm.java +++ b/src/me/rockyhawk/commandpanels/builder/floodgate/SimpleForm.java @@ -48,7 +48,7 @@ public class SimpleForm { // Check conditions for which button to use in the slot if (!button.getConditions().trim().isEmpty()) { ConditionNode conditionNode = new ConditionParser().parse(button.getConditions()); - boolean result = conditionNode.evaluate(p, ctx); + boolean result = conditionNode.evaluate(p, panel, ctx); if (!result) continue; } diff --git a/src/me/rockyhawk/commandpanels/builder/inventory/PanelFactory.java b/src/me/rockyhawk/commandpanels/builder/inventory/PanelFactory.java index aa13c9d..3da3c17 100644 --- a/src/me/rockyhawk/commandpanels/builder/inventory/PanelFactory.java +++ b/src/me/rockyhawk/commandpanels/builder/inventory/PanelFactory.java @@ -63,7 +63,7 @@ public class PanelFactory { // Check conditions for which item to use in the slot if (!item.conditions().trim().isEmpty()) { ConditionNode conditionNode = new ConditionParser().parse(item.conditions()); - boolean result = conditionNode.evaluate(p, ctx); + boolean result = conditionNode.evaluate(p, panel, ctx); if (!result) continue; } diff --git a/src/me/rockyhawk/commandpanels/builder/logic/ComparisonNode.java b/src/me/rockyhawk/commandpanels/builder/logic/ComparisonNode.java index eabbce9..ef0981f 100644 --- a/src/me/rockyhawk/commandpanels/builder/logic/ComparisonNode.java +++ b/src/me/rockyhawk/commandpanels/builder/logic/ComparisonNode.java @@ -1,6 +1,7 @@ package me.rockyhawk.commandpanels.builder.logic; import me.rockyhawk.commandpanels.Context; +import me.rockyhawk.commandpanels.session.Panel; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; @@ -23,7 +24,7 @@ public class ComparisonNode implements ConditionNode { } @Override - public boolean evaluate(Player player, Context ctx) { + public boolean evaluate(Player player, Panel panel, Context ctx) { /* Do not parse placeholders of conditions before using this it will be handled internally Remove spaces from placeholders before parsing, so they can be compared with no spaces correctly @@ -48,6 +49,7 @@ public class ComparisonNode implements ConditionNode { return leftValue >= rightValue; case "$HASPERM": Player p = Bukkit.getPlayer(parsedLeft); + panel.addObservedPerm(parsedRight); if (p == null) return false; return p.hasPermission(parsedRight); default: diff --git a/src/me/rockyhawk/commandpanels/builder/logic/ConditionNode.java b/src/me/rockyhawk/commandpanels/builder/logic/ConditionNode.java index b97bb6c..fdd5634 100644 --- a/src/me/rockyhawk/commandpanels/builder/logic/ConditionNode.java +++ b/src/me/rockyhawk/commandpanels/builder/logic/ConditionNode.java @@ -1,8 +1,9 @@ package me.rockyhawk.commandpanels.builder.logic; import me.rockyhawk.commandpanels.Context; +import me.rockyhawk.commandpanels.session.Panel; import org.bukkit.entity.Player; public interface ConditionNode { - boolean evaluate(Player player, Context ctx); + boolean evaluate(Player player, Panel panel, Context ctx); } diff --git a/src/me/rockyhawk/commandpanels/builder/logic/LogicalNode.java b/src/me/rockyhawk/commandpanels/builder/logic/LogicalNode.java index 23cb1d7..330a60c 100644 --- a/src/me/rockyhawk/commandpanels/builder/logic/LogicalNode.java +++ b/src/me/rockyhawk/commandpanels/builder/logic/LogicalNode.java @@ -1,6 +1,7 @@ package me.rockyhawk.commandpanels.builder.logic; import me.rockyhawk.commandpanels.Context; +import me.rockyhawk.commandpanels.session.Panel; import org.bukkit.entity.Player; import java.util.List; @@ -15,12 +16,12 @@ public class LogicalNode implements ConditionNode { } @Override - public boolean evaluate(Player player, Context ctx) { + public boolean evaluate(Player player, Panel panel, Context ctx) { switch (operator) { case "$AND": - return conditions.stream().allMatch(cond -> cond.evaluate(player, ctx)); + return conditions.stream().allMatch(cond -> cond.evaluate(player, panel, ctx)); case "$OR": - return conditions.stream().anyMatch(cond -> cond.evaluate(player, ctx)); + return conditions.stream().anyMatch(cond -> cond.evaluate(player, panel, ctx)); default: return false; } diff --git a/src/me/rockyhawk/commandpanels/builder/logic/NotNode.java b/src/me/rockyhawk/commandpanels/builder/logic/NotNode.java index 37ad86d..3b005ce 100644 --- a/src/me/rockyhawk/commandpanels/builder/logic/NotNode.java +++ b/src/me/rockyhawk/commandpanels/builder/logic/NotNode.java @@ -1,6 +1,7 @@ package me.rockyhawk.commandpanels.builder.logic; import me.rockyhawk.commandpanels.Context; +import me.rockyhawk.commandpanels.session.Panel; import org.bukkit.entity.Player; public class NotNode implements ConditionNode { @@ -11,8 +12,8 @@ public class NotNode implements ConditionNode { } @Override - public boolean evaluate(Player player, Context ctx) { - return !child.evaluate(player, ctx); + public boolean evaluate(Player player, Panel panel, Context ctx) { + return !child.evaluate(player, panel, ctx); } } diff --git a/src/me/rockyhawk/commandpanels/interaction/commands/requirements/ConditionTag.java b/src/me/rockyhawk/commandpanels/interaction/commands/requirements/ConditionTag.java index bb154fd..23f0283 100644 --- a/src/me/rockyhawk/commandpanels/interaction/commands/requirements/ConditionTag.java +++ b/src/me/rockyhawk/commandpanels/interaction/commands/requirements/ConditionTag.java @@ -17,17 +17,17 @@ public class ConditionTag implements RequirementTagResolver { @Override public boolean check(Context ctx, Panel panel, Player player, String raw, String args) { - return parseCondition(ctx, player, args); + return parseCondition(ctx, panel, player, args); } @Override public void execute(Context ctx, Panel panel, Player player, String raw, String args) { } - private boolean parseCondition(Context ctx, Player player, String args) { + private boolean parseCondition(Context ctx, Panel panel, Player player, String args) { if (!args.trim().isEmpty()) { ConditionNode conditionNode = new ConditionParser().parse(args); - return conditionNode.evaluate(player, ctx); + return conditionNode.evaluate(player, panel, ctx); } return false; } diff --git a/src/me/rockyhawk/commandpanels/interaction/commands/tags/RefreshPanelTag.java b/src/me/rockyhawk/commandpanels/interaction/commands/tags/RefreshPanelTag.java index e88f31f..7d695ed 100644 --- a/src/me/rockyhawk/commandpanels/interaction/commands/tags/RefreshPanelTag.java +++ b/src/me/rockyhawk/commandpanels/interaction/commands/tags/RefreshPanelTag.java @@ -14,8 +14,8 @@ public class RefreshPanelTag implements CommandTagResolver { } /** - * Schedules refreshes as they need to happen after - * other actions such as permission changes + * Schedules refreshes so that it runs in the next tick + * other commands have a better chance of running first */ @Override public void handle(Context ctx, Panel panel, Player player, String raw, String command) { diff --git a/src/me/rockyhawk/commandpanels/session/Panel.java b/src/me/rockyhawk/commandpanels/session/Panel.java index ae50aa9..4bf49ed 100644 --- a/src/me/rockyhawk/commandpanels/session/Panel.java +++ b/src/me/rockyhawk/commandpanels/session/Panel.java @@ -12,12 +12,15 @@ import org.bukkit.entity.Player; import org.bukkit.persistence.PersistentDataContainer; import org.bukkit.persistence.PersistentDataType; +import java.util.ArrayList; import java.util.List; public abstract class Panel { private final String name; private final String title; private final String conditions; + + private final List observedPerms; // List of permissions used in conditions for a panel private final String command; // Command used to open the panel private final List aliases; // Aliases for command that opens the panel private final List commands; // Commands that run when panel is opened @@ -31,6 +34,7 @@ public abstract class Panel { this.aliases = config.getStringList("aliases"); this.commands = config.getStringList("commands"); this.type = config.getString("type", "inventory"); + this.observedPerms = new ArrayList<>(); } // Check run for permission checks with commands @@ -39,7 +43,7 @@ public abstract class Panel { if (this.conditions.trim().isEmpty()) return true; try { ConditionNode node = new ConditionParser().parse(this.conditions); - return node.evaluate(player, ctx); + return node.evaluate(player, this, ctx); } catch (Exception e) { return false; } @@ -48,10 +52,10 @@ public abstract class Panel { // Checks for opening fresh panels public boolean canOpen(Player p, Context ctx) { // Do not open if user is in cooldown period - NamespacedKey keyTime = new NamespacedKey(ctx.plugin, "last_open_time"); - Long lastOpen = p.getPersistentDataContainer().get(keyTime, PersistentDataType.LONG); - long cooldown = ctx.fileHandler.config.getInt("cooldown-ticks") * 50L; // ticks in config, converted to millis - if (lastOpen != null && System.currentTimeMillis() - lastOpen < cooldown) { + NamespacedKey keyTime = new NamespacedKey(ctx.plugin, "last_open_tick"); + Integer lastOpenTick = p.getPersistentDataContainer().get(keyTime, PersistentDataType.INTEGER); + int cooldownTicks = ctx.fileHandler.config.getInt("cooldown-ticks"); + if (lastOpenTick != null && Bukkit.getCurrentTick() - lastOpenTick < cooldownTicks) { ctx.text.sendError(p, Message.COOLDOWN_ERROR); return false; } @@ -65,11 +69,11 @@ public abstract class Panel { public void updatePanelData(Context ctx, Player p) { NamespacedKey keyCurrent = new NamespacedKey(ctx.plugin, "current"); NamespacedKey keyPrevious = new NamespacedKey(ctx.plugin, "previous"); - NamespacedKey keyTime = new NamespacedKey(ctx.plugin, "last_open_time"); + NamespacedKey keyTick = new NamespacedKey(ctx.plugin, "last_open_tick"); PersistentDataContainer container = p.getPersistentDataContainer(); // Time the player last opened any panel - container.set(keyTime, PersistentDataType.LONG, System.currentTimeMillis()); + container.set(keyTick, PersistentDataType.INTEGER, Bukkit.getCurrentTick()); // Move current → previous String current = container.get(keyCurrent, PersistentDataType.STRING); @@ -106,4 +110,15 @@ public abstract class Panel { public String getTitle() { return title; } + + /** + * Observed permissions are permissions that are found from HASPERM in panels + * They will allow panels to auto refresh if their state ever changes + */ + public List getObservedPerms() { + return observedPerms; + } + public void addObservedPerm(String node) { + observedPerms.add(node); + } } \ No newline at end of file diff --git a/src/me/rockyhawk/commandpanels/session/inventory/InventoryPanelUpdater.java b/src/me/rockyhawk/commandpanels/session/inventory/InventoryPanelUpdater.java index ff81e48..00e165e 100644 --- a/src/me/rockyhawk/commandpanels/session/inventory/InventoryPanelUpdater.java +++ b/src/me/rockyhawk/commandpanels/session/inventory/InventoryPanelUpdater.java @@ -13,11 +13,17 @@ import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.ItemStack; import org.bukkit.persistence.PersistentDataType; +import java.util.HashMap; +import java.util.Map; + public class InventoryPanelUpdater { private ScheduledTask checkTask; private ScheduledTask updateTask; + // List of permission states for observed permissions + private final Map lastObservedPermStates = new HashMap<>(); + /** * Panel updater will maintain itself with a checkTask that will end the updater * If it finds the panel has been closed it will end the updater tasks @@ -58,6 +64,7 @@ public class InventoryPanelUpdater { NamespacedKey baseIdKey = new NamespacedKey(ctx.plugin, "base_item_id"); NamespacedKey fillItem = new NamespacedKey(ctx.plugin, "fill_item"); + // Loop through items in the panel and update their state for (int slot = 0; slot < inv.getSize(); slot++) { ItemStack item = inv.getItem(slot); if (item == null || item.getType().isAir()) continue; @@ -104,6 +111,18 @@ public class InventoryPanelUpdater { if (!(holder instanceof InventoryPanel) || holder != panel) { stop(); + return; + } + + // Do a refresh if an observed perms state changes + for (String node : panel.getObservedPerms()) { + boolean currentState = p.hasPermission(node); + Boolean previousState = lastObservedPermStates.get(node); + lastObservedPermStates.put(node, currentState); + if (previousState != null && previousState != currentState) { + panel.open(ctx, p, false); + return; + } } }, 2, diff --git a/src/me/rockyhawk/commandpanels/session/inventory/listeners/ClickEvents.java b/src/me/rockyhawk/commandpanels/session/inventory/listeners/ClickEvents.java index b82bd03..e3a8d17 100644 --- a/src/me/rockyhawk/commandpanels/session/inventory/listeners/ClickEvents.java +++ b/src/me/rockyhawk/commandpanels/session/inventory/listeners/ClickEvents.java @@ -1,11 +1,13 @@ package me.rockyhawk.commandpanels.session.inventory.listeners; import me.rockyhawk.commandpanels.Context; +import me.rockyhawk.commandpanels.formatter.language.Message; import me.rockyhawk.commandpanels.interaction.commands.CommandRunner; import me.rockyhawk.commandpanels.interaction.commands.RequirementRunner; import me.rockyhawk.commandpanels.session.ClickActions; import me.rockyhawk.commandpanels.session.inventory.InventoryPanel; import me.rockyhawk.commandpanels.session.inventory.PanelItem; +import org.bukkit.Bukkit; import org.bukkit.NamespacedKey; import org.bukkit.entity.Player; import org.bukkit.event.Event; @@ -51,6 +53,15 @@ public class ClickEvents implements Listener { e.setCancelled(true); e.setResult(Event.Result.DENY); + // Do not run commands if user is in cooldown (item click cooldown should match heartbeat updater speed) + NamespacedKey lastClickTick = new NamespacedKey(ctx.plugin, "last_click_tick"); + Integer lastOpen = player.getPersistentDataContainer().get(lastClickTick, PersistentDataType.INTEGER); + int currentTick = Bukkit.getCurrentTick(); + if (lastOpen != null && currentTick - lastOpen < 2) { + return; + } + + player.getPersistentDataContainer().set(lastClickTick, PersistentDataType.INTEGER, currentTick); String itemId = container.get(baseIdKey, PersistentDataType.STRING); // Check valid interaction types