Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0796705
fix: warn when ANVIL inventory is loaded below 1.21
1robie Oct 13, 2026
44fd8e4
Merge remote-tracking branch 'origin/develop' into develop
1robie Oct 14, 2026
11c47c2
Merge remote-tracking branch 'upstream/develop' into develop
1robie Oct 14, 2026
6571082
fix: optimize cache handling and improve player argument management (…
1robie Oct 14, 2026
b7696e2
refactor: streamline inventory handling and improve performance
1robie Oct 14, 2026
d08c536
refactor: remove BarApi and replace TimerTask with WrappedTask in Inv…
1robie Oct 14, 2026
1b079f1
fix: update dialog configuration to use 'default-value' instead of 'i…
1robie Oct 15, 2026
39cef0f
fix: correct configuration file name in download command error messag…
1robie Oct 15, 2026
3769442
fix: add validation for item_drag button rules and log warnings for i…
1robie Oct 15, 2026
1609f32
fix: correct calculation for hours played statistic in MenuPlaceholders
1robie Oct 15, 2026
2495357
feat: add rule loader registration to ButtonManager and ZButtonManager
1robie Oct 15, 2026
1b1340e
fix: change loaders map to LinkedHashMap for consistent iteration ord…
1robie Oct 15, 2026
6ca3751
fix: enhance button option instantiation with additional constructor …
1robie Oct 15, 2026
1fb9f65
fix: log warning when title animation provider is not registered in I…
1robie Oct 15, 2026
be41816
fix: log warning for unknown check-item type in ItemDragLoader
1robie Oct 15, 2026
e82ef62
fix: prevent negative page index in player_previous_page placeholder
1robie Oct 15, 2026
d03114b
refactor: move ZData to Common
1robie Oct 15, 2026
525d48a
fix: wrong way to get ItemStack from MythicItem
1robie Oct 17, 2026
b33b004
fix: update dependencies for Paper-26-3 and adjust archive task settings
1robie Oct 18, 2026
49476cc
refactor: improve command execution handling in PerformButton and cle…
1robie Oct 18, 2026
57c6d37
feat: add serialization methods for ItemStack and implement SoundOpti…
1robie Oct 21, 2026
be77996
Merge branch 'Maxlego08:develop' into develop
1robie Oct 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions API/src/main/java/fr/maxlego08/menu/api/ButtonManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import fr.maxlego08.menu.api.requirement.Action;
import fr.maxlego08.menu.api.requirement.Permissible;
import fr.maxlego08.menu.api.requirement.Requirement;
import fr.maxlego08.menu.api.rules.loader.RuleLoader;
import fr.maxlego08.menu.api.utils.Loader;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.Plugin;
Expand Down Expand Up @@ -110,6 +111,16 @@ public interface ButtonManager {
*/
void registerAction(@NotNull ActionLoader actionLoader);

/**
* Registers a {@link RuleLoader} so its rule type (and aliases) can be used in item rules,
* for example in an {@code item_drag} button or an {@code item-rule} requirement.
* <p>
* Unlike {@code @AutoRuleLoader}, which only scans the zMenu package, this lets external plugins add rule types.
*
* @param ruleLoader The {@link RuleLoader} instance to register.
*/
void registerRuleLoader(@NotNull RuleLoader ruleLoader);

/**
* Retrieves an {@link Optional} {@link ActionLoader} based on a specific action key.
*
Expand Down
11 changes: 11 additions & 0 deletions API/src/main/java/fr/maxlego08/menu/api/InventoryManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import fr.maxlego08.menu.api.itemstack.MenuItemStackConversion;
import fr.maxlego08.menu.api.loader.MaterialLoader;
import fr.maxlego08.menu.api.pagination.PaginationManager;
import fr.maxlego08.menu.api.sound.SoundOption;
import fr.maxlego08.menu.api.utils.Message;
import fr.maxlego08.menu.api.utils.MetaUpdater;
import fr.maxlego08.menu.api.utils.Placeholders;
Expand Down Expand Up @@ -636,4 +637,14 @@ public interface InventoryManager extends Listener {
*/
PaginationManager getPaginationManager();

String serializeItemStack(ItemStack itemStack);

ItemStack deserializeItemStack(String serializedItemStack);

default SoundOption createSoundOption(String soundName, String categoryName, float pitch, float volume) {
return this.createSoundOption(soundName, categoryName, pitch, volume, false);
}

SoundOption createSoundOption(String soundName, String categoryName, float pitch, float volume, boolean isCustom);

}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package fr.maxlego08.menu.api.button;

import com.tcoded.folialib.impl.PlatformScheduler;
import com.tcoded.folialib.wrapper.task.WrappedTask;
import fr.maxlego08.menu.api.MenuPlugin;
import fr.maxlego08.menu.api.configuration.Configuration;
import fr.maxlego08.menu.api.utils.Placeholders;
import fr.maxlego08.menu.zcore.logger.Logger;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import org.jetbrains.annotations.Contract;
Expand All @@ -15,6 +17,7 @@

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

public abstract class PerformButton extends SlotButton {

Expand Down Expand Up @@ -171,11 +174,12 @@ private void execute(@NotNull MenuPlugin plugin, @NotNull Player player, @NotNul
player.chat("/" + plugin.parse(player, command));
} else {
String finalCommand = command;
Runnable runnable = () -> Bukkit.dispatchCommand(executor, plugin.parse(player, finalCommand));
if (plugin.isFolia()) {
if (executor instanceof Player) scheduler.runAtEntity(((Player) executor), w -> runnable.run());
else scheduler.runNextTick(w -> runnable.run());
} else runnable.run();
Consumer<WrappedTask> task = w -> Bukkit.dispatchCommand(executor, plugin.parse(player, finalCommand));
if (executor instanceof Entity entity) {
scheduler.runAtEntity(entity, task);
} else {
scheduler.runNextTick(task);
}
}
} catch (Exception exception) {
if (Configuration.enableDebug){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@
*/
public class CompatibilityUtil {

/**
* getTopInventory is called on every move, click and packet, so the lookup is resolved once per view class.
* Holds null when the class has no such method.
*/
private static final ClassValue<Method> TOP_INVENTORY_METHODS = new ClassValue<>() {
@Override
protected Method computeValue(@NotNull Class<?> type) {
try {
Method method = type.getMethod("getTopInventory");
method.setAccessible(true);
return method;
} catch (NoSuchMethodException | RuntimeException e) {
return null;
}
}
};

/**
* In API versions 1.20.6 and earlier, InventoryView is a class.
Expand All @@ -26,24 +42,22 @@ public class CompatibilityUtil {
*/
@Nullable
public static Inventory getTopInventory(@NotNull InventoryEvent event) {
try {
Object view = event.getView();
Method getTopInventory = view.getClass().getMethod("getTopInventory");
getTopInventory.setAccessible(true);
return (Inventory) getTopInventory.invoke(view);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
return null;
}
return getTopInventory(event.getView());
}

@Nullable
public static Inventory getTopInventory(@NotNull Player player) {
return getTopInventory((Object) player.getOpenInventory());
}

@Nullable
private static Inventory getTopInventory(@Nullable Object view) {
if (view == null) return null;
Method getTopInventory = TOP_INVENTORY_METHODS.get(view.getClass());
if (getTopInventory == null) return null;
try {
Object view = player.getOpenInventory();
Method getTopInventory = view.getClass().getMethod("getTopInventory");
getTopInventory.setAccessible(true);
return (Inventory) getTopInventory.invoke(view);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
} catch (InvocationTargetException | IllegalAccessException e) {
return null;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@ public class OfflinePlayerCache {
*/
@NotNull
public static OfflinePlayer get(@NotNull String playerName) {
return offlinePlayer.computeIfAbsent(playerName, name -> {
Player player = Bukkit.getPlayer(name);
return player != null ? player : Bukkit.getOfflinePlayer(name);
});
Player player = Bukkit.getPlayer(playerName);
if (player != null) return player;
return offlinePlayer.computeIfAbsent(playerName, Bukkit::getOfflinePlayer);
}

public static void clearCache(){
Expand Down
13 changes: 0 additions & 13 deletions API/src/main/java/fr/maxlego08/menu/api/utils/Tuples.java

This file was deleted.

56 changes: 36 additions & 20 deletions Common/src/main/java/fr/maxlego08/menu/common/utils/PapiUtils.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
package fr.maxlego08.menu.common.utils;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import fr.maxlego08.menu.api.configuration.Configuration;
import fr.maxlego08.menu.api.placeholder.Placeholder;
import org.bukkit.OfflinePlayer;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

public class PapiUtils extends TranslationHelper {

private static final long MAXIMUM_CACHE_SIZE = 10_000;

private static volatile Placeholder placeholder;
private transient final ConcurrentHashMap<String, CacheEntry> cache = new ConcurrentHashMap<>();
private transient volatile Cache<String, String> cache;
private transient volatile long cacheDuration = -1;

private Placeholder use() {
if (placeholder == null) {
Expand All @@ -20,6 +25,20 @@ private Placeholder use() {
return placeholder;
}

private Cache<String, String> getCache() {
long duration = Configuration.cachePlaceholderAPI;
Cache<String, String> current = this.cache;
if (current == null || this.cacheDuration != duration) {
current = CacheBuilder.newBuilder()
.maximumSize(MAXIMUM_CACHE_SIZE)
.expireAfterWrite(Math.max(duration, 0), TimeUnit.MILLISECONDS)
.build();
this.cache = current;
this.cacheDuration = duration;
}
return current;
}

public Object papi(Object placeHolder, OfflinePlayer player, boolean useCache) {
return placeHolder instanceof String ? this.papi((String) placeHolder, player, useCache) : placeHolder;
}
Expand All @@ -28,22 +47,30 @@ public String papi(String placeHolder, OfflinePlayer player, boolean useCache) {
if (placeHolder == null) return null;
if (player == null) return placeHolder;
if (!placeHolder.contains("%")) return placeHolder;
String cacheKey = placeHolder + ";" + player.getUniqueId();
CacheEntry cachedResult = this.cache.get(cacheKey);

if (Configuration.enableCachePlaceholderAPI && cachedResult != null && cachedResult.isValid() && useCache) {
return cachedResult.value;
if (!Configuration.enableCachePlaceholderAPI) {
return this.parse(placeHolder, player);
}

String result = this.use().setPlaceholders(player, placeHolder).replace("%player%", player.getName());
Cache<String, String> cache = this.getCache();
String cacheKey = placeHolder + ";" + player.getUniqueId();

if (Configuration.enableCachePlaceholderAPI) {
this.cache.put(cacheKey, new CacheEntry(result, System.currentTimeMillis()));
if (useCache) {
String cachedResult = cache.getIfPresent(cacheKey);
if (cachedResult != null) return cachedResult;
}

String result = this.parse(placeHolder, player);
cache.put(cacheKey, result);
return result;
}

private String parse(String placeHolder, OfflinePlayer player) {
String result = this.use().setPlaceholders(player, placeHolder);
String playerName = player.getName();
return playerName == null ? result : result.replace("%player%", playerName);
}

public List<String> papi(List<String> placeHolders, OfflinePlayer player, boolean useCache) {
if (player == null) return placeHolders;
List<String> parsed = new ArrayList<>(placeHolders.size());
Expand All @@ -52,15 +79,4 @@ public List<String> papi(List<String> placeHolders, OfflinePlayer player, boolea
}
return parsed;
}

/**
* @param timeStamp Time when the cache entry was created
*/
private record CacheEntry(String value, long timeStamp) {

public boolean isValid() {
// Check if the cache entry is still valid (not older than 1000 milliseconds)
return System.currentTimeMillis() - this.timeStamp <Configuration.cachePlaceholderAPI;
}
}
}
65 changes: 12 additions & 53 deletions Common/src/main/java/fr/maxlego08/menu/common/utils/ZUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@SuppressWarnings("deprecation")
public abstract class ZUtils extends MessageUtils {
private static final Timer TIMER = new Timer();
private static final Pattern HEX_COLOR_PATTERN = Pattern.compile("#[a-fA-F0-9]{6}");
private static final Pattern HEX_COLOR_REVERSE_PATTERN = Pattern.compile(net.md_5.bungee.api.ChatColor.COLOR_CHAR + "x[a-fA-F0-9-" + net.md_5.bungee.api.ChatColor.COLOR_CHAR + "]{12}");
// For plugin support from 1.8 to 1.12
private static Material[] byId;

Expand Down Expand Up @@ -288,69 +288,28 @@ protected boolean hasPermission(Permissible permissible, String permission) {
}


/**
* Schedules a fixed-rate execution of a task associated with the given plugin.
*
* @param plugin the MenuPlugin for which the task is scheduled
* @param delay the delay in milliseconds before task is to be executed and between successive task executions
* @param consumer the BiConsumer to be executed with the TimerTask and a boolean indicating the task's success
* @return the scheduled TimerTask
*/
protected TimerTask scheduleFix(MenuPlugin plugin, long delay, BiConsumer<TimerTask, Boolean> consumer) {
return this.scheduleFix(plugin, delay, delay, consumer);
}

/**
* Schedules a fixed-rate execution of a task associated with the given plugin.
*
* @param plugin the MenuPlugin for which the task is scheduled
* @param startAt the delay in milliseconds before the task is first executed
* @param delay the delay in milliseconds between successive task executions
* @param consumer the BiConsumer to be executed with the TimerTask and a boolean indicating the task's success
* @return the scheduled TimerTask
*/
protected TimerTask scheduleFix(MenuPlugin plugin, long startAt, long delay, BiConsumer<TimerTask, Boolean> consumer) {
TimerTask task = new TimerTask() {
@Override
public void run() {
if (!plugin.isEnabled()) {
this.cancel();
consumer.accept(this, false);
return;
}
plugin.getScheduler().runNextTick(w -> consumer.accept(this, true));
}
};
TIMER.scheduleAtFixedRate(task, startAt, delay);
return task;
}

protected String color(String message) {
if (message == null) return null;
if (NMSUtils.isHexColor()) {
Pattern pattern = Pattern.compile("#[a-fA-F0-9]{6}");
Matcher matcher = pattern.matcher(message);
if (NMSUtils.isHexColor() && message.indexOf('#') >= 0) {
Matcher matcher = HEX_COLOR_PATTERN.matcher(message);
StringBuilder builder = new StringBuilder(message.length() + 32);
while (matcher.find()) {
String color = message.substring(matcher.start(), matcher.end());
message = message.replace(color, String.valueOf(net.md_5.bungee.api.ChatColor.of(color)));
matcher = pattern.matcher(message);
matcher.appendReplacement(builder, Matcher.quoteReplacement(net.md_5.bungee.api.ChatColor.of(matcher.group()).toString()));
}
message = matcher.appendTail(builder).toString();
}
return net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', message);
}

public String colorReverse(String message) {
Pattern pattern = Pattern.compile(net.md_5.bungee.api.ChatColor.COLOR_CHAR + "x[a-fA-F0-9-" + net.md_5.bungee.api.ChatColor.COLOR_CHAR + "]{12}");
Matcher matcher = pattern.matcher(message);
Matcher matcher = HEX_COLOR_REVERSE_PATTERN.matcher(message);
StringBuilder builder = new StringBuilder(message.length());
while (matcher.find()) {
String color = message.substring(matcher.start(), matcher.end());
String colorReplace = color.replace("§x", "#");
colorReplace = colorReplace.replace("§", "");
message = message.replace(color, colorReplace);
matcher = pattern.matcher(message);
String colorReplace = matcher.group().replace("§x", "#").replace("§", "");
matcher.appendReplacement(builder, Matcher.quoteReplacement(colorReplace));
}

return message.replace("§", "&");
return matcher.appendTail(builder).toString().replace("§", "&");
}

protected List<String> color(List<String> messages) {
Expand Down
Loading
Loading