Compare commits

...

9 Commits

Author SHA1 Message Date
rockyhawk64
589d9cc046 4.1.0 2025-10-06 22:25:03 +11:00
rockyhawk64
733e7396dd update readme 2025-10-06 21:29:41 +11:00
rockyhawk64
580dd3e0ef update readme 2025-10-06 21:28:22 +11:00
rockyhawk64
4172e4866e update readme 2025-10-06 21:24:00 +11:00
rockyhawk64
ba489b1128 simplify and sort tab complete panel list 2025-10-06 20:43:09 +11:00
rockyhawk64
01c242ac3b update version nums 2025-10-06 20:13:27 +11:00
rockyhawk64
df90aba890 cooldown config variable added 2025-10-06 20:11:35 +11:00
rockyhawk64
e4cacd5e4e bug fixes and cooldown added 2025-10-06 16:46:04 +11:00
rockyhawk64
1001d8b6b8 clear session data on player leave 2025-10-06 11:49:49 +11:00
19 changed files with 212 additions and 74 deletions

107
README.md
View File

@ -1,61 +1,114 @@
![TitleLogo](https://commandpanels.net/resource_images/main_logo.png) ![TitleLogo](https://commandpanels.net/resource_images/main_logo.png)
## Trusted GUI Plugin since 2019 ## 🏆 Trusted GUI Plugin since 2019
CommandPanels makes it easy to create interactive menus without coding. CommandPanels isn't like other menu plugins it's a **GUI framework**. it gives you true dynamic control through **logic**, **data**, and **panel variety**.
From shops and quests to server tools, you can design custom GUIs with YAML or the online editor. From shops and quests to custom tools and admin panels, you can design powerful GUIs using YAML or the online editor.
- ✅ Actively maintained for 6+ years - ✅ Actively maintained for 6+ years
- 🖼️ Online editor for faster menu building - 🖼️ Online editor for rapid menu building
- 🌍 Cross-platform support (Java + Bedrock) - 🧠 Advanced logic and data support
- 📘 Full docs and community Discord - 🌍 Cross-platform (Java + Bedrock)
- 📘 Full documentation and community Discord
---
## Useful Links ## Useful Links
### [📘 Documentation](https://docs.commandpanels.net) - [📘 **Documentation**](https://docs.commandpanels.net)
### [🛠️ Online Editor](https://commandpanels.net/editor) - [🛠️ **Online Editor**](https://commandpanels.net/editor)
### [💬 Join Discord](https://discord.gg/WFQMTZxa53) - [💬 **Discord**](https://discord.gg/WFQMTZxa53)
---
## Online Editor ## Online Editor
The online editor includes support for all three panel types. This means you can visually create and edit Dialog, Inventory, and Floodgate panels with ease, previews are not 1:1 but will significantly speed up development and testing.
The online editor includes support for **all three panel types**.
Its not just a basic form builder, its a **live YAML editor** with structure checks and visual previews, helping you work faster and avoid YAML errors.
![ExampleScreenshot](https://commandpanels.net/resource_images/example_editor.png) ![ExampleScreenshot](https://commandpanels.net/resource_images/example_editor.png)
- ⚡ Visual layout builder with slot highlighting
- 🧹 Automatic indentation & structure checks
- ✅ Works with Inventory, Dialog, and Floodgate panels
- 🧠 Supports complex panels, not just simple menus
---
## Real Logic & Real Data
CommandPanels lets you build menus that **react to players**, not just display static items.
- Inline `$AND`, `$OR`, `$NOT` operators and grouping
- Multiple items in a single slot with logical fallbacks
- **Persistent** or **session-based data** usable anywhere, even in other plugins via PlaceholderAPI
**Example Condition:**
~~~
conditions: "$NOT (%player_name% $EQUALS Steve) $AND %vault_eco_balance% $ATLEAST 5000"
~~~
This allows for **powerful, dynamic behavior** without scripting.
---
## GUI Types ## GUI Types
### Inventory Panels *(can be used for complex GUI creation)* ### Inventory Panels
Create fully interactive GUIs for shops, kits, navigation menus, or custom tools.
![RawInventory](https://commandpanels.net/resource_images/raw_inventory.webp) ![RawInventory](https://commandpanels.net/resource_images/raw_inventory.webp)
### Dialog Panels *(can be used for custom interfaces and requesting player input)*
---
### Dialog Panels
Build structured, custom interfaces that can be used to request custom input.
![RawDialog](https://commandpanels.net/resource_images/raw_dialog.webp) ![RawDialog](https://commandpanels.net/resource_images/raw_dialog.webp)
### Floodgate Panels *(can be used for Bedrock players on your server)*
---
### Floodgate Panels
Bring **full GUI support to Bedrock players** using Geyser and Floodgate.
CommandPanels is one of the only plugins that natively supports this.
![RawFloodgate](https://commandpanels.net/resource_images/raw_floodgate.webp) ![RawFloodgate](https://commandpanels.net/resource_images/raw_floodgate.webp)
---
## About ## About
Minecraft servers use graphical interfaces (GUIs) for a wide range of features,
from server lobbies and player shops to quests and custom tools. CommandPanels provides a
streamlined YAML scripting format designed to make GUI creation both accessible and highly customizable.
With built-in support for variables, persistent data, placeholders, Minecraft servers rely on GUIs for everything from shops and lobbies to quests and server tools.
conditions, and more, you can design interactive menus tailored to your server's needs without complex programming. CommandPanels provides a **streamlined YAML scripting format** designed to make GUI creation both **accessible** and **deeply customizable**.
CommandPanels supports three distinct panel types, each with unique capabilities: - Inline logic & conditions
- Dynamic placeholders
- Inventory Panels allow you to build item-based interfaces where players can interact with and manipulate items directly. - Persistent & session data
- Dialog Panels are perfect for creating structured, menu-based conversations and navigation systems, ideal for quests or guided menus. - Full PlaceholderAPI support
- Floodgate Panels enable full GUI support for Bedrock Edition players through Geyser and Floodgate, ensuring cross-platform compatibility. - Modern, clean codebase
Developed in Australia 🇦🇺 Developed in Australia 🇦🇺
---
## Server Compatibility ## Server Compatibility
This plugin is a **Paper** plugin, it will only work on Paper and its forks. CommandPanels is fully compatible with **Paper** and **Folia** servers.
---
## Our Partner ## Our Partner
![ParterLogo](https://commandpanels.net/resource_images/partner_logo.png) ![ParterLogo](https://commandpanels.net/resource_images/partner_logo.png)
We have proudly partnered with [ReviveNode](http://billing.revivenode.com/aff.php?aff=379)! We have proudly partnered with [ReviveNode](http://billing.revivenode.com/aff.php?aff=379)!
CommandPanels users have been offered 15% off on the first month by using the Promocode: **PANELS** CommandPanels users get **15% off their first month** with the promo code: **PANELS**
ReviveNode is a leading Minecraft hosting provider that offers affordable and high-quality server hosting solutions. ReviveNode provides high-performance, reliable Minecraft hosting, making them the perfect partner for CommandPanels.
Their focus on performance, reliability, and customer support makes them the perfect partner for CommandPanels.
---
## Trusted by Servers Worldwide
For over 6 years, CommandPanels has powered **thousands of Minecraft servers**.
Whether you're running survival, MMO, minigames, or custom networks, it can handle it.

View File

@ -1,6 +1,6 @@
# |------------------------------------------------------------------------ # |------------------------------------------------------------------------
# | CommandPanels Dialog File # | CommandPanels Dialog File
# | Official Panel v2.0 # | Official Panel v2
# | https://www.spigotmc.org/resources/command-panels-custom-guis.67788/ # | https://www.spigotmc.org/resources/command-panels-custom-guis.67788/
# |------------------------------------------------------------------------ # |------------------------------------------------------------------------
conditions: '%player_name% $HASPERM example.permission' conditions: '%player_name% $HASPERM example.permission'

View File

@ -1,6 +1,6 @@
# |------------------------------------------------------------------------ # |------------------------------------------------------------------------
# | CommandPanels Custom Floodgate File # | CommandPanels Custom Floodgate File
# | Official Panel v2.0 # | Official Panel v2
# | https://www.spigotmc.org/resources/command-panels-custom-guis.67788/ # | https://www.spigotmc.org/resources/command-panels-custom-guis.67788/
# |------------------------------------------------------------------------ # |------------------------------------------------------------------------
conditions: '%player_name% $HASPERM example.permission' conditions: '%player_name% $HASPERM example.permission'

View File

@ -1,6 +1,6 @@
# |------------------------------------------------------------------------ # |------------------------------------------------------------------------
# | CommandPanels Simple Floodgate File # | CommandPanels Simple Floodgate File
# | Official Panel v2.0 # | Official Panel v2
# | https://www.spigotmc.org/resources/command-panels-custom-guis.67788/ # | https://www.spigotmc.org/resources/command-panels-custom-guis.67788/
# |------------------------------------------------------------------------ # |------------------------------------------------------------------------
conditions: "%player_name% $HASPERM example.permission" conditions: "%player_name% $HASPERM example.permission"

View File

@ -1,6 +1,6 @@
# |------------------------------------------------------------------------ # |------------------------------------------------------------------------
# | CommandPanels Inventory File # | CommandPanels Inventory File
# | Official Panel v2.0 # | Official Panel v2
# | https://www.spigotmc.org/resources/command-panels-custom-guis.67788/ # | https://www.spigotmc.org/resources/command-panels-custom-guis.67788/
# |------------------------------------------------------------------------ # |------------------------------------------------------------------------

View File

@ -1,6 +1,6 @@
# |------------------------------------------------------------------------ # |------------------------------------------------------------------------
# | CommandPanels Config File # | CommandPanels Config File
# | By RockyHawk v6.0 # | By RockyHawk v7
# | https://www.spigotmc.org/resources/67788/ # | https://www.spigotmc.org/resources/67788/
# | # |
# |------------------------------------------------------------------------ # |------------------------------------------------------------------------
@ -8,5 +8,8 @@
# When disabled, custom commands to open panels will not be registered # When disabled, custom commands to open panels will not be registered
custom-commands: true custom-commands: true
# Will make console logs when panels are opened and closed # 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 panel-snooper: false

View File

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

View File

@ -5,6 +5,7 @@ import me.rockyhawk.commandpanels.formatter.Placeholders;
import me.rockyhawk.commandpanels.formatter.language.TextFormatter; import me.rockyhawk.commandpanels.formatter.language.TextFormatter;
import me.rockyhawk.commandpanels.formatter.data.DataLoader; import me.rockyhawk.commandpanels.formatter.data.DataLoader;
import me.rockyhawk.commandpanels.interaction.openpanel.PanelOpenCommand; import me.rockyhawk.commandpanels.interaction.openpanel.PanelOpenCommand;
import me.rockyhawk.commandpanels.session.SessionDataUtils;
import me.rockyhawk.commandpanels.session.inventory.generator.GenerateManager; import me.rockyhawk.commandpanels.session.inventory.generator.GenerateManager;
import me.rockyhawk.commandpanels.session.inventory.listeners.ClickEvents; import me.rockyhawk.commandpanels.session.inventory.listeners.ClickEvents;
import me.rockyhawk.commandpanels.session.inventory.listeners.InventoryEvents; import me.rockyhawk.commandpanels.session.inventory.listeners.InventoryEvents;
@ -38,6 +39,7 @@ public class Context {
Bukkit.getServer().getPluginManager().registerEvents(new InventoryEvents(this), plugin); Bukkit.getServer().getPluginManager().registerEvents(new InventoryEvents(this), plugin);
Bukkit.getServer().getPluginManager().registerEvents(panelCommand, plugin); Bukkit.getServer().getPluginManager().registerEvents(panelCommand, plugin);
Bukkit.getServer().getPluginManager().registerEvents(new ClickEvents(this), plugin); Bukkit.getServer().getPluginManager().registerEvents(new ClickEvents(this), plugin);
Bukkit.getServer().getPluginManager().registerEvents(new SessionDataUtils(this), plugin);
Bukkit.getServer().getPluginManager().registerEvents(generator, plugin); Bukkit.getServer().getPluginManager().registerEvents(generator, plugin);
// Register proxy channels // Register proxy channels

View File

@ -45,15 +45,11 @@ public class TabComplete {
if (args.length == 2) { if (args.length == 2) {
if (args[0].equalsIgnoreCase("open") && sender.hasPermission("commandpanels.command.open")) { if (args[0].equalsIgnoreCase("open") && sender.hasPermission("commandpanels.command.open")) {
if (sender instanceof Player player) { String prefix = args[1].toLowerCase();
for (String panelName : ctx.plugin.panels.keySet()) { for (String panelName : ctx.plugin.panels.keySet()) {
Panel panel = ctx.plugin.panels.get(panelName); if (panelName.toLowerCase().contains(prefix)) {
if (panel.canOpen(player, ctx)) { output.add(panelName);
output.add(panelName);
}
} }
} else {
output.addAll(ctx.plugin.panels.keySet());
} }
} else if (args[0].equalsIgnoreCase("data") && sender.hasPermission("commandpanels.command.data")) { } else if (args[0].equalsIgnoreCase("data") && sender.hasPermission("commandpanels.command.data")) {
output.addAll(Arrays.asList("get", "set", "overwrite", "math", "del", "clear")); output.addAll(Arrays.asList("get", "set", "overwrite", "math", "del", "clear"));

View File

@ -51,7 +51,7 @@ public class OpenCommand implements SubCommand {
} }
} }
if(sender instanceof Player && !panel.canOpen((Player) sender, ctx)){ if(sender instanceof Player && !panel.passesConditions((Player) sender, ctx)){
ctx.text.sendError(sender, Message.COMMAND_NO_PERMISSION); ctx.text.sendError(sender, Message.COMMAND_NO_PERMISSION);
return true; return true;
} }

View File

@ -111,6 +111,7 @@ public enum Message {
// Misc // Misc
DIALOG_NO_BUTTONS("Dialog needs at least one button"), DIALOG_NO_BUTTONS("Dialog needs at least one button"),
COOLDOWN_ERROR("You're opening panels too quickly"),
TELEPORT_ERROR("Error with teleport tag"), TELEPORT_ERROR("Error with teleport tag"),
REQUIRE_HEADDATABASE("Download the HeadDatabase plugin to use this feature!"); REQUIRE_HEADDATABASE("Download the HeadDatabase plugin to use this feature!");

View File

@ -39,9 +39,7 @@ public class CommandRunner {
} }
public void runCommands(Panel panel, Player player, List<String> commands) { public void runCommands(Panel panel, Player player, List<String> commands) {
// Keep command execution thread safe runCommands(panel, player, commands, 0);
Bukkit.getGlobalRegionScheduler().run(ctx.plugin, task ->
runCommands(panel, player, commands, 0));
} }
private void runCommands(Panel panel, Player player, List<String> commands, int index) { private void runCommands(Panel panel, Player player, List<String> commands, int index) {

View File

@ -59,7 +59,7 @@ public class PanelOpenCommand implements Listener {
} }
// Stop and do not open panel if conditions are false // Stop and do not open panel if conditions are false
if (!panel.canOpen(e.getPlayer(), ctx)) { if (!panel.passesConditions(e.getPlayer(), ctx)) {
continue; continue;
} }

View File

@ -3,6 +3,9 @@ package me.rockyhawk.commandpanels.session;
import me.rockyhawk.commandpanels.Context; import me.rockyhawk.commandpanels.Context;
import me.rockyhawk.commandpanels.builder.logic.ConditionNode; import me.rockyhawk.commandpanels.builder.logic.ConditionNode;
import me.rockyhawk.commandpanels.builder.logic.ConditionParser; import me.rockyhawk.commandpanels.builder.logic.ConditionParser;
import me.rockyhawk.commandpanels.formatter.language.Message;
import me.rockyhawk.commandpanels.session.inventory.InventoryPanel;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey; import org.bukkit.NamespacedKey;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@ -30,7 +33,8 @@ public abstract class Panel {
this.type = config.getString("type", "inventory"); this.type = config.getString("type", "inventory");
} }
public boolean canOpen(Player player, Context ctx) { // Check run for permission checks with commands
public boolean passesConditions(Player player, Context ctx) {
// Check the panel condition // Check the panel condition
if (this.conditions.trim().isEmpty()) return true; if (this.conditions.trim().isEmpty()) return true;
try { try {
@ -41,12 +45,32 @@ 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) {
ctx.text.sendError(p, Message.COOLDOWN_ERROR);
return false;
}
// Do not allow the same panel to be opened again if already open
return !(p.getOpenInventory().getTopInventory().getHolder() instanceof InventoryPanel panel)
|| !panel.getName().equals(getName());
}
// Updates data to set the current panel and previous panel. // Updates data to set the current panel and previous 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");
PersistentDataContainer container = p.getPersistentDataContainer(); PersistentDataContainer container = p.getPersistentDataContainer();
// Time the player last opened any panel
container.set(keyTime, PersistentDataType.LONG, System.currentTimeMillis());
// Move current previous // Move current previous
String current = container.get(keyCurrent, PersistentDataType.STRING); String current = container.get(keyCurrent, PersistentDataType.STRING);
current = (current != null) ? current : ""; current = (current != null) ? current : "";
@ -54,6 +78,9 @@ public abstract class Panel {
// Set this panel as the new current // Set this panel as the new current
container.set(keyCurrent, PersistentDataType.STRING, this.name); container.set(keyCurrent, PersistentDataType.STRING, this.name);
if(ctx.fileHandler.config.getBoolean("panel-snooper")){
ctx.text.sendInfo(Bukkit.getConsoleSender(), Message.PANEL_OPEN_LOG, p.getName(), this.name);
}
} }

View File

@ -0,0 +1,33 @@
package me.rockyhawk.commandpanels.session;
import me.rockyhawk.commandpanels.Context;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
public class SessionDataUtils implements Listener {
private final Context ctx;
public SessionDataUtils(Context ctx) {
this.ctx = ctx;
}
@EventHandler
public void onJoinEvent(PlayerJoinEvent e) {
removeSessionData(e.getPlayer());
}
@EventHandler
public void onQuitEvent(PlayerQuitEvent e) {
removeSessionData(e.getPlayer());
}
private void removeSessionData(Player p){
p.getPersistentDataContainer().getKeys().stream()
.filter(key -> key.getNamespace().equalsIgnoreCase("commandpanels"))
.forEach(key -> p.getPersistentDataContainer().remove(key));
}
}

View File

@ -64,7 +64,10 @@ public class DialogPanel extends Panel {
} }
if(isNewPanelSession) { if(isNewPanelSession) {
// Update panel data values // Don't open same panel if its already open
if(!canOpen(player, ctx)){
return;
}
updatePanelData(ctx, player); updatePanelData(ctx, player);
// Run panel commands // Run panel commands

View File

@ -54,7 +54,10 @@ public class FloodgatePanel extends Panel {
} }
if(isNewPanelSession) { if(isNewPanelSession) {
// Update panel data values // Don't open same panel if its already open
if(!canOpen(player, ctx)){
return;
}
updatePanelData(ctx, player); updatePanelData(ctx, player);
// Run panel commands // Run panel commands

View File

@ -67,7 +67,7 @@ public class InventoryPanel extends Panel implements InventoryHolder {
if(isNewPanelSession) { if(isNewPanelSession) {
// Don't open same panel if its already open // Don't open same panel if its already open
if(checkCurrentPanel(player)){ if(!canOpen(player, ctx)){
return; return;
} }
updatePanelData(ctx, player); updatePanelData(ctx, player);
@ -75,23 +75,17 @@ public class InventoryPanel extends Panel implements InventoryHolder {
// Run panel commands // Run panel commands
CommandRunner runner = new CommandRunner(ctx); CommandRunner runner = new CommandRunner(ctx);
runner.runCommands(this, player, this.getCommands()); runner.runCommands(this, player, this.getCommands());
// Start a panel updater
InventoryPanelUpdater updater = new InventoryPanelUpdater();
updater.start(ctx, player, this);
} }
// Build and open the panel // Build and open the panel
PanelBuilder builder = new InventoryPanelBuilder(ctx, player); PanelBuilder builder = new InventoryPanelBuilder(ctx, player);
builder.open(this); builder.open(this);
}
// Do not open the same panel again if its already open if(isNewPanelSession) {
private boolean checkCurrentPanel(Player p) { // Start a panel updater
if(p.getOpenInventory().getTopInventory().getHolder() instanceof InventoryPanel panel){ InventoryPanelUpdater updater = new InventoryPanelUpdater();
return panel.getName().equals(getName()); updater.start(ctx, player, this);
} }
return false;
} }
public String getRows() { public String getRows() {

View File

@ -15,34 +15,39 @@ import org.bukkit.persistence.PersistentDataType;
public class InventoryPanelUpdater { public class InventoryPanelUpdater {
private ScheduledTask task; private ScheduledTask checkTask;
private ScheduledTask updateTask;
/**
* 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) { public void start(Context ctx, Player p, InventoryPanel panel) {
// If restarted, stop current event // Stop existing tasks if any
stop(); stop();
InventoryPanelBuilder panelBuilder = new InventoryPanelBuilder(ctx, p); // Determine update delay
ItemBuilder builder = new ItemBuilder(ctx, panelBuilder);
int updateDelay = 20; int updateDelay = 20;
if (panel.getUpdateDelay().matches("\\d+")) { if (panel.getUpdateDelay().matches("\\d+")) {
// Update delay value is a number
updateDelay = Integer.parseInt(panel.getUpdateDelay()); updateDelay = Integer.parseInt(panel.getUpdateDelay());
} }
// If update delay is 0 then do not run the updater // If update delay is 0 then do not run the updater
if (updateDelay == 0) { if (updateDelay == 0) {
this.task = null; this.updateTask = null;
return; return;
} }
// Schedule repeating GUI update task on the player's region InventoryPanelBuilder panelBuilder = new InventoryPanelBuilder(ctx, p);
this.task = Bukkit.getRegionScheduler().runAtFixedRate( ItemBuilder builder = new ItemBuilder(ctx, panelBuilder);
// Main update task
this.updateTask = Bukkit.getRegionScheduler().runAtFixedRate(
ctx.plugin, ctx.plugin,
p.getLocation(), p.getLocation(),
(scheduledTask) -> { (scheduledTask) -> {
Inventory inv = p.getOpenInventory().getTopInventory(); Inventory inv = p.getOpenInventory().getTopInventory();
InventoryHolder holder = inv.getHolder(); InventoryHolder holder = inv.getHolder();
if (!(holder instanceof InventoryPanel) || holder != panel) { if (!(holder instanceof InventoryPanel) || holder != panel) {
stop(); stop();
@ -88,12 +93,32 @@ public class InventoryPanelUpdater {
updateDelay, updateDelay,
updateDelay updateDelay
); );
// Fast heartbeat check task, should run frequently
this.checkTask = Bukkit.getRegionScheduler().runAtFixedRate(
ctx.plugin,
p.getLocation(),
(scheduledTask) -> {
Inventory inv = p.getOpenInventory().getTopInventory();
InventoryHolder holder = inv.getHolder();
if (!(holder instanceof InventoryPanel) || holder != panel) {
stop();
}
},
2,
2
);
} }
public void stop() { public void stop() {
if (task != null) { if (checkTask != null) {
task.cancel(); checkTask.cancel();
task = null; checkTask = null;
}
if (updateTask != null) {
updateTask.cancel();
updateTask = null;
} }
} }
} }