Compare commits

...

3 Commits

Author SHA1 Message Date
rockyhawk64
4b1a7c87f8 use player scheduler 2025-10-12 02:10:52 +11:00
rockyhawk64
855334ce0a permission observer ability to disable added 2025-10-12 00:56:46 +11:00
rockyhawk64
cd127d229b add permission observer feature 2025-10-11 23:53:03 +11:00
15 changed files with 88 additions and 32 deletions

View File

@ -1,6 +1,6 @@
# |------------------------------------------------------------------------
# | CommandPanels Config File
# | By RockyHawk v7
# | By RockyHawk v8
# | https://www.spigotmc.org/resources/67788/
# |
# |------------------------------------------------------------------------
@ -11,5 +11,9 @@ custom-commands: true
# Amount of time in ticks players must wait between opening panels
cooldown-ticks: 5
# Will make console logs when panels are opened
panel-snooper: false
# Logs to console whenever a panel is opened
panel-snooper: false
# When enabled, panels will automatically refresh if any observed permissions change.
# Permissions used in HASPERM conditions are considered observed permissions.
permission-observer: true

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

@ -5,7 +5,6 @@ import io.papermc.paper.threadedregions.scheduler.ScheduledTask;
import me.rockyhawk.commandpanels.Context;
import me.rockyhawk.commandpanels.builder.inventory.InventoryPanelBuilder;
import me.rockyhawk.commandpanels.builder.inventory.items.ItemBuilder;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
@ -13,11 +12,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
@ -43,9 +48,8 @@ public class InventoryPanelUpdater {
ItemBuilder builder = new ItemBuilder(ctx, panelBuilder);
// Main update task
this.updateTask = Bukkit.getRegionScheduler().runAtFixedRate(
this.updateTask = p.getScheduler().runAtFixedRate(
ctx.plugin,
p.getLocation(),
(scheduledTask) -> {
Inventory inv = p.getOpenInventory().getTopInventory();
InventoryHolder holder = inv.getHolder();
@ -58,6 +62,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;
@ -90,22 +95,38 @@ public class InventoryPanelUpdater {
inv.setItem(slot, newItem);
}
},
null,
updateDelay,
updateDelay
);
final boolean isUsingPermObserver = ctx.fileHandler.config.getBoolean("permission-observer");
// Fast heartbeat check task, should run frequently
this.checkTask = Bukkit.getRegionScheduler().runAtFixedRate(
this.checkTask = p.getScheduler().runAtFixedRate(
ctx.plugin,
p.getLocation(),
(scheduledTask) -> {
Inventory inv = p.getOpenInventory().getTopInventory();
InventoryHolder holder = inv.getHolder();
if (!(holder instanceof InventoryPanel) || holder != panel) {
stop();
return;
}
// Permission Observer: Refresh if an observed perms state changes
if(!isUsingPermObserver) return; // Skip if disabled
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;
}
}
},
null,
2,
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