Compare commits

...

4 Commits

Author SHA1 Message Date
rockyhawk64
732995f2f2 command and teleport tag fix folia 2025-10-12 14:31:00 +11:00
rockyhawk64
7d27cc133b permission observer should not stop when updater is off 2025-10-12 13:35:48 +11:00
rockyhawk64
334856a421 outside commands feature 2025-10-12 11:38:20 +11:00
rockyhawk64
8b80ab0ed3 update data file when players leave and join 2025-10-12 10:51:42 +11:00
8 changed files with 115 additions and 76 deletions

View File

@ -1,5 +1,5 @@
name: CommandPanels
version: 4.1.0
version: 4.1.1
api-version: 1.21.10
main: me.rockyhawk.commandpanels.CommandPanels

View File

@ -70,7 +70,10 @@ public class CommandRunner {
}
// Run the command
runCommand(panel, player, command);
Bukkit.getGlobalRegionScheduler().run(
ctx.plugin,
task -> runCommand(panel, player, command)
);
// Move to the next command
runCommands(panel, player, commands, index + 1);

View File

@ -47,7 +47,7 @@ public class TeleportTag implements CommandTagResolver {
}
Location teleportLocation = new Location(teleportedWorld, x, y, z, yaw, pitch);
if (teleportedPlayer != null) {
teleportedPlayer.teleport(teleportLocation);
player.teleportAsync(teleportLocation);
}
} catch (Exception ex) {
ctx.text.sendError(player, Message.TELEPORT_ERROR);

View File

@ -52,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_tick");
Integer lastOpenTick = p.getPersistentDataContainer().get(keyTime, PersistentDataType.INTEGER);
int cooldownTicks = ctx.fileHandler.config.getInt("cooldown-ticks");
if (lastOpenTick != null && Bukkit.getCurrentTick() - lastOpenTick < cooldownTicks) {
NamespacedKey keyTime = new NamespacedKey(ctx.plugin, "last_open_time");
Long lastOpenTime = p.getPersistentDataContainer().get(keyTime, PersistentDataType.LONG);
long cooldownMillis = ctx.fileHandler.config.getLong("cooldown-ticks") * 50L;
if (lastOpenTime != null && System.currentTimeMillis() - lastOpenTime < cooldownMillis) {
ctx.text.sendError(p, Message.COOLDOWN_ERROR);
return false;
}
@ -69,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 keyTick = new NamespacedKey(ctx.plugin, "last_open_tick");
NamespacedKey keyMillis = new NamespacedKey(ctx.plugin, "last_open_time");
PersistentDataContainer container = p.getPersistentDataContainer();
// Time the player last opened any panel
container.set(keyTick, PersistentDataType.INTEGER, Bukkit.getCurrentTick());
container.set(keyMillis, PersistentDataType.LONG, System.currentTimeMillis());
// Move current previous
String current = container.get(keyCurrent, PersistentDataType.STRING);

View File

@ -15,14 +15,20 @@ public class SessionDataUtils implements Listener {
this.ctx = ctx;
}
/**
* On player Join and Leave,
* Remove Session data and do an async save of the data file
*/
@EventHandler
public void onJoinEvent(PlayerJoinEvent e) {
removeSessionData(e.getPlayer());
ctx.dataLoader.saveDataFileAsync();
}
@EventHandler
public void onQuitEvent(PlayerQuitEvent e) {
removeSessionData(e.getPlayer());
ctx.dataLoader.saveDataFileAsync();
}
private void removeSessionData(Player p){

View File

@ -4,6 +4,7 @@ import me.rockyhawk.commandpanels.Context;
import me.rockyhawk.commandpanels.builder.PanelBuilder;
import me.rockyhawk.commandpanels.builder.inventory.InventoryPanelBuilder;
import me.rockyhawk.commandpanels.interaction.commands.CommandRunner;
import me.rockyhawk.commandpanels.session.ClickActions;
import me.rockyhawk.commandpanels.session.Panel;
import me.rockyhawk.commandpanels.session.floodgate.FloodgatePanel;
import org.bukkit.Bukkit;
@ -22,6 +23,7 @@ public class InventoryPanel extends Panel implements InventoryHolder {
private final String rows;
private final Map<String, PanelItem> items = new HashMap<>();
private final Map<String, List<String>> slots = new HashMap<>();
private final ClickActions outside;
private final String floodgate;
private final String updateDelay;
@ -32,6 +34,12 @@ public class InventoryPanel extends Panel implements InventoryHolder {
this.floodgate = config.getString("floodgate", "");
this.updateDelay = config.getString("update-delay", "20");
outside = new ClickActions(
config.getStringList("outside.requirements"),
config.getStringList("outside.commands"),
config.getStringList("outside.fail")
);
ConfigurationSection slotSection = config.getConfigurationSection("layout");
if (slotSection != null) {
for (String key : slotSection.getKeys(false)) {
@ -104,9 +112,14 @@ public class InventoryPanel extends Panel implements InventoryHolder {
return updateDelay;
}
public ClickActions getOutsideCommands() {
return outside;
}
// For InventoryHolder implementation
@Override
public Inventory getInventory() {
return null;
}
}

View File

@ -17,52 +17,72 @@ import java.util.Map;
public class InventoryPanelUpdater {
private ScheduledTask checkTask;
private ScheduledTask heartbeatTask;
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
*/
public void start(Context ctx, Player p, InventoryPanel panel) {
// Stop existing tasks if any
stop();
stop(); // always clean slate
// Determine update delay
int updateDelay = 20;
if (panel.getUpdateDelay().matches("\\d+")) {
updateDelay = Integer.parseInt(panel.getUpdateDelay());
startHeartbeat(ctx, p, panel);
int updateDelay = parseUpdateDelay(panel.getUpdateDelay());
if (updateDelay > 0) {
startUpdater(ctx, p, panel, updateDelay);
}
}
// If update delay is 0 then do not run the updater
if (updateDelay == 0) {
this.updateTask = null;
return;
}
private void startHeartbeat(Context ctx, Player p, InventoryPanel panel) {
final boolean isUsingPermObserver = ctx.fileHandler.config.getBoolean("permission-observer");
InventoryPanelBuilder panelBuilder = new InventoryPanelBuilder(ctx, p);
ItemBuilder builder = new ItemBuilder(ctx, panelBuilder);
// Main update task
this.updateTask = p.getScheduler().runAtFixedRate(
heartbeatTask = p.getScheduler().runAtFixedRate(
ctx.plugin,
(scheduledTask) -> {
(task) -> {
Inventory inv = p.getOpenInventory().getTopInventory();
InventoryHolder holder = inv.getHolder();
// Stop everything if the panel is closed
if (!(holder instanceof InventoryPanel) || holder != panel) {
stop();
return;
}
// Handle permission observer
if (!isUsingPermObserver) return;
for (String node : panel.getObservedPerms()) {
boolean current = p.hasPermission(node);
Boolean previous = lastObservedPermStates.put(node, current);
if (previous != null && previous != current) {
panel.open(ctx, p, false);
return;
}
}
},
null,
2,
2
);
}
private void startUpdater(Context ctx, Player p, InventoryPanel panel, int updateDelay) {
InventoryPanelBuilder panelBuilder = new InventoryPanelBuilder(ctx, p);
ItemBuilder builder = new ItemBuilder(ctx, panelBuilder);
NamespacedKey itemIdKey = new NamespacedKey(ctx.plugin, "item_id");
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
updateTask = p.getScheduler().runAtFixedRate(
ctx.plugin,
(task) -> {
Inventory inv = p.getOpenInventory().getTopInventory();
InventoryHolder holder = inv.getHolder();
if (!(holder instanceof InventoryPanel) || holder != panel) {
stopUpdater(); // only stop this task, heartbeat may continue
return;
}
for (int slot = 0; slot < inv.getSize(); slot++) {
ItemStack item = inv.getItem(slot);
if (item == null || item.getType().isAir()) continue;
@ -99,44 +119,28 @@ public class InventoryPanelUpdater {
updateDelay,
updateDelay
);
final boolean isUsingPermObserver = ctx.fileHandler.config.getBoolean("permission-observer");
// Fast heartbeat check task, should run frequently
this.checkTask = p.getScheduler().runAtFixedRate(
ctx.plugin,
(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;
private int parseUpdateDelay(String delayStr) {
if (delayStr != null && delayStr.matches("\\d+")) {
return Integer.parseInt(delayStr);
}
}
},
null,
2,
2
);
return 20; // default
}
public void stop() {
if (checkTask != null) {
checkTask.cancel();
checkTask = null;
stopHeartbeat();
stopUpdater();
}
private void stopHeartbeat() {
if (heartbeatTask != null) {
heartbeatTask.cancel();
heartbeatTask = null;
}
}
private void stopUpdater() {
if (updateTask != null) {
updateTask.cancel();
updateTask = null;

View File

@ -1,7 +1,6 @@
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;
@ -33,6 +32,20 @@ public class ClickEvents implements Listener {
requirements = new RequirementRunner(ctx);
}
@EventHandler
public void onOutsideInventoryClick(InventoryClickEvent e) {
if (!(e.getWhoClicked() instanceof Player player)) return;
if (e.getClickedInventory() != null) return;
if (!(player.getOpenInventory().getTopInventory().getHolder() instanceof InventoryPanel panel)) return;
ClickActions actions = panel.getOutsideCommands();
if(!requirements.processRequirements(panel, player, actions.requirements())){
commands.runCommands(panel, player, actions.fail());
return;
}
commands.runCommands(panel, player, actions.commands());
}
@EventHandler
public void onInventoryClick(InventoryClickEvent e) {
if (!(e.getWhoClicked() instanceof Player player)) return;
@ -54,14 +67,14 @@ public class ClickEvents implements Listener {
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) {
NamespacedKey lastClick = new NamespacedKey(ctx.plugin, "last_click_time");
Long lastOpenMillis = player.getPersistentDataContainer().get(lastClick, PersistentDataType.LONG);
long currentMillis = System.currentTimeMillis();
if (lastOpenMillis != null && currentMillis - lastOpenMillis < 100L) {
return;
}
player.getPersistentDataContainer().set(lastClickTick, PersistentDataType.INTEGER, currentTick);
player.getPersistentDataContainer().set(lastClick, PersistentDataType.LONG, currentMillis);
String itemId = container.get(baseIdKey, PersistentDataType.STRING);
// Check valid interaction types