add permission observer feature

This commit is contained in:
rockyhawk64 2025-10-11 23:53:03 +11:00
parent dc61d9155f
commit cd127d229b
14 changed files with 74 additions and 24 deletions

View File

@ -33,7 +33,7 @@ public class ActionBuilder implements Listener {
if (!button.getConditions().trim().isEmpty()) { if (!button.getConditions().trim().isEmpty()) {
ConditionNode conditionNode = new ConditionParser().parse(button.getConditions()); 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()); Component name = ctx.text.parseTextToComponent(player, button.getName());

View File

@ -61,7 +61,7 @@ public class DialogPanelBuilder extends PanelBuilder {
// Check conditions for which component to use // Check conditions for which component to use
if (!comp.getConditions().trim().isEmpty()) { if (!comp.getConditions().trim().isEmpty()) {
ConditionNode conditionNode = new ConditionParser().parse(comp.getConditions()); ConditionNode conditionNode = new ConditionParser().parse(comp.getConditions());
boolean result = conditionNode.evaluate(player, ctx); boolean result = conditionNode.evaluate(player, panel, ctx);
if (!result) continue; if (!result) continue;
} }

View File

@ -50,7 +50,7 @@ public class CustomForm {
// Evaluate conditions // Evaluate conditions
if (!comp.getConditions().trim().isEmpty()) { if (!comp.getConditions().trim().isEmpty()) {
ConditionNode conditionNode = new ConditionParser().parse(comp.getConditions()); ConditionNode conditionNode = new ConditionParser().parse(comp.getConditions());
if (!conditionNode.evaluate(player, ctx)) continue; if (!conditionNode.evaluate(player, panel, ctx)) continue;
} }
// Create the component // Create the component

View File

@ -48,7 +48,7 @@ public class SimpleForm {
// Check conditions for which button to use in the slot // Check conditions for which button to use in the slot
if (!button.getConditions().trim().isEmpty()) { if (!button.getConditions().trim().isEmpty()) {
ConditionNode conditionNode = new ConditionParser().parse(button.getConditions()); ConditionNode conditionNode = new ConditionParser().parse(button.getConditions());
boolean result = conditionNode.evaluate(p, ctx); boolean result = conditionNode.evaluate(p, panel, ctx);
if (!result) continue; if (!result) continue;
} }

View File

@ -63,7 +63,7 @@ public class PanelFactory {
// Check conditions for which item to use in the slot // Check conditions for which item to use in the slot
if (!item.conditions().trim().isEmpty()) { if (!item.conditions().trim().isEmpty()) {
ConditionNode conditionNode = new ConditionParser().parse(item.conditions()); ConditionNode conditionNode = new ConditionParser().parse(item.conditions());
boolean result = conditionNode.evaluate(p, ctx); boolean result = conditionNode.evaluate(p, panel, ctx);
if (!result) continue; if (!result) continue;
} }

View File

@ -1,6 +1,7 @@
package me.rockyhawk.commandpanels.builder.logic; package me.rockyhawk.commandpanels.builder.logic;
import me.rockyhawk.commandpanels.Context; import me.rockyhawk.commandpanels.Context;
import me.rockyhawk.commandpanels.session.Panel;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
@ -23,7 +24,7 @@ public class ComparisonNode implements ConditionNode {
} }
@Override @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 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 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; return leftValue >= rightValue;
case "$HASPERM": case "$HASPERM":
Player p = Bukkit.getPlayer(parsedLeft); Player p = Bukkit.getPlayer(parsedLeft);
panel.addObservedPerm(parsedRight);
if (p == null) return false; if (p == null) return false;
return p.hasPermission(parsedRight); return p.hasPermission(parsedRight);
default: default:

View File

@ -1,8 +1,9 @@
package me.rockyhawk.commandpanels.builder.logic; package me.rockyhawk.commandpanels.builder.logic;
import me.rockyhawk.commandpanels.Context; import me.rockyhawk.commandpanels.Context;
import me.rockyhawk.commandpanels.session.Panel;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
public interface ConditionNode { public interface ConditionNode {
boolean evaluate(Player player, Context ctx); boolean evaluate(Player player, Panel panel, Context ctx);
} }

View File

@ -1,6 +1,7 @@
package me.rockyhawk.commandpanels.builder.logic; package me.rockyhawk.commandpanels.builder.logic;
import me.rockyhawk.commandpanels.Context; import me.rockyhawk.commandpanels.Context;
import me.rockyhawk.commandpanels.session.Panel;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import java.util.List; import java.util.List;
@ -15,12 +16,12 @@ public class LogicalNode implements ConditionNode {
} }
@Override @Override
public boolean evaluate(Player player, Context ctx) { public boolean evaluate(Player player, Panel panel, Context ctx) {
switch (operator) { switch (operator) {
case "$AND": case "$AND":
return conditions.stream().allMatch(cond -> cond.evaluate(player, ctx)); return conditions.stream().allMatch(cond -> cond.evaluate(player, panel, ctx));
case "$OR": case "$OR":
return conditions.stream().anyMatch(cond -> cond.evaluate(player, ctx)); return conditions.stream().anyMatch(cond -> cond.evaluate(player, panel, ctx));
default: default:
return false; return false;
} }

View File

@ -1,6 +1,7 @@
package me.rockyhawk.commandpanels.builder.logic; package me.rockyhawk.commandpanels.builder.logic;
import me.rockyhawk.commandpanels.Context; import me.rockyhawk.commandpanels.Context;
import me.rockyhawk.commandpanels.session.Panel;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
public class NotNode implements ConditionNode { public class NotNode implements ConditionNode {
@ -11,8 +12,8 @@ public class NotNode implements ConditionNode {
} }
@Override @Override
public boolean evaluate(Player player, Context ctx) { public boolean evaluate(Player player, Panel panel, Context ctx) {
return !child.evaluate(player, ctx); return !child.evaluate(player, panel, ctx);
} }
} }

View File

@ -17,17 +17,17 @@ public class ConditionTag implements RequirementTagResolver {
@Override @Override
public boolean check(Context ctx, Panel panel, Player player, String raw, String args) { 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 @Override
public void execute(Context ctx, Panel panel, Player player, String raw, String args) { 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()) { if (!args.trim().isEmpty()) {
ConditionNode conditionNode = new ConditionParser().parse(args); ConditionNode conditionNode = new ConditionParser().parse(args);
return conditionNode.evaluate(player, ctx); return conditionNode.evaluate(player, panel, ctx);
} }
return false; return false;
} }

View File

@ -14,8 +14,8 @@ public class RefreshPanelTag implements CommandTagResolver {
} }
/** /**
* Schedules refreshes as they need to happen after * Schedules refreshes so that it runs in the next tick
* other actions such as permission changes * other commands have a better chance of running first
*/ */
@Override @Override
public void handle(Context ctx, Panel panel, Player player, String raw, String command) { public void handle(Context ctx, Panel panel, Player player, String raw, String command) {

View File

@ -12,12 +12,15 @@ import org.bukkit.entity.Player;
import org.bukkit.persistence.PersistentDataContainer; import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType; import org.bukkit.persistence.PersistentDataType;
import java.util.ArrayList;
import java.util.List; import java.util.List;
public abstract class Panel { public abstract class Panel {
private final String name; private final String name;
private final String title; private final String title;
private final String conditions; private final String conditions;
private final List<String> observedPerms; // List of permissions used in conditions for a panel
private final String command; // Command used to open the panel private final String command; // Command used to open the panel
private final List<String> aliases; // Aliases for command that opens the panel private final List<String> aliases; // Aliases for command that opens the panel
private final List<String> commands; // Commands that run when panel is opened private final List<String> commands; // Commands that run when panel is opened
@ -31,6 +34,7 @@ public abstract class Panel {
this.aliases = config.getStringList("aliases"); this.aliases = config.getStringList("aliases");
this.commands = config.getStringList("commands"); this.commands = config.getStringList("commands");
this.type = config.getString("type", "inventory"); this.type = config.getString("type", "inventory");
this.observedPerms = new ArrayList<>();
} }
// Check run for permission checks with commands // Check run for permission checks with commands
@ -39,7 +43,7 @@ public abstract class Panel {
if (this.conditions.trim().isEmpty()) return true; if (this.conditions.trim().isEmpty()) return true;
try { try {
ConditionNode node = new ConditionParser().parse(this.conditions); ConditionNode node = new ConditionParser().parse(this.conditions);
return node.evaluate(player, ctx); return node.evaluate(player, this, ctx);
} catch (Exception e) { } catch (Exception e) {
return false; return false;
} }
@ -48,10 +52,10 @@ public abstract class Panel {
// Checks for opening fresh panels // Checks for opening fresh panels
public boolean canOpen(Player p, Context ctx) { public boolean canOpen(Player p, Context ctx) {
// Do not open if user is in cooldown period // Do not open if user is in cooldown period
NamespacedKey keyTime = new NamespacedKey(ctx.plugin, "last_open_time"); NamespacedKey keyTime = new NamespacedKey(ctx.plugin, "last_open_tick");
Long lastOpen = p.getPersistentDataContainer().get(keyTime, PersistentDataType.LONG); Integer lastOpenTick = p.getPersistentDataContainer().get(keyTime, PersistentDataType.INTEGER);
long cooldown = ctx.fileHandler.config.getInt("cooldown-ticks") * 50L; // ticks in config, converted to millis int cooldownTicks = ctx.fileHandler.config.getInt("cooldown-ticks");
if (lastOpen != null && System.currentTimeMillis() - lastOpen < cooldown) { if (lastOpenTick != null && Bukkit.getCurrentTick() - lastOpenTick < cooldownTicks) {
ctx.text.sendError(p, Message.COOLDOWN_ERROR); ctx.text.sendError(p, Message.COOLDOWN_ERROR);
return false; return false;
} }
@ -65,11 +69,11 @@ public abstract class Panel {
public void updatePanelData(Context ctx, Player p) { public void updatePanelData(Context ctx, Player p) {
NamespacedKey keyCurrent = new NamespacedKey(ctx.plugin, "current"); NamespacedKey keyCurrent = new NamespacedKey(ctx.plugin, "current");
NamespacedKey keyPrevious = new NamespacedKey(ctx.plugin, "previous"); 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(); PersistentDataContainer container = p.getPersistentDataContainer();
// Time the player last opened any panel // Time the player last opened any panel
container.set(keyTime, PersistentDataType.LONG, System.currentTimeMillis()); container.set(keyTick, PersistentDataType.INTEGER, Bukkit.getCurrentTick());
// Move current previous // Move current previous
String current = container.get(keyCurrent, PersistentDataType.STRING); String current = container.get(keyCurrent, PersistentDataType.STRING);
@ -106,4 +110,15 @@ public abstract class Panel {
public String getTitle() { public String getTitle() {
return title; 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<String> getObservedPerms() {
return observedPerms;
}
public void addObservedPerm(String node) {
observedPerms.add(node);
}
} }

View File

@ -13,11 +13,17 @@ import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataType; import org.bukkit.persistence.PersistentDataType;
import java.util.HashMap;
import java.util.Map;
public class InventoryPanelUpdater { public class InventoryPanelUpdater {
private ScheduledTask checkTask; private ScheduledTask checkTask;
private ScheduledTask updateTask; private ScheduledTask updateTask;
// List of permission states for observed permissions
private final Map<String, Boolean> lastObservedPermStates = new HashMap<>();
/** /**
* Panel updater will maintain itself with a checkTask that will end the updater * 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 * 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 baseIdKey = new NamespacedKey(ctx.plugin, "base_item_id");
NamespacedKey fillItem = new NamespacedKey(ctx.plugin, "fill_item"); 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++) { for (int slot = 0; slot < inv.getSize(); slot++) {
ItemStack item = inv.getItem(slot); ItemStack item = inv.getItem(slot);
if (item == null || item.getType().isAir()) continue; if (item == null || item.getType().isAir()) continue;
@ -104,6 +111,18 @@ public class InventoryPanelUpdater {
if (!(holder instanceof InventoryPanel) || holder != panel) { if (!(holder instanceof InventoryPanel) || holder != panel) {
stop(); 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, 2,

View File

@ -1,11 +1,13 @@
package me.rockyhawk.commandpanels.session.inventory.listeners; package me.rockyhawk.commandpanels.session.inventory.listeners;
import me.rockyhawk.commandpanels.Context; 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.CommandRunner;
import me.rockyhawk.commandpanels.interaction.commands.RequirementRunner; import me.rockyhawk.commandpanels.interaction.commands.RequirementRunner;
import me.rockyhawk.commandpanels.session.ClickActions; import me.rockyhawk.commandpanels.session.ClickActions;
import me.rockyhawk.commandpanels.session.inventory.InventoryPanel; import me.rockyhawk.commandpanels.session.inventory.InventoryPanel;
import me.rockyhawk.commandpanels.session.inventory.PanelItem; import me.rockyhawk.commandpanels.session.inventory.PanelItem;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey; import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.Event; import org.bukkit.event.Event;
@ -51,6 +53,15 @@ public class ClickEvents implements Listener {
e.setCancelled(true); e.setCancelled(true);
e.setResult(Event.Result.DENY); 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); String itemId = container.get(baseIdKey, PersistentDataType.STRING);
// Check valid interaction types // Check valid interaction types