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()) {
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());

View File

@ -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;
}

View File

@ -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

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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:

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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) {

View File

@ -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<String> observedPerms; // List of permissions used in conditions for a 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> 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<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.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<String, Boolean> 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,

View File

@ -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