skins();
}
diff --git a/api/src/main/java/ru/abstractmenus/api/ProviderSection.java b/api/src/main/java/ru/abstractmenus/api/ProviderSection.java
new file mode 100644
index 0000000..900b5ae
--- /dev/null
+++ b/api/src/main/java/ru/abstractmenus/api/ProviderSection.java
@@ -0,0 +1,106 @@
+package ru.abstractmenus.api;
+
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * One section of the {@link ProviderRegistry}, holding registered handlers
+ * of a single provider type (economy, permissions, levels, placeholders,
+ * or skins).
+ *
+ * Each handler is registered under a string id (e.g. {@code "vault"},
+ * {@code "playerpoints"}), with a priority for auto-resolution and an
+ * owning {@link MenuExtension} for cleanup. Sections are mirrors of one
+ * another structurally; {@code ProviderRegistry.economy()} returns a
+ * {@code ProviderSection}, and so on. There is no per-type
+ * boilerplate on the registry interface.
+ *
+ * Resolution
+ *
+ *
+ * - {@link #resolve()} returns the highest-priority registered handler,
+ * overridden by the {@code config.conf providers.} value if it
+ * names a registered handler. Returns {@code null} if the section is
+ * empty.
+ * - {@link #resolve(String)} returns the explicitly-named handler, or
+ * {@code null} if no handler with that id is registered.
+ *
+ *
+ * Example
+ *
+ * {@code
+ * // Register a custom economy provider in onEnable:
+ * api.providers().economy().register(
+ * "playerpoints", new PlayerPointsEconomy(pp), 100, this);
+ *
+ * // From an action, ask for the configured/auto-resolved economy:
+ * EconomyHandler eco = api.providers().economy().resolve();
+ *
+ * // Or for a specific provider by id:
+ * EconomyHandler vault = api.providers().economy().resolve("vault");
+ * }
+ *
+ * Thread-safety
+ *
+ * Implementations are expected to be thread-safe for concurrent reads and
+ * writes - addon enable/disable can race against extractor lookups on Folia
+ * since regions run independently. The reference implementation
+ * synchronises on the section instance and resolves with-default
+ * atomically (so a concurrent unregister cannot leave the resolver
+ * pointing at a freed handler).
+ */
+public interface ProviderSection {
+
+ /**
+ * Register a handler under {@code id}. If an entry already exists for
+ * {@code id}, it is replaced. {@code priority} controls auto-resolve
+ * order: highest wins. {@code owner} is the {@link MenuExtension} that
+ * registered this entry; AbstractMenus' addon manager uses it for
+ * cleanup on disable / reload.
+ *
+ * @param id case-insensitive identifier (e.g. {@code "vault"})
+ * @param handler the handler instance
+ * @param priority higher wins in {@link #resolve()} when no config
+ * default is set; core providers register at 50, addons
+ * typically at 100
+ * @param owner the registering extension; used for cleanup
+ */
+ void register(String id, T handler, int priority, MenuExtension owner);
+
+ /**
+ * Resolve the handler that should serve "default" lookups. Tries the
+ * configured id from {@code config.conf providers.} first; if
+ * missing or set to {@code "auto"}, falls back to the highest-priority
+ * registered handler.
+ *
+ * @return a registered handler, or {@code null} if the section is empty
+ */
+ T resolve();
+
+ /**
+ * Resolve a specific handler by id.
+ *
+ * @param id case-insensitive identifier
+ * @return the registered handler, or {@code null} if not registered
+ */
+ T resolve(String id);
+
+ /** All registered handlers, in registration order. Read-only snapshot. */
+ Collection all();
+
+ /**
+ * All registered ids, lowercased and in registration order. Useful for
+ * "did you mean" or "unknown id, registered: [...]" error messages
+ * where you want to surface the actual configurable names rather than
+ * impl class names.
+ *
+ * @return read-only snapshot of registered ids
+ */
+ Set ids();
+
+ /**
+ * @param id case-insensitive identifier
+ * @return whether a handler with this id is registered
+ */
+ boolean has(String id);
+}
diff --git a/api/src/main/java/ru/abstractmenus/api/TypeRegistry.java b/api/src/main/java/ru/abstractmenus/api/TypeRegistry.java
index ecee338..3496ef7 100644
--- a/api/src/main/java/ru/abstractmenus/api/TypeRegistry.java
+++ b/api/src/main/java/ru/abstractmenus/api/TypeRegistry.java
@@ -13,9 +13,10 @@
* {@link AbstractMenusApi#itemProperties()}, and
* {@link AbstractMenusApi#catalogs()} — one per extension surface.
*
- * Registration takes a {@link MenuExtension} "owner" so
- * {@link #unregisterAll(MenuExtension)} can wipe every entry an addon
- * contributed when that addon is disabled or reloaded.
+ *
Registration takes a {@link MenuExtension} "owner" so AbstractMenus'
+ * internal addon manager can wipe every entry an addon contributed when
+ * that addon is disabled or reloaded. Addons themselves cannot trigger
+ * that wipe; the cleanup hook lives on the impl, not on this interface.
*
*
Example
*
@@ -61,9 +62,9 @@ public interface TypeRegistry {
* @param key HOCON-visible name (case-insensitive)
* @param type the class token; must be assignable to {@code T}
* @param serializer HOCON serializer for {@code type}
- * @param owner the registering extension; used for later
- * {@link #unregisterAll(MenuExtension)} cleanup. Pass
- * the core extension for core registrations.
+ * @param owner the registering extension; AbstractMenus' addon
+ * manager uses this for cleanup on disable/reload.
+ * Pass the core extension for core registrations.
*/
void register(String key,
Class type,
@@ -92,12 +93,4 @@ void register(String key,
* @return all registered keys (lowercased)
*/
Set keys();
-
- /**
- * Remove every entry registered by {@code owner}. Invoked by the addon
- * manager when an extension is disabled or reloaded.
- *
- * @param owner the extension whose entries should be wiped
- */
- void unregisterAll(MenuExtension owner);
}
diff --git a/api/src/main/java/ru/abstractmenus/api/handler/EconomyHandler.java b/api/src/main/java/ru/abstractmenus/api/handler/EconomyHandler.java
index 382dec9..c20ab62 100644
--- a/api/src/main/java/ru/abstractmenus/api/handler/EconomyHandler.java
+++ b/api/src/main/java/ru/abstractmenus/api/handler/EconomyHandler.java
@@ -20,7 +20,7 @@
* Registration
*
* A single handler is active at a time. Register yours via
- * {@link ru.abstractmenus.api.ProviderRegistry#registerEconomy} inside your
+ * {@link ru.abstractmenus.api.ProviderSection#register} inside your
* addon's {@link ru.abstractmenus.api.MenuExtension#onEnable}:
*
* Example — bridging PlayerPoints
@@ -76,7 +76,7 @@
* blocking IO; if the underlying economy plugin hits a database, cache the
* last known balance and refresh asynchronously.
*
- * @see ru.abstractmenus.api.ProviderRegistry#registerEconomy
+ * @see ru.abstractmenus.api.ProviderSection#register
* @see PermissionsHandler
* @see LevelHandler
*/
diff --git a/api/src/main/java/ru/abstractmenus/api/handler/LevelHandler.java b/api/src/main/java/ru/abstractmenus/api/handler/LevelHandler.java
index 49f6c19..bfc6919 100644
--- a/api/src/main/java/ru/abstractmenus/api/handler/LevelHandler.java
+++ b/api/src/main/java/ru/abstractmenus/api/handler/LevelHandler.java
@@ -24,7 +24,7 @@
*
* Multiple handlers may coexist — the highest-priority one is picked
* when a menu does not name a provider explicitly. Register yours via
- * {@link ru.abstractmenus.api.ProviderRegistry#registerLevels} inside your
+ * {@link ru.abstractmenus.api.ProviderSection#register} inside your
* addon's {@link ru.abstractmenus.api.MenuExtension#onEnable}.
*
* Example — bridging MMOCore
@@ -91,7 +91,7 @@
* blocking IO; if the underlying levelling plugin hits a database, serve the
* last known value from memory and refresh asynchronously.
*
- * @see ru.abstractmenus.api.ProviderRegistry#registerLevels
+ * @see ru.abstractmenus.api.ProviderSection#register
* @see EconomyHandler
* @see PermissionsHandler
*/
diff --git a/api/src/main/java/ru/abstractmenus/api/handler/PermissionsHandler.java b/api/src/main/java/ru/abstractmenus/api/handler/PermissionsHandler.java
index ad3f3cb..bad4101 100644
--- a/api/src/main/java/ru/abstractmenus/api/handler/PermissionsHandler.java
+++ b/api/src/main/java/ru/abstractmenus/api/handler/PermissionsHandler.java
@@ -27,7 +27,7 @@
*
* Multiple handlers may coexist — the highest-priority one is picked
* when a menu does not name a provider explicitly. Register yours via
- * {@link ru.abstractmenus.api.ProviderRegistry#registerPermissions} inside
+ * {@link ru.abstractmenus.api.ProviderSection#register} inside
* your addon's {@link ru.abstractmenus.api.MenuExtension#onEnable}.
*
* If no permissions handler is registered, permission nodes are checked
@@ -112,7 +112,7 @@
* asynchronously provided the in-memory view updates before the next
* {@code has*} call.
*
- * @see ru.abstractmenus.api.ProviderRegistry#registerPermissions
+ * @see ru.abstractmenus.api.ProviderSection#register
* @see EconomyHandler
* @see LevelHandler
*/
diff --git a/api/src/main/java/ru/abstractmenus/api/handler/PlaceholderHandler.java b/api/src/main/java/ru/abstractmenus/api/handler/PlaceholderHandler.java
index eb049f9..7b57fb6 100644
--- a/api/src/main/java/ru/abstractmenus/api/handler/PlaceholderHandler.java
+++ b/api/src/main/java/ru/abstractmenus/api/handler/PlaceholderHandler.java
@@ -34,7 +34,7 @@
* Although the placeholder handler lives in
* {@link ru.abstractmenus.api.ProviderRegistry} for API symmetry with the
* other sections, in practice it is registered once globally
- * via {@link ru.abstractmenus.api.ProviderRegistry#registerPlaceholders} at
+ * via {@link ru.abstractmenus.api.ProviderSection#register} at
* {@link ru.abstractmenus.api.MenuExtension#onEnable} time, not selected
* per-action. Per-element provider selection is not meaningful for
* placeholders — the core engine (PAPI) is chain-of-responsibility by
@@ -97,7 +97,7 @@
* cache aggressively and serve stale values. The engine itself should never
* block.
*
- * @see ru.abstractmenus.api.ProviderRegistry#registerPlaceholders
+ * @see ru.abstractmenus.api.ProviderSection#register
* @see EconomyHandler
*/
public interface PlaceholderHandler {
diff --git a/api/src/main/java/ru/abstractmenus/api/handler/SkinHandler.java b/api/src/main/java/ru/abstractmenus/api/handler/SkinHandler.java
index ef865eb..f5bc9ba 100644
--- a/api/src/main/java/ru/abstractmenus/api/handler/SkinHandler.java
+++ b/api/src/main/java/ru/abstractmenus/api/handler/SkinHandler.java
@@ -24,7 +24,7 @@
*
* Multiple handlers may coexist — the highest-priority one is picked
* when a menu does not name a provider explicitly. Register yours via
- * {@link ru.abstractmenus.api.ProviderRegistry#registerSkins} inside your
+ * {@link ru.abstractmenus.api.ProviderSection#register} inside your
* addon's {@link ru.abstractmenus.api.MenuExtension#onEnable}.
*
*
When no skin handler is registered, {@code setSkin} and {@code resetSkin}
@@ -96,7 +96,7 @@
* and apply on the main thread — blocking here freezes every online
* player.
*
- * @see ru.abstractmenus.api.ProviderRegistry#registerSkins
+ * @see ru.abstractmenus.api.ProviderSection#register
* @see EconomyHandler
*/
public interface SkinHandler {
diff --git a/api/src/main/java/ru/abstractmenus/api/text/Colors.java b/api/src/main/java/ru/abstractmenus/api/text/Colors.java
index c711f4e..83d5a87 100644
--- a/api/src/main/java/ru/abstractmenus/api/text/Colors.java
+++ b/api/src/main/java/ru/abstractmenus/api/text/Colors.java
@@ -82,8 +82,15 @@ public class Colors {
* {@code net.md_5.bungee.api.ChatColor} and reflectively looking up the
* {@code of(String)} method. Any {@link Throwable} is silently caught,
* treating the server as legacy-only.
+ *
+ * Set-once: subsequent calls are silently skipped so an addon cannot
+ * flip the global RGB-handling mode. The skip (rather than throw)
+ * keeps Bukkit {@code /reload} working; the trade-off is that a
+ * config flip of {@code useMiniMessage} doesn't take effect until
+ * a server restart.
*/
public static void init(boolean replaceRgb) {
+ if (replacer != null) return;
if (isSupportRgb() && replaceRgb) {
replacer = new RgbReplacer();
} else {
diff --git a/build.gradle b/build.gradle
index 997e5f5..2417ef7 100644
--- a/build.gradle
+++ b/build.gradle
@@ -9,7 +9,7 @@ plugins {
allprojects {
group = 'ru.abstractmenus'
- version = '2.0.0-alpha'
+ version = '2.0.0-alpha.2'
repositories {
mavenLocal()
diff --git a/plugin/build.gradle b/plugin/build.gradle
index 9092520..ef92f88 100644
--- a/plugin/build.gradle
+++ b/plugin/build.gradle
@@ -45,8 +45,15 @@ dependencies {
implementation project(':api')
implementation 'com.fathzer:javaluator:3.0.6'
- implementation 'net.kyori:adventure-platform-bukkit:4.4.1'
- implementation 'net.kyori:adventure-text-minimessage:4.26.1'
+
+ // Adventure (Component, MiniMessage, LegacyComponentSerializer) is
+ // bundled INSIDE Paper itself (via paper-api's BOM). Pull it in
+ // compileOnly so we get the types at compile time but do not bundle
+ // ~2.7 MB of duplicate Adventure classes into the shadow jar.
+ // adventure-platform-bukkit was used by nothing in our code so it is
+ // dropped entirely.
+ compileOnly 'net.kyori:adventure-text-minimessage:4.26.1'
+
implementation "com.github.technicallycoded:FoliaLib:0.4.4"
shadow files('libs/MMOItems-6.5.4.jar')
diff --git a/plugin/src/main/java/ru/abstractmenus/AbstractMenus.java b/plugin/src/main/java/ru/abstractmenus/AbstractMenus.java
index 9d70629..4af53f8 100644
--- a/plugin/src/main/java/ru/abstractmenus/AbstractMenus.java
+++ b/plugin/src/main/java/ru/abstractmenus/AbstractMenus.java
@@ -11,7 +11,7 @@
import org.bukkit.plugin.java.JavaPlugin;
import ru.abstractmenus.api.*;
import ru.abstractmenus.api.AbstractMenusApi;
-import ru.abstractmenus.api.AbstractMenusApiImpl;
+import ru.abstractmenus.impl.AbstractMenusApiImpl;
import ru.abstractmenus.api.inventory.Menu;
import ru.abstractmenus.api.text.Colors;
import ru.abstractmenus.api.variables.VariableManager;
@@ -68,6 +68,12 @@ public final class AbstractMenus extends JavaPlugin {
private AbstractMenusApi api;
private AddonManager addonManager;
private MainConfig mainConfig;
+ /**
+ * The plugin's own dogfood {@link MenuExtension}. Held as a field so
+ * {@code /am addons list} can render it as the {@code [built-in]} entry
+ * and tell it apart from operator-installed Path 1 / Path 2 addons.
+ */
+ private MenuExtension core;
@Getter
@Setter
@@ -146,7 +152,7 @@ public void onEnable() {
// Core extension — dogfood: the plugin registers its own types through
// the same SPI external addons will use.
- MenuExtension core = new CoreExtension();
+ core = new CoreExtension();
core.onLoad(api);
core.onEnable(api);
@@ -175,6 +181,13 @@ public void onEnable() {
} catch (Exception e) {
Logger.severe("Cannot enable plugin: " + e.getMessage());
e.printStackTrace();
+ // disablePlugin() from inside onEnable is re-entrant: Bukkit calls
+ // our onDisable while we are still in this stack frame. Every
+ // singleton accessed in onDisable is null-guarded so a partial
+ // init does not NPE on the way out. Rethrowing instead would
+ // skip our cleanup (service-manager registrations, listeners,
+ // database connections) since Bukkit does not call onDisable
+ // for plugins whose onEnable threw.
disablePlugin();
}
}
@@ -207,7 +220,9 @@ public void onDisable() {
if (BungeeManager.instance() != null)
BungeeManager.instance().stopOnlineTimer();
- VariableManagerImpl.instance().shutdown();
+ if (VariableManagerImpl.instance() != null) {
+ VariableManagerImpl.instance().shutdown();
+ }
Events.unregisterAll();
getServer().getMessenger().unregisterIncomingPluginChannel(this, "BungeeCord");
@@ -266,7 +281,12 @@ public static AbstractMenus instance() {
return instance;
}
- // from SkinRestorer
+ // from SkinRestorer.
+ //
+ // YamlConfiguration.loadConfiguration below reads spigot.yml / paper.yml
+ // synchronously on the main thread. These files are <10 KB on a real
+ // server and the call only fires once, during onEnable, before any
+ // gameplay can be affected. Acceptable startup-only IO.
public boolean determineProxy() {
Path spigotFile = Paths.get("spigot.yml");
Path paperFile = Paths.get("paper.yml");
diff --git a/plugin/src/main/java/ru/abstractmenus/addon/AddonClassLoader.java b/plugin/src/main/java/ru/abstractmenus/addon/AddonClassLoader.java
index df9a9b9..f27b3ce 100644
--- a/plugin/src/main/java/ru/abstractmenus/addon/AddonClassLoader.java
+++ b/plugin/src/main/java/ru/abstractmenus/addon/AddonClassLoader.java
@@ -21,9 +21,27 @@ public final class AddonClassLoader extends URLClassLoader {
static final String[] PARENT_FIRST_PREFIXES = {
"ru.abstractmenus.api.",
"org.bukkit.",
+ "org.spigotmc.",
"io.papermc.",
"com.destroystokyo.paper.",
"net.kyori.adventure.",
+ // Paperweight-userdev exposes remapped NMS / Mojang internals to
+ // the plugin classloader. Keep them parent-first so an addon
+ // cannot ship its own copy of, say, net.minecraft.nbt.CompoundTag
+ // and have it hide the real one for any code that crosses the
+ // addon boundary.
+ "net.minecraft.",
+ "com.mojang.",
+ // Server-bundled libraries. If an addon ships its own copy with
+ // a different version or shaded-but-not-relocated, the same
+ // class loaded by two classloaders is not the same class -
+ // ClassCastException at the first hand-off. Force parent-first
+ // so everyone uses the server's copy.
+ "ca.spottedleaf.", // Paper concurrent utilities
+ "io.netty.", // Netty (Paper bundles)
+ "it.unimi.dsi.fastutil.",
+ "com.google.gson.",
+ "com.google.common.", // Guava
"java.",
"javax.",
"jdk.",
diff --git a/plugin/src/main/java/ru/abstractmenus/addon/AddonConf.java b/plugin/src/main/java/ru/abstractmenus/addon/AddonConf.java
index 455a8e8..519c554 100644
--- a/plugin/src/main/java/ru/abstractmenus/addon/AddonConf.java
+++ b/plugin/src/main/java/ru/abstractmenus/addon/AddonConf.java
@@ -7,6 +7,7 @@
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
+import java.util.regex.Pattern;
/**
* Immutable metadata parsed from an addon's {@code addon.conf} file. Use
@@ -27,6 +28,15 @@ public record AddonConf(
List pluginSoftDependencies
) {
+ /**
+ * Loose Java class-name pattern: identifier chars + dots, must start
+ * with a letter / underscore / dollar. Catches obvious typos in
+ * addon.conf's {@code main} field with a clear error before the
+ * cleaner-but-deeper {@code ClassNotFoundException} from loadClass.
+ */
+ private static final Pattern CLASS_NAME =
+ Pattern.compile("[A-Za-z_$][A-Za-z0-9_$]*(\\.[A-Za-z_$][A-Za-z0-9_$]*)*");
+
/**
* Parse HOCON text into an AddonConf.
*
@@ -48,15 +58,19 @@ public static AddonConf parse(String hocon) {
throw new AddonConfParseException("Failed to parse addon.conf: " + e.getMessage(), e);
}
- String name = requireString(root, "name");
- String version = requireString(root, "version");
- String main = requireString(root, "main");
+ String name = requireString(root, "name");
+ String version = requireString(root, "version");
+ String main = requireString(root, "main");
+ if (!CLASS_NAME.matcher(main).matches()) {
+ throw new AddonConfParseException(
+ "Field 'main' is not a legal Java class name: '" + main + "'");
+ }
- List authors = optionalStringList(root, "authors");
- String description = root.node("description").getString("");
- String targetApiVersion = optionalString(root, "targetApiVersion");
- List addonDependencies = optionalStringList(root, "addonDependencies");
- List pluginDependencies = optionalStringList(root, "pluginDependencies");
+ List authors = optionalStringList(root, "authors");
+ String description = root.node("description").getString("");
+ String targetApiVersion = optionalString(root, "targetApiVersion");
+ List addonDependencies = optionalStringList(root, "addonDependencies");
+ List pluginDependencies = optionalStringList(root, "pluginDependencies");
List pluginSoftDependencies = optionalStringList(root, "pluginSoftDependencies");
return new AddonConf(
diff --git a/plugin/src/main/java/ru/abstractmenus/addon/AddonDependencyGraph.java b/plugin/src/main/java/ru/abstractmenus/addon/AddonDependencyGraph.java
index e2ae328..087e4a3 100644
--- a/plugin/src/main/java/ru/abstractmenus/addon/AddonDependencyGraph.java
+++ b/plugin/src/main/java/ru/abstractmenus/addon/AddonDependencyGraph.java
@@ -20,7 +20,43 @@ public final class AddonDependencyGraph {
private AddonDependencyGraph() {}
/**
- * Sort addons into enabling order.
+ * Find every addon whose declared dependencies include a name that is
+ * not present (or transitively unsatisfied) in the graph. Useful for
+ * pre-filtering before {@link #topoSort} so a single bad addon does
+ * not poison the whole batch.
+ *
+ * Runs to a fixed point: if A depends on B and B depends on missing
+ * C, both A and B are reported. A single pass would only catch B,
+ * leaving A to fail later inside {@code topoSort} or {@code onEnable}.
+ *
+ * @param dependencies graph (same shape as {@link #topoSort})
+ * @return set of addon names whose dependency closure cannot be
+ * satisfied, in iteration order of {@code dependencies}
+ */
+ public static Set unsatisfied(Map> dependencies) {
+ Set bad = new LinkedHashSet<>();
+ boolean changed;
+ do {
+ changed = false;
+ for (Map.Entry> e : dependencies.entrySet()) {
+ if (bad.contains(e.getKey())) continue;
+ for (String dep : e.getValue()) {
+ if (!dependencies.containsKey(dep) || bad.contains(dep)) {
+ bad.add(e.getKey());
+ changed = true;
+ break;
+ }
+ }
+ }
+ } while (changed);
+ return bad;
+ }
+
+ /**
+ * Sort addons into enabling order. Caller must ensure every dep
+ * referenced is present in {@code dependencies} - use
+ * {@link #unsatisfied} first to filter out bad nodes. Cycle detection
+ * still throws.
*
* @param dependencies map from addon name → list of names it depends on.
* Iteration order of the input map is preserved
@@ -28,22 +64,8 @@ private AddonDependencyGraph() {}
* {@link java.util.LinkedHashMap} for determinism).
* @return addon names in dependency-first order (deps before dependants)
* @throws AddonDependencyCycleException if a cycle is detected
- * @throws AddonDependencyException if a declared dep refers to a
- * name not present in the graph
*/
public static List topoSort(Map> dependencies) {
- Set known = dependencies.keySet();
-
- // Validation: every declared dep must exist in the graph.
- for (Map.Entry> e : dependencies.entrySet()) {
- for (String dep : e.getValue()) {
- if (!known.contains(dep)) {
- throw new AddonDependencyException(
- "Addon '" + e.getKey() + "' depends on unknown addon '" + dep + "'");
- }
- }
- }
-
Set permanent = new HashSet<>();
Set temporary = new LinkedHashSet<>(); // order preserved for cycle message
List order = new ArrayList<>();
diff --git a/plugin/src/main/java/ru/abstractmenus/addon/AddonManager.java b/plugin/src/main/java/ru/abstractmenus/addon/AddonManager.java
index 8747ffe..572e45c 100644
--- a/plugin/src/main/java/ru/abstractmenus/addon/AddonManager.java
+++ b/plugin/src/main/java/ru/abstractmenus/addon/AddonManager.java
@@ -3,15 +3,26 @@
import ru.abstractmenus.AbstractMenus;
import ru.abstractmenus.api.AbstractMenusApi;
import ru.abstractmenus.api.Logger;
-
+import ru.abstractmenus.api.MenuExtension;
+import ru.abstractmenus.impl.ProviderRegistryImpl;
+import ru.abstractmenus.impl.TypeRegistryImpl;
+
+import java.io.IOException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
/**
* Loads, enables, and manages AM-loaded addons (the lightweight jars in
@@ -23,29 +34,52 @@
*/
public final class AddonManager {
- private final AbstractMenus plugin;
+ /**
+ * TTL for the availableNotLoaded() cache. Tab completion fires on every
+ * keystroke; without a cache that is N jar opens + N HOCON parses on the
+ * main thread per TAB. 2 seconds is short enough that the operator does
+ * not see staleness in practice (drop a jar, wait a beat, hit TAB).
+ */
+ private static final long AVAILABLE_CACHE_TTL_MS = 2_000L;
+
+ /**
+ * Hard cap on the size of {@code addon.conf} read from a jar. A real
+ * addon.conf is well under a kilobyte; refusing to read more than 64 KB
+ * defends against a malicious or corrupted jar declaring a huge
+ * uncompressed size that would OOM the server when {@code readAllBytes}
+ * tries to allocate the buffer.
+ */
+ private static final int MAX_ADDON_CONF_BYTES = 64 * 1024;
+
private final Path addonsDir;
private final AbstractMenusApi api;
+ private final PluginDepChecker depChecker;
+ private final ClassLoader parentClassLoader;
/** name (lowercased) → LoadedAddon, insertion-ordered (matches enable order) */
private final Map addons = new LinkedHashMap<>();
+ private volatile List cachedAvailable = List.of();
+ private volatile long cachedAvailableAt = 0L;
+
public AddonManager(AbstractMenus plugin, AbstractMenusApi api) {
- this.plugin = plugin;
this.api = api;
this.addonsDir = plugin.getDataFolder().toPath().resolve("addons");
+ this.depChecker = name -> plugin.getServer().getPluginManager().getPlugin(name) != null;
+ this.parentClassLoader = plugin.getClass().getClassLoader();
}
/**
- * Test-only overload: inject addonsDir directly, skip Bukkit plugin-dep
- * checks (since no {@code plugin.getServer()} is available in pure-unit
- * tests). Any addon with a non-empty {@code pluginDependencies} will fail
- * under this constructor.
+ * Test-only overload: inject addonsDir directly. Bukkit-plugin
+ * dependency checks are stubbed to always report present so a test
+ * addon can declare {@code pluginDependencies} without booting a
+ * server.
*/
AddonManager(Path addonsDir, AbstractMenusApi api) {
- this.plugin = null;
this.api = api;
this.addonsDir = addonsDir;
+ this.depChecker = PluginDepChecker.ALL_PRESENT;
+ this.parentClassLoader = AddonManager.class.getClassLoader();
}
/**
@@ -60,48 +94,53 @@ public void loadAll() {
return;
}
- // Soft-filter: drop addons whose required Bukkit plugin deps are missing.
- // In test mode (plugin == null), skip this check — tests must not declare
- // pluginDependencies.
+ // Filter out addons whose hard plugin deps are missing. Soft deps
+ // log a warning but do not block the addon.
var byName = new LinkedHashMap();
- if (plugin != null) {
- var pluginManager = plugin.getServer().getPluginManager();
- for (LoadedAddon la : pending.values()) {
- AddonConf c = la.getConf();
- boolean missing = false;
- for (String dep : c.pluginDependencies()) {
- if (pluginManager.getPlugin(dep) == null) {
- Logger.warning("Addon " + c.name()
- + " requires plugin '" + dep + "' which is not installed — skipping");
- la.markFailed(new IllegalStateException("missing plugin dependency: " + dep));
- missing = true;
- break;
- }
- }
- if (missing) {
- addons.put(c.name().toLowerCase(), la); // keep the failed entry visible in /am addons list
- continue;
- }
- byName.put(c.name().toLowerCase(), la);
+ for (LoadedAddon la : pending.values()) {
+ if (checkPluginDeps(la)) {
+ byName.put(la.getConf().name().toLowerCase(), la);
}
- } else {
- byName.putAll(pending);
}
if (byName.isEmpty()) return;
- // Sort by addon-level dependencies.
+ // Build the addon-dep graph.
Map> depGraph = new LinkedHashMap<>();
for (var e : byName.entrySet()) {
List deps = e.getValue().getConf().addonDependencies().stream()
.map(String::toLowerCase).toList();
depGraph.put(e.getKey(), deps);
}
+
+ // Pre-filter addons whose deps point to names not in the graph -
+ // typically because the dep is a plugin-as-addon (Path 1) which
+ // doesn't show up in the addons/ folder, or simply a typo. Mark
+ // them FAILED individually instead of poisoning the whole batch
+ // through topoSort. Runs to fixed point so transitive failures
+ // (A -> B -> missing C) catch both A and B in the same pass.
+ Set unsatisfied = AddonDependencyGraph.unsatisfied(depGraph);
+ Set originalGraphKeys = Set.copyOf(depGraph.keySet());
+ for (String key : unsatisfied) {
+ LoadedAddon la = byName.remove(key);
+ depGraph.remove(key);
+ String missing = la.getConf().addonDependencies().stream()
+ .filter(d -> !originalGraphKeys.contains(d.toLowerCase())
+ || unsatisfied.contains(d.toLowerCase()))
+ .findFirst().orElse("?");
+ String msg = "missing addon dependency: " + missing;
+ Logger.warning("Addon " + la.getConf().name() + " " + msg + " - skipping");
+ la.markFailed(new IllegalStateException(msg));
+ addons.put(key, la);
+ }
+
+ if (byName.isEmpty()) return;
+
List order;
try {
order = AddonDependencyGraph.topoSort(depGraph);
- } catch (AddonDependencyException ex) {
- Logger.severe("Addon dependency graph error: " + ex.getMessage());
+ } catch (AddonDependencyCycleException ex) {
+ Logger.severe("Addon dependency cycle detected: " + ex.getMessage());
for (var la : byName.values()) {
la.markFailed(ex);
addons.put(la.getConf().name().toLowerCase(), la);
@@ -119,13 +158,21 @@ public void loadAll() {
Logger.severe("Addon " + la.getConf().name() + " failed in onLoad: " + t);
t.printStackTrace();
la.markFailed(t);
+ // onLoad shouldn't register types per the contract, but a
+ // misbehaving addon might have done so before throwing -
+ // strip whatever it managed so the next addon's enable
+ // sees a clean registry state.
+ rollbackRegistrations(la);
}
}
// Stage 2: onEnable in dependency order.
for (String k : order) {
LoadedAddon la = byName.get(k);
- if (la.getStatus() == AddonStatus.FAILED) {
+ // Skip both FAILED entries and the defensive case where Stage 1
+ // somehow returned without an extension instance - either way,
+ // calling onEnable on a null extension would NPE.
+ if (la.getStatus() == AddonStatus.FAILED || la.getExtension() == null) {
addons.put(k, la);
continue;
}
@@ -151,23 +198,32 @@ public void loadAll() {
* Reflectively instantiate the addon's main class and verify it implements
* MenuExtension.
*/
- private ru.abstractmenus.api.MenuExtension instantiate(LoadedAddon la) throws Exception {
+ private MenuExtension instantiate(LoadedAddon la) throws Exception {
Class> main = la.getClassLoader().loadClass(la.getConf().main());
- if (!ru.abstractmenus.api.MenuExtension.class.isAssignableFrom(main)) {
+ if (!MenuExtension.class.isAssignableFrom(main)) {
throw new IllegalStateException("main class " + main.getName()
+ " does not implement MenuExtension");
}
- return (ru.abstractmenus.api.MenuExtension) main.getDeclaredConstructor().newInstance();
+ return (MenuExtension) main.getDeclaredConstructor().newInstance();
}
- /** Strip any type registrations the failed addon managed to make. */
+ /**
+ * Strip any type registrations the failed addon managed to make.
+ *
+ * Casts each registry to its {@code Impl} because {@code unregisterAll}
+ * is intentionally not on the public {@link ru.abstractmenus.api.TypeRegistry}
+ * / {@link ru.abstractmenus.api.ProviderRegistry} interfaces - addons
+ * shouldn't be able to wipe each other's registrations.
+ */
private void rollbackRegistrations(LoadedAddon la) {
if (la.getExtension() == null) return;
- api.actions().unregisterAll(la.getExtension());
- api.rules().unregisterAll(la.getExtension());
- api.activators().unregisterAll(la.getExtension());
- api.itemProperties().unregisterAll(la.getExtension());
- api.catalogs().unregisterAll(la.getExtension());
+ MenuExtension ext = la.getExtension();
+ ((TypeRegistryImpl>) api.actions()).unregisterAll(ext);
+ ((TypeRegistryImpl>) api.rules()).unregisterAll(ext);
+ ((TypeRegistryImpl>) api.activators()).unregisterAll(ext);
+ ((TypeRegistryImpl>) api.itemProperties()).unregisterAll(ext);
+ ((TypeRegistryImpl>) api.catalogs()).unregisterAll(ext);
+ ((ProviderRegistryImpl) api.providers()).unregisterAll(ext);
}
/**
@@ -176,8 +232,8 @@ private void rollbackRegistrations(LoadedAddon la) {
*/
public void unloadAll() {
// Disable in reverse enable order.
- var reversed = new java.util.ArrayList<>(addons.values());
- java.util.Collections.reverse(reversed);
+ var reversed = new ArrayList<>(addons.values());
+ Collections.reverse(reversed);
for (LoadedAddon la : reversed) {
try {
if (la.getStatus() == AddonStatus.ENABLED && la.getExtension() != null) {
@@ -204,6 +260,11 @@ public void unloadAll() {
* re-parse the jar → enable. Returns the new {@link LoadedAddon}, or
* empty if no addon of that name is currently loaded.
*
+ *
Goes through {@link #enableSingle} so plugin-dep and addon-dep
+ * checks re-run; a dependency that disappeared between the original
+ * load and this reload causes a clean FAILED state instead of a
+ * confusing trace deep inside {@code onEnable}.
+ *
* @param name addon name (case-insensitive)
* @return the freshly loaded addon, or empty if not found / no jar present
*/
@@ -224,6 +285,7 @@ public Optional reload(String name) {
}
try { existing.getClassLoader().close(); } catch (Exception ignored) {}
addons.remove(key);
+ cachedAvailableAt = 0L;
// Re-discover: find the jar whose addon.conf name matches.
Path freshJar = findJarByName(name);
@@ -240,43 +302,17 @@ public Optional reload(String name) {
return Optional.empty();
}
- // Enable the single addon. We don't re-chain the full topological sort
- // for a single-addon reload — assume its addonDependencies are already
- // enabled (they were, before this reload).
- try {
- fresh.setExtension(instantiate(fresh));
- fresh.getExtension().onLoad(api);
- fresh.getExtension().onEnable(api);
- fresh.markEnabled();
- addons.put(fresh.getConf().name().toLowerCase(), fresh);
- Logger.info("Reloaded addon: " + fresh.getConf().name() + " v" + fresh.getConf().version());
- } catch (Throwable t) {
- Logger.severe("Addon " + name + " failed during reload: " + t);
- t.printStackTrace();
- fresh.markFailed(t);
- rollbackRegistrations(fresh);
- addons.put(fresh.getConf().name().toLowerCase(), fresh);
- }
-
+ enableSingle(fresh);
return Optional.of(fresh);
}
/** Scan addonsDir again, return the first jar whose addon.conf.name matches. */
private Path findJarByName(String name) {
- if (!java.nio.file.Files.isDirectory(addonsDir)) return null;
- try (var stream = java.nio.file.Files.newDirectoryStream(addonsDir, "*.jar")) {
- for (Path jar : stream) {
- try (var jf = new java.util.jar.JarFile(jar.toFile())) {
- var entry = jf.getJarEntry("addon.conf");
- if (entry == null) continue;
- String hocon = new String(jf.getInputStream(entry).readAllBytes(),
- java.nio.charset.StandardCharsets.UTF_8);
- AddonConf c = AddonConf.parse(hocon);
- if (c.name().equalsIgnoreCase(name)) return jar;
- } catch (Exception ignored) {}
- }
- } catch (Exception ignored) {}
- return null;
+ return scanJarConfs().entrySet().stream()
+ .filter(e -> e.getValue().name().equalsIgnoreCase(name))
+ .map(Map.Entry::getKey)
+ .findFirst()
+ .orElse(null);
}
public Collection loaded() {
@@ -287,6 +323,31 @@ public Optional get(String name) {
return Optional.ofNullable(addons.get(name.toLowerCase()));
}
+ /**
+ * Every {@link MenuExtension} that has registered at least one type or
+ * provider on the running API. Includes:
+ *
+ * - Path 2 AM-loaded addons (also in {@link #loaded()})
+ * - Path 1 plugin-as-addons (NOT in {@link #loaded()} — they live
+ * on Bukkit's plugin lifecycle, we only see them through their
+ * registry footprint)
+ * - Plugin-internal extensions like {@code CoreExtension}
+ *
+ *
+ * Used by {@code /am addons list} to surface Path 1 entries that
+ * would otherwise be invisible under {@code /am addons}.
+ */
+ public Set knownExtensions() {
+ Set all = new HashSet<>();
+ all.addAll(((TypeRegistryImpl>) api.actions()).seenOwners());
+ all.addAll(((TypeRegistryImpl>) api.rules()).seenOwners());
+ all.addAll(((TypeRegistryImpl>) api.activators()).seenOwners());
+ all.addAll(((TypeRegistryImpl>) api.itemProperties()).seenOwners());
+ all.addAll(((TypeRegistryImpl>) api.catalogs()).seenOwners());
+ all.addAll(((ProviderRegistryImpl) api.providers()).seenOwners());
+ return all;
+ }
+
/**
* Scan {@link #addonsDir} for {@code *.jar} files. For each, extract
* {@code addon.conf}, parse it, and build a LoadedAddon (without enabling
@@ -301,16 +362,16 @@ public Optional get(String name) {
Map discover() {
Map pending = new LinkedHashMap<>();
- if (!java.nio.file.Files.isDirectory(addonsDir)) {
+ if (!Files.isDirectory(addonsDir)) {
try {
- java.nio.file.Files.createDirectories(addonsDir);
- } catch (java.io.IOException e) {
+ Files.createDirectories(addonsDir);
+ } catch (IOException e) {
Logger.warning("Could not create addons directory " + addonsDir + ": " + e.getMessage());
}
return pending;
}
- try (var stream = java.nio.file.Files.newDirectoryStream(addonsDir, "*.jar")) {
+ try (var stream = Files.newDirectoryStream(addonsDir, "*.jar")) {
for (Path jar : stream) {
try {
LoadedAddon addon = readAddonJar(jar);
@@ -326,7 +387,7 @@ Map discover() {
Logger.warning("Failed to load addon " + jar.getFileName() + ": " + e.getMessage());
}
}
- } catch (java.io.IOException e) {
+ } catch (IOException e) {
Logger.warning("Failed to scan addons directory: " + e.getMessage());
}
@@ -361,6 +422,7 @@ public Optional loadOne(String name) {
return Optional.empty();
}
enableSingle(la);
+ cachedAvailableAt = 0L;
return Optional.of(la);
}
@@ -384,39 +446,121 @@ public List rescan() {
enableSingle(la);
newlyLoaded.add(la);
}
+ if (!newlyLoaded.isEmpty()) cachedAvailableAt = 0L;
return newlyLoaded;
}
/**
* Return addon-conf {@code name}s found on disk under the addons
* directory but not yet loaded into memory. Used by tab completion
- * for {@code /am addons load }. Cost is one jar open and one
- * HOCON parse per .jar in the directory - acceptable at typical
- * scale (1-20 addons), but be aware this is not free.
+ * for {@code /am addons load }.
+ *
+ * Result is cached for {@value #AVAILABLE_CACHE_TTL_MS} ms because
+ * tab completion runs synchronously on the main thread and a cold call
+ * does one jar open + HOCON parse per *.jar in the addons folder.
*/
public List availableNotLoaded() {
- if (!java.nio.file.Files.isDirectory(addonsDir)) return List.of();
- List result = new ArrayList<>();
- try (var stream = java.nio.file.Files.newDirectoryStream(addonsDir, "*.jar")) {
+ long now = System.currentTimeMillis();
+ if (now - cachedAvailableAt < AVAILABLE_CACHE_TTL_MS) {
+ return cachedAvailable;
+ }
+ List result = scanJarConfs().values().stream()
+ .map(AddonConf::name)
+ .filter(n -> !addons.containsKey(n.toLowerCase()))
+ .toList();
+ cachedAvailable = result;
+ cachedAvailableAt = now;
+ return result;
+ }
+
+ /**
+ * One canonical pass over {@code addonsDir}: for each {@code *.jar},
+ * read its {@code addon.conf} entry and parse it. Entries that lack
+ * addon.conf or fail to parse are skipped silently (the operator
+ * already saw the warning at startup discover() time).
+ *
+ * Sole shared helper for {@link #findJarByName} and
+ * {@link #availableNotLoaded} to avoid duplicating the open-read-parse
+ * triple. Note that {@link #discover()} is separate because it ALSO
+ * builds the {@link AddonClassLoader}, which we don't want for the
+ * tab-completion path.
+ *
+ * @return jar Path → parsed AddonConf, in directory iteration order
+ */
+ private Map scanJarConfs() {
+ if (!Files.isDirectory(addonsDir)) return Map.of();
+ Map result = new LinkedHashMap<>();
+ try (var stream = Files.newDirectoryStream(addonsDir, "*.jar")) {
for (Path jar : stream) {
- try (var jf = new java.util.jar.JarFile(jar.toFile())) {
- var entry = jf.getJarEntry("addon.conf");
+ try (var jf = new JarFile(jar.toFile())) {
+ JarEntry entry = jf.getJarEntry("addon.conf");
if (entry == null) continue;
- String hocon = new String(jf.getInputStream(entry).readAllBytes(),
- java.nio.charset.StandardCharsets.UTF_8);
- AddonConf conf = AddonConf.parse(hocon);
- if (!addons.containsKey(conf.name().toLowerCase())) {
- result.add(conf.name());
- }
- } catch (Exception ignored) {
- // Malformed jar - skip silently, the operator already saw
- // the warning at server-start discover() time.
- }
+ result.put(jar, AddonConf.parse(readBoundedAddonConf(jf, entry)));
+ } catch (Exception ignored) {}
}
} catch (Exception ignored) {}
return result;
}
+ /**
+ * Read {@code addon.conf} into a String, refusing entries larger than
+ * {@link #MAX_ADDON_CONF_BYTES}. Both the declared uncompressed size
+ * (zip header) and the actual byte count are checked so a crafted
+ * jar can't lie about either one.
+ */
+ private static String readBoundedAddonConf(JarFile jar, JarEntry entry) throws IOException {
+ long declared = entry.getSize();
+ if (declared > MAX_ADDON_CONF_BYTES) {
+ throw new IOException("addon.conf too large (" + declared + " bytes, cap "
+ + MAX_ADDON_CONF_BYTES + ")");
+ }
+ try (var in = jar.getInputStream(entry)) {
+ byte[] bytes = in.readNBytes(MAX_ADDON_CONF_BYTES + 1);
+ if (bytes.length > MAX_ADDON_CONF_BYTES) {
+ throw new IOException("addon.conf exceeded " + MAX_ADDON_CONF_BYTES
+ + " bytes during read (declared size " + declared + ")");
+ }
+ return new String(bytes, StandardCharsets.UTF_8);
+ }
+ }
+
+ /**
+ * Verify hard {@code pluginDependencies} are present and log warnings
+ * for any missing {@code pluginSoftDependencies}.
+ *
+ * If a hard dep is missing the addon is marked FAILED and parked
+ * in the loaded map (so {@code /am addons list} surfaces it) and the
+ * method returns false. Soft-dep misses log a warning but do not
+ * block enabling.
+ *
+ * @return true if the addon may proceed to enable, false if a hard
+ * dependency is missing
+ */
+ private boolean checkPluginDeps(LoadedAddon la) {
+ AddonConf c = la.getConf();
+ String key = c.name().toLowerCase();
+
+ for (String dep : c.pluginDependencies()) {
+ if (!depChecker.isPresent(dep)) {
+ String msg = "missing plugin dependency: " + dep;
+ Logger.warning("Addon " + c.name() + " " + msg + " - skipping");
+ la.markFailed(new IllegalStateException(msg));
+ addons.put(key, la);
+ return false;
+ }
+ }
+
+ for (String dep : c.pluginSoftDependencies()) {
+ if (!depChecker.isPresent(dep)) {
+ Logger.warning("Addon " + c.name()
+ + " soft-depends on plugin '" + dep
+ + "' which is not installed - features that need it may no-op");
+ }
+ }
+
+ return true;
+ }
+
/**
* Verify Bukkit-side and addon-side dependencies, then run
* onLoad + onEnable. Installs the result into the loaded map
@@ -426,18 +570,7 @@ public List availableNotLoaded() {
private void enableSingle(LoadedAddon la) {
String key = la.getConf().name().toLowerCase();
- if (plugin != null) {
- var pm = plugin.getServer().getPluginManager();
- for (String dep : la.getConf().pluginDependencies()) {
- if (pm.getPlugin(dep) == null) {
- String msg = "missing plugin dependency: " + dep;
- Logger.warning("Addon " + la.getConf().name() + " " + msg);
- la.markFailed(new IllegalStateException(msg));
- addons.put(key, la);
- return;
- }
- }
- }
+ if (!checkPluginDeps(la)) return;
for (String dep : la.getConf().addonDependencies()) {
LoadedAddon depAddon = addons.get(dep.toLowerCase());
@@ -472,27 +605,21 @@ private void enableSingle(LoadedAddon la) {
* Read a single addon jar: extract {@code addon.conf}, parse it, build a
* classloader. Throws if addon.conf is missing or malformed.
*/
- private LoadedAddon readAddonJar(Path jarPath) throws java.io.IOException {
+ private LoadedAddon readAddonJar(Path jarPath) throws IOException {
String hocon;
- try (var jar = new java.util.jar.JarFile(jarPath.toFile())) {
- var entry = jar.getJarEntry("addon.conf");
+ try (var jar = new JarFile(jarPath.toFile())) {
+ JarEntry entry = jar.getJarEntry("addon.conf");
if (entry == null) {
- throw new java.io.IOException("no addon.conf at jar root");
- }
- try (var in = jar.getInputStream(entry)) {
- hocon = new String(in.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8);
+ throw new IOException("no addon.conf at jar root");
}
+ hocon = readBoundedAddonConf(jar, entry);
}
AddonConf conf = AddonConf.parse(hocon);
- ClassLoader parent = (plugin != null)
- ? plugin.getClass().getClassLoader()
- : AddonManager.class.getClassLoader();
AddonClassLoader cl = new AddonClassLoader(
- new java.net.URL[]{jarPath.toUri().toURL()},
- parent);
+ new URL[]{jarPath.toUri().toURL()},
+ parentClassLoader);
return new LoadedAddon(conf, cl);
}
-
}
diff --git a/plugin/src/main/java/ru/abstractmenus/addon/PluginDepChecker.java b/plugin/src/main/java/ru/abstractmenus/addon/PluginDepChecker.java
new file mode 100644
index 0000000..4e996b7
--- /dev/null
+++ b/plugin/src/main/java/ru/abstractmenus/addon/PluginDepChecker.java
@@ -0,0 +1,20 @@
+package ru.abstractmenus.addon;
+
+/**
+ * Bukkit-plugin presence probe used by {@link AddonManager} to verify
+ * an addon's {@code pluginDependencies} / {@code pluginSoftDependencies}
+ * before instantiation.
+ *
+ * Lifted to its own interface so the production class doesn't carry
+ * a nullable {@code plugin} field that every caller has to guard - the
+ * test mode injects {@link #ALL_PRESENT} which always reports true.
+ */
+@FunctionalInterface
+interface PluginDepChecker {
+
+ /** @return true if a Bukkit plugin with this name is loaded and enabled. */
+ boolean isPresent(String pluginName);
+
+ /** Test-mode checker: every name reports as present. No Bukkit calls. */
+ PluginDepChecker ALL_PRESENT = name -> true;
+}
diff --git a/plugin/src/main/java/ru/abstractmenus/api/ProviderRegistryImpl.java b/plugin/src/main/java/ru/abstractmenus/api/ProviderRegistryImpl.java
deleted file mode 100644
index 50ae7b4..0000000
--- a/plugin/src/main/java/ru/abstractmenus/api/ProviderRegistryImpl.java
+++ /dev/null
@@ -1,155 +0,0 @@
-package ru.abstractmenus.api;
-
-import ru.abstractmenus.api.handler.EconomyHandler;
-import ru.abstractmenus.api.handler.LevelHandler;
-import ru.abstractmenus.api.handler.PermissionsHandler;
-import ru.abstractmenus.api.handler.PlaceholderHandler;
-import ru.abstractmenus.api.handler.SkinHandler;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.IdentityHashMap;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.function.Function;
-
-/**
- * Default {@link ProviderRegistry} implementation. Five sections sharing an
- * inner generic {@link Section} class. Insertion-ordered per section so that
- * equal-priority ties resolve to the first-registered entry.
- *
- *
Thread-safe for registration/unregistration via per-section synchronized
- * methods, but production use expects all mutation to happen on the main
- * server thread during plugin / extension enable/disable.
- */
-public final class ProviderRegistryImpl implements ProviderRegistry {
-
- private final Section economy = new Section<>();
- private final Section permissions = new Section<>();
- private final Section levels = new Section<>();
- private final Section placeholders = new Section<>();
- private final Section skins = new Section<>();
-
- /** section kind → configured-default id (e.g. "economy" → "playerpoints"). */
- private Function configDefaults = kind -> null; // no-op by default
-
- /** Wire up the config-backed default source. Called once from AbstractMenusApiImpl. */
- public void setConfigDefaults(Function lookup) {
- this.configDefaults = lookup;
- }
-
- // ---- Config-default resolution helper --------------------------------
-
- private T resolveWithConfig(String kind, Section section) {
- String configured = configDefaults.apply(kind);
- if (configured != null && !configured.equalsIgnoreCase("auto")) {
- T h = section.byId(configured);
- if (h != null) return h;
- // Configured id not found — fall back to auto.
- }
- return section.auto();
- }
-
- // ---- Economy ---------------------------------------------------------
-
- @Override public void registerEconomy(String id, EconomyHandler h, int pr, MenuExtension o) { economy.put(id, h, pr, o); }
- @Override public EconomyHandler economy() { return resolveWithConfig("economy", economy); }
- @Override public EconomyHandler economy(String id) { return economy.byId(id); }
- @Override public Collection allEconomy() { return economy.all(); }
- @Override public boolean hasEconomy(String id) { return economy.has(id); }
-
- // ---- Permissions -----------------------------------------------------
-
- @Override public void registerPermissions(String id, PermissionsHandler h, int pr, MenuExtension o) { permissions.put(id, h, pr, o); }
- @Override public PermissionsHandler permissions() { return resolveWithConfig("permissions", permissions); }
- @Override public PermissionsHandler permissions(String id) { return permissions.byId(id); }
- @Override public Collection allPermissions() { return permissions.all(); }
- @Override public boolean hasPermissions(String id) { return permissions.has(id); }
-
- // ---- Levels ----------------------------------------------------------
-
- @Override public void registerLevels(String id, LevelHandler h, int pr, MenuExtension o) { levels.put(id, h, pr, o); }
- @Override public LevelHandler levels() { return resolveWithConfig("levels", levels); }
- @Override public LevelHandler levels(String id) { return levels.byId(id); }
- @Override public Collection allLevels() { return levels.all(); }
- @Override public boolean hasLevels(String id) { return levels.has(id); }
-
- // ---- Placeholders ----------------------------------------------------
-
- @Override public void registerPlaceholders(String id, PlaceholderHandler h, int pr, MenuExtension o) { placeholders.put(id, h, pr, o); }
- @Override public PlaceholderHandler placeholders() { return resolveWithConfig("placeholders", placeholders); }
- @Override public PlaceholderHandler placeholders(String id) { return placeholders.byId(id); }
- @Override public Collection allPlaceholders() { return placeholders.all(); }
- @Override public boolean hasPlaceholders(String id) { return placeholders.has(id); }
-
- // ---- Skins -----------------------------------------------------------
-
- @Override public void registerSkins(String id, SkinHandler h, int pr, MenuExtension o) { skins.put(id, h, pr, o); }
- @Override public SkinHandler skins() { return resolveWithConfig("skins", skins); }
- @Override public SkinHandler skins(String id) { return skins.byId(id); }
- @Override public Collection allSkins() { return skins.all(); }
- @Override public boolean hasSkins(String id) { return skins.has(id); }
-
- // ---- Cleanup ---------------------------------------------------------
-
- @Override
- public void unregisterAll(MenuExtension owner) {
- economy.unregisterAll(owner);
- permissions.unregisterAll(owner);
- levels.unregisterAll(owner);
- placeholders.unregisterAll(owner);
- skins.unregisterAll(owner);
- }
-
- // ---- Inner section ---------------------------------------------------
-
- private static final class Section {
- private final Map> byId = new LinkedHashMap<>();
- private final Map> keysByOwner = new IdentityHashMap<>();
-
- synchronized void put(String id, T handler, int priority, MenuExtension owner) {
- String k = id.toLowerCase();
- byId.put(k, new Entry<>(handler, priority));
- keysByOwner.computeIfAbsent(owner, o -> new HashSet<>()).add(k);
- }
-
- synchronized T byId(String id) {
- Entry e = byId.get(id.toLowerCase());
- return e == null ? null : e.handler;
- }
-
- synchronized boolean has(String id) {
- return byId.containsKey(id.toLowerCase());
- }
-
- synchronized T auto() {
- Entry best = null;
- for (Entry e : byId.values()) {
- if (best == null || e.priority > best.priority) best = e;
- }
- return best == null ? null : best.handler;
- }
-
- synchronized Collection all() {
- List list = new ArrayList<>(byId.size());
- for (Entry e : byId.values()) list.add(e.handler);
- return Collections.unmodifiableList(list);
- }
-
- synchronized void unregisterAll(MenuExtension owner) {
- Set keys = keysByOwner.remove(owner);
- if (keys == null) return;
- for (String k : keys) byId.remove(k);
- }
-
- private static final class Entry {
- final T handler;
- final int priority;
- Entry(T handler, int priority) { this.handler = handler; this.priority = priority; }
- }
- }
-}
diff --git a/plugin/src/main/java/ru/abstractmenus/api/TypeRegistryImpl.java b/plugin/src/main/java/ru/abstractmenus/api/TypeRegistryImpl.java
deleted file mode 100644
index 225e081..0000000
--- a/plugin/src/main/java/ru/abstractmenus/api/TypeRegistryImpl.java
+++ /dev/null
@@ -1,100 +0,0 @@
-package ru.abstractmenus.api;
-
-import ru.abstractmenus.hocon.api.serialize.NodeSerializer;
-import ru.abstractmenus.hocon.api.serialize.NodeSerializers;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.IdentityHashMap;
-import java.util.Map;
-import java.util.Set;
-import java.util.logging.Logger;
-
-/**
- * In-memory {@link TypeRegistry} implementation. Not thread-safe for
- * concurrent registration; consumers must register/unregister from the main
- * server thread.
- *
- * Note on {@code NodeSerializers.unregister}: hocon 1.0.6 does NOT expose
- * an {@code unregister(Class)} method. Therefore stale serializer entries
- * survive in {@link NodeSerializers} after {@link #unregisterAll(MenuExtension)},
- * but that is harmless — the {@link #byKey} map is the authoritative lookup
- * table, and a subsequent {@link #register} call for the same class token
- * overwrites the serializer entry.
- */
-public final class TypeRegistryImpl implements TypeRegistry {
-
- private static final Logger LOG = Logger.getLogger(TypeRegistryImpl.class.getName());
-
- private final NodeSerializers serializers;
-
- /** key (lowercased) → registered class */
- private final Map> byKey = new HashMap<>();
-
- /** class → key (reverse index, kept in sync with {@link #byKey}) */
- private final Map, String> byType = new IdentityHashMap<>();
-
- /** owner → set of keys they registered (for unregisterAll) */
- private final Map> keysByOwner = new IdentityHashMap<>();
-
- public TypeRegistryImpl(NodeSerializers serializers) {
- this.serializers = serializers;
- }
-
- @Override
- public synchronized void register(String key,
- Class type,
- NodeSerializer serializer,
- MenuExtension owner) {
- String k = key.toLowerCase();
-
- Class extends T> existing = byKey.get(k);
- if (existing != null) {
- LOG.warning("TypeRegistry: overwriting existing entry '" + k
- + "' (" + existing.getName() + " -> " + type.getName() + ")");
- byType.remove(existing);
- // Intentionally do not remove from owner tracking — the old owner
- // no longer has this key since it's overwritten; cleanup of their
- // orphan entries happens on their own unregisterAll.
- }
-
- byKey.put(k, type);
- byType.put(type, k);
- serializers.register(type, serializer);
-
- keysByOwner.computeIfAbsent(owner, o -> new HashSet<>()).add(k);
- }
-
- @Override
- public synchronized Class extends T> get(String key) {
- return byKey.get(key.toLowerCase());
- }
-
- @Override
- public synchronized String name(Class extends T> type) {
- return byType.get(type);
- }
-
- @Override
- public synchronized Set keys() {
- return Collections.unmodifiableSet(new HashSet<>(byKey.keySet()));
- }
-
- @Override
- public synchronized void unregisterAll(MenuExtension owner) {
- Set keys = keysByOwner.remove(owner);
- if (keys == null) return;
-
- for (String k : keys) {
- Class extends T> type = byKey.remove(k);
- if (type != null) {
- byType.remove(type);
- // NodeSerializers.unregister(Class) does not exist in hocon 1.0.6.
- // The stale serializer entry in NodeSerializers is harmless —
- // byKey is authoritative, and re-registration overwrites it.
- // serializers.unregister(type);
- }
- }
- }
-}
diff --git a/plugin/src/main/java/ru/abstractmenus/command/Command.java b/plugin/src/main/java/ru/abstractmenus/command/Command.java
index 742f805..a8366de 100644
--- a/plugin/src/main/java/ru/abstractmenus/command/Command.java
+++ b/plugin/src/main/java/ru/abstractmenus/command/Command.java
@@ -68,7 +68,7 @@ public void execute(CommandSender sender, List args) {
} catch (Throwable t) {
if (arg.getDef() != null && sender instanceof Player) {
Player player = (Player) sender;
- String def = AbstractMenusApi.get().providers().placeholders().replace(player, arg.getDef());
+ String def = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, arg.getDef());
try {
Object defObj = arg.parse(sender, def);
diff --git a/plugin/src/main/java/ru/abstractmenus/commands/Command.java b/plugin/src/main/java/ru/abstractmenus/commands/Command.java
index ce9a88f..f580a39 100644
--- a/plugin/src/main/java/ru/abstractmenus/commands/Command.java
+++ b/plugin/src/main/java/ru/abstractmenus/commands/Command.java
@@ -9,10 +9,11 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.Deque;
+import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Stack;
public abstract class Command implements CommandExecutor, TabCompleter {
@@ -53,7 +54,9 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull org.bukkit.comm
if (!checkPermission(sender, this)) return false;
if (args.length > 0) {
- Stack stack = new Stack<>();
+ // ArrayDeque (not Stack — Stack extends Vector and synchronises
+ // every op for no benefit on a single-threaded dispatch path).
+ Deque stack = new ArrayDeque<>();
Command sub;
for (String arg : args) {
@@ -137,4 +140,22 @@ public List tabComplete(CommandSender sender, String[] args) {
return sub.tabComplete(sender, Arrays.copyOfRange(args, 1, args.length));
}
+ /**
+ * Helper for subclasses overriding {@link #tabComplete}: filter
+ * {@code candidates} to entries whose lowercase form starts with
+ * {@code prefix.toLowerCase()}, and return them sorted alphabetically.
+ *
+ * Pulled up to the base because at least three subcommand
+ * implementations (CommandAddons being the largest) used to ship a
+ * private static copy of this same five-line filter.
+ */
+ protected static List filterByPrefix(Iterable candidates, String prefix) {
+ String lower = prefix.toLowerCase();
+ List result = new ArrayList<>();
+ for (String s : candidates) {
+ if (s.toLowerCase().startsWith(lower)) result.add(s);
+ }
+ Collections.sort(result);
+ return result;
+ }
}
diff --git a/plugin/src/main/java/ru/abstractmenus/commands/am/CommandAddons.java b/plugin/src/main/java/ru/abstractmenus/commands/am/CommandAddons.java
index 54e4f57..3d73967 100644
--- a/plugin/src/main/java/ru/abstractmenus/commands/am/CommandAddons.java
+++ b/plugin/src/main/java/ru/abstractmenus/commands/am/CommandAddons.java
@@ -1,16 +1,22 @@
package ru.abstractmenus.commands.am;
import org.bukkit.command.CommandSender;
+import org.bukkit.plugin.PluginDescriptionFile;
+import org.bukkit.plugin.java.JavaPlugin;
import ru.abstractmenus.AbstractMenus;
import ru.abstractmenus.addon.AddonManager;
import ru.abstractmenus.addon.AddonStatus;
import ru.abstractmenus.addon.LoadedAddon;
+import ru.abstractmenus.api.MenuExtension;
import ru.abstractmenus.api.text.Colors;
import ru.abstractmenus.commands.Command;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Set;
+import java.util.regex.Pattern;
/** {@code /am addons [list|reload |info |load |rescan]} */
public class CommandAddons extends Command {
@@ -18,6 +24,39 @@ public class CommandAddons extends Command {
private static final List SUBCOMMANDS =
List.of("list", "reload", "info", "load", "rescan");
+ /**
+ * Strips legacy {@code &x}, section-sign {@code §x}, and hex
+ * {@code <#RRGGBB>} formatting tokens from a string. Applied to every
+ * addon-supplied value (names, versions, exception messages, etc.)
+ * before it is rendered through {@link Colors#of} into operator chat.
+ *
+ * Without this, a malicious addon could put {@code "&aOK, password=XYZ"}
+ * in its addon.conf name or in an exception message and have it render
+ * as a green "legitimate-looking" line in the operator's console-out
+ * mirror. Threat model is operator-installed third-party addons - they
+ * are semi-trusted but should not be able to social-engineer the
+ * operator via formatted output.
+ */
+ private static final Pattern UNSAFE_FORMAT = Pattern.compile(
+ "&[0-9a-fk-orA-FK-OR]|§[0-9a-fk-orA-FK-OR]|<#[0-9a-fA-F]{6}>");
+
+ /**
+ * Strips MiniMessage tags ({@code }, {@code },
+ * {@code }, etc.). Applied alongside
+ * {@link #UNSAFE_FORMAT} when MiniMessage rendering is enabled - without
+ * it a malicious addon could put a {@code }
+ * tag in its name and turn an operator's {@code /am addons list} click
+ * into privilege escalation. Conservatively matches anything shaped like
+ * {@code }; legit addon names don't need angle brackets.
+ */
+ private static final Pattern MM_TAG = Pattern.compile("<[/!?]?[a-zA-Z][^>]*>");
+
+ private static String safe(String untrusted) {
+ if (untrusted == null) return "";
+ String stripped = UNSAFE_FORMAT.matcher(untrusted).replaceAll("");
+ return MM_TAG.matcher(stripped).replaceAll("");
+ }
+
public CommandAddons() {
setUsage(
Colors.of("&7/am addons list &e- list all AM-loaded addons"),
@@ -42,33 +81,81 @@ public void execute(CommandSender sender, String[] args) {
}
switch (args[0].toLowerCase()) {
- case "list" -> list(sender, am);
- case "reload" -> reload(sender, am, args);
- case "info" -> info(sender, am, args);
- case "load" -> load(sender, am, args);
- case "rescan" -> rescan(sender, am);
- default -> sender.sendMessage(getUsage());
+ case "list" -> list(sender, am);
+ case "reload" -> reload(sender, am, args);
+ case "info" -> info(sender, am, args);
+ case "load" -> load(sender, am, args);
+ case "rescan" -> rescan(sender, am);
+ default -> sender.sendMessage(getUsage());
}
}
private void list(CommandSender sender, AddonManager am) {
- var addons = am.loaded();
- if (addons.isEmpty()) {
- sender.sendMessage(Colors.of("&7No AM-loaded addons."));
+ var path2 = am.loaded();
+ Set path1 = pathOneExtensions(am);
+ MenuExtension core = AbstractMenus.instance().getCore();
+
+ int total = path2.size() + path1.size() + (core != null ? 1 : 0);
+ if (total == 0) {
+ sender.sendMessage(Colors.of("&7No addons."));
return;
}
- sender.sendMessage(Colors.of("&e&lAddons (" + addons.size() + "):"));
- for (LoadedAddon la : addons) {
+
+ sender.sendMessage(Colors.of("&e&lAddons (" + total + "):"));
+
+ // Path 2 — render unchanged.
+ for (LoadedAddon la : path2) {
String color = switch (la.getStatus()) {
- case ENABLED -> "&a";
+ case ENABLED -> "&a";
case DISABLED -> "&7";
- case FAILED -> "&c";
- case PENDING -> "&e";
+ case FAILED -> "&c";
+ case PENDING -> "&e";
};
- sender.sendMessage(Colors.of(color + " " + la.getConf().name()
- + " &8v" + la.getConf().version()
+ sender.sendMessage(Colors.of(color + " " + safe(la.getConf().name())
+ + " &8v" + safe(la.getConf().version())
+ " &7[" + la.getStatus() + "]"));
}
+
+ // Path 1 — derived state from the JavaPlugin lifecycle when available.
+ for (MenuExtension ext : path1) {
+ boolean enabled = !(ext instanceof JavaPlugin jp) || jp.isEnabled();
+ String color = enabled ? "&a" : "&c";
+ String status = enabled ? "ENABLED" : "DISABLED";
+ sender.sendMessage(Colors.of(color + " " + safe(ext.name())
+ + " &8v" + safe(ext.version())
+ + " &7[" + status + "] &8[as-plugin]"));
+ }
+
+ // Built-in (CoreExtension) — last so the operator's eye lands on
+ // operator-installed addons first.
+ if (core != null) {
+ sender.sendMessage(Colors.of("&a " + safe(core.name())
+ + " &8v" + safe(core.version())
+ + " &7[ENABLED] &8[built-in]"));
+ }
+ }
+
+ /**
+ * Path 1 plugin-as-addons: every {@link MenuExtension} we see in the
+ * registry footprint that is neither a Path 2 AM-loaded addon nor the
+ * built-in {@code CoreExtension}.
+ */
+ private static Set pathOneExtensions(AddonManager am) {
+ // Names of Path 2 addons - their MenuExtension instances must be
+ // skipped from the "registry-footprint" set.
+ Set path2Exts = new LinkedHashSet<>();
+ for (LoadedAddon la : am.loaded()) {
+ if (la.getExtension() != null) path2Exts.add(la.getExtension());
+ }
+ MenuExtension core = AbstractMenus.instance().getCore();
+
+ Set path1 = new LinkedHashSet<>();
+ for (MenuExtension ext : am.knownExtensions()) {
+ if (ext == core) continue;
+ if (path2Exts.contains(ext)) continue;
+ path1.add(ext);
+ }
+ return path1;
}
private void reload(CommandSender sender, AddonManager am, String[] args) {
@@ -79,15 +166,15 @@ private void reload(CommandSender sender, AddonManager am, String[] args) {
String name = args[1];
var result = am.reload(name);
if (result.isEmpty()) {
- sender.sendMessage(Colors.of("&cAddon '" + name + "' not found or no jar present."));
+ sender.sendMessage(Colors.of("&cAddon '" + safe(name) + "' not found or no jar present."));
return;
}
LoadedAddon la = result.get();
if (la.getStatus() == AddonStatus.ENABLED) {
- sender.sendMessage(Colors.of("&aReloaded " + la.getConf().name() + "."));
+ sender.sendMessage(Colors.of("&aReloaded " + safe(la.getConf().name()) + "."));
} else {
sender.sendMessage(Colors.of("&cReload failed: "
- + (la.getError() == null ? "unknown error" : la.getError().getMessage())));
+ + (la.getError() == null ? "unknown error" : safe(la.getError().getMessage()))));
}
}
@@ -96,34 +183,85 @@ private void info(CommandSender sender, AddonManager am, String[] args) {
sender.sendMessage(Colors.of("&cUsage: /am addons info "));
return;
}
- var opt = am.get(args[1]);
- if (opt.isEmpty()) {
- sender.sendMessage(Colors.of("&cAddon '" + args[1] + "' not found."));
+ String name = args[1];
+
+ // Path 2 — full addon.conf metadata.
+ var opt = am.get(name);
+ if (opt.isPresent()) {
+ renderPathTwoInfo(sender, opt.get());
+ return;
+ }
+
+ // Built-in.
+ MenuExtension core = AbstractMenus.instance().getCore();
+ if (core != null && core.name().equalsIgnoreCase(name)) {
+ sender.sendMessage(Colors.of("&e&l" + safe(core.name()) + " &7v" + safe(core.version())));
+ sender.sendMessage(Colors.of("&7 status: &fENABLED &8[built-in]"));
return;
}
- LoadedAddon la = opt.get();
+
+ // Path 1 — surface the JavaPlugin description when available.
+ for (MenuExtension ext : pathOneExtensions(am)) {
+ if (!ext.name().equalsIgnoreCase(name)) continue;
+ renderPathOneInfo(sender, ext);
+ return;
+ }
+
+ sender.sendMessage(Colors.of("&cAddon '" + safe(name) + "' not found."));
+ }
+
+ private static void renderPathTwoInfo(CommandSender sender, LoadedAddon la) {
var c = la.getConf();
- sender.sendMessage(Colors.of("&e&l" + c.name() + " &7v" + c.version()));
+ sender.sendMessage(Colors.of("&e&l" + safe(c.name()) + " &7v" + safe(c.version())));
sender.sendMessage(Colors.of("&7 status: &f" + la.getStatus()));
if (!c.authors().isEmpty()) {
- sender.sendMessage(Colors.of("&7 authors: &f" + String.join(", ", c.authors())));
+ sender.sendMessage(Colors.of("&7 authors: &f" + safe(String.join(", ", c.authors()))));
}
if (!c.description().isEmpty()) {
- sender.sendMessage(Colors.of("&7 description: &f" + c.description()));
+ sender.sendMessage(Colors.of("&7 description: &f" + safe(c.description())));
}
if (c.targetApiVersion() != null) {
- sender.sendMessage(Colors.of("&7 targetApiVersion: &f" + c.targetApiVersion()));
+ sender.sendMessage(Colors.of("&7 targetApiVersion: &f" + safe(c.targetApiVersion())));
}
if (!c.addonDependencies().isEmpty()) {
sender.sendMessage(Colors.of("&7 addonDependencies: &f"
- + String.join(", ", c.addonDependencies())));
+ + safe(String.join(", ", c.addonDependencies()))));
}
if (!c.pluginDependencies().isEmpty()) {
sender.sendMessage(Colors.of("&7 pluginDependencies: &f"
- + String.join(", ", c.pluginDependencies())));
+ + safe(String.join(", ", c.pluginDependencies()))));
}
if (la.getStatus() == AddonStatus.FAILED && la.getError() != null) {
- sender.sendMessage(Colors.of("&7 error: &c" + la.getError().getMessage()));
+ sender.sendMessage(Colors.of("&7 error: &c" + safe(la.getError().getMessage())));
+ }
+ }
+
+ private static void renderPathOneInfo(CommandSender sender, MenuExtension ext) {
+ sender.sendMessage(Colors.of("&e&l" + safe(ext.name()) + " &7v" + safe(ext.version())
+ + " &8[as-plugin]"));
+
+ if (ext instanceof JavaPlugin jp) {
+ PluginDescriptionFile desc = jp.getDescription();
+ sender.sendMessage(Colors.of("&7 status: &f" + (jp.isEnabled() ? "ENABLED" : "DISABLED")));
+ if (!desc.getAuthors().isEmpty()) {
+ sender.sendMessage(Colors.of("&7 authors: &f"
+ + safe(String.join(", ", desc.getAuthors()))));
+ }
+ if (desc.getDescription() != null && !desc.getDescription().isEmpty()) {
+ sender.sendMessage(Colors.of("&7 description: &f" + safe(desc.getDescription())));
+ }
+ if (!desc.getDepend().isEmpty()) {
+ sender.sendMessage(Colors.of("&7 depend: &f"
+ + safe(String.join(", ", desc.getDepend()))));
+ }
+ if (!desc.getSoftDepend().isEmpty()) {
+ sender.sendMessage(Colors.of("&7 softDepend: &f"
+ + safe(String.join(", ", desc.getSoftDepend()))));
+ }
+ } else {
+ // Non-JavaPlugin Path 1 - rare but legal (an extension produced
+ // by a plugin's onEnable that isn't the plugin instance itself).
+ sender.sendMessage(Colors.of("&7 status: &fENABLED"));
}
}
@@ -135,17 +273,17 @@ private void load(CommandSender sender, AddonManager am, String[] args) {
String name = args[1];
var result = am.loadOne(name);
if (result.isEmpty()) {
- sender.sendMessage(Colors.of("&cNo unloaded addon named '" + name + "' found in addons/. "
+ sender.sendMessage(Colors.of("&cNo unloaded addon named '" + safe(name) + "' found in addons/. "
+ "Check the jar is in plugins/AbstractMenus/addons/ and addon.conf names it correctly."));
return;
}
LoadedAddon la = result.get();
if (la.getStatus() == AddonStatus.ENABLED) {
- sender.sendMessage(Colors.of("&aLoaded " + la.getConf().name()
- + " v" + la.getConf().version() + "."));
+ sender.sendMessage(Colors.of("&aLoaded " + safe(la.getConf().name())
+ + " v" + safe(la.getConf().version()) + "."));
} else {
sender.sendMessage(Colors.of("&cLoad failed: "
- + (la.getError() == null ? "unknown error" : la.getError().getMessage())));
+ + (la.getError() == null ? "unknown error" : safe(la.getError().getMessage()))));
}
}
@@ -162,12 +300,26 @@ private void rescan(CommandSender sender, AddonManager am) {
+ (failed > 0 ? ", &c" + failed + " failed" : "") + "&a."));
for (LoadedAddon la : newlyLoaded) {
String color = la.getStatus() == AddonStatus.ENABLED ? "&a" : "&c";
- sender.sendMessage(Colors.of(color + " " + la.getConf().name()
- + " &8v" + la.getConf().version()
+ sender.sendMessage(Colors.of(color + " " + safe(la.getConf().name())
+ + " &8v" + safe(la.getConf().version())
+ " &7[" + la.getStatus() + "]"));
}
}
+ /**
+ * Names valid for {@code /am addons info } tab-complete: every
+ * Path 2 loaded addon, every Path 1 plugin-as-addon, and the built-in
+ * core extension.
+ */
+ private static List allInfoNames(AddonManager am) {
+ List names = new ArrayList<>();
+ for (LoadedAddon la : am.loaded()) names.add(la.getConf().name());
+ for (MenuExtension ext : pathOneExtensions(am)) names.add(ext.name());
+ MenuExtension core = AbstractMenus.instance().getCore();
+ if (core != null) names.add(core.name());
+ return names;
+ }
+
@Override
public List tabComplete(CommandSender sender, String[] args) {
if (args.length == 0) return Collections.emptyList();
@@ -186,9 +338,13 @@ public List tabComplete(CommandSender sender, String[] args) {
if (am == null) return Collections.emptyList();
String prefix = args[1].toLowerCase();
return switch (args[0].toLowerCase()) {
- case "reload", "info" -> filterByPrefix(
- am.loaded().stream().map(la -> la.getConf().name()).toList(),
+ // reload only works on Path 2 (the only ones with a jar in
+ // addons/ to re-read).
+ case "reload" -> filterByPrefix(
+ () -> am.loaded().stream().map(la -> la.getConf().name()).iterator(),
prefix);
+ // info works on all three: Path 2, Path 1, built-in core.
+ case "info" -> filterByPrefix(allInfoNames(am), prefix);
case "load" -> filterByPrefix(am.availableNotLoaded(), prefix);
default -> Collections.emptyList();
};
@@ -197,12 +353,4 @@ public List tabComplete(CommandSender sender, String[] args) {
return Collections.emptyList();
}
- private static List filterByPrefix(List source, String prefix) {
- List result = new ArrayList<>();
- for (String s : source) {
- if (s.toLowerCase().startsWith(prefix)) result.add(s);
- }
- Collections.sort(result);
- return result;
- }
}
diff --git a/plugin/src/main/java/ru/abstractmenus/core/CoreActionsBundle.java b/plugin/src/main/java/ru/abstractmenus/core/CoreActionsBundle.java
index f604a43..ea5328f 100644
--- a/plugin/src/main/java/ru/abstractmenus/core/CoreActionsBundle.java
+++ b/plugin/src/main/java/ru/abstractmenus/core/CoreActionsBundle.java
@@ -95,11 +95,7 @@ void register(AbstractMenusApi api, MenuExtension owner) {
api.actions().register("setHealth", ActionHealthSet.class, new ActionHealthSet.Serializer(), owner);
api.actions().register("sound", ActionSound.class, new ActionSound.Serializer(), owner);
- try {
- // SoundCategory missing on legacy Bukkit
- api.actions().register("customSound", ActionSoundCustom.class, new ActionSoundCustom.Serializer(), owner);
- } catch (Throwable ignore) {
- }
+ api.actions().register("customSound", ActionSoundCustom.class, new ActionSoundCustom.Serializer(), owner);
api.actions().register("takeLevel", ActionLevelTake.class, new ActionLevelTake.Serializer(), owner);
api.actions().register("takeMoney", ActionMoneyTake.class, new ActionMoneyTake.Serializer(), owner);
diff --git a/plugin/src/main/java/ru/abstractmenus/core/CoreExtension.java b/plugin/src/main/java/ru/abstractmenus/core/CoreExtension.java
index 8d5649d..7994ee1 100644
--- a/plugin/src/main/java/ru/abstractmenus/core/CoreExtension.java
+++ b/plugin/src/main/java/ru/abstractmenus/core/CoreExtension.java
@@ -11,10 +11,18 @@
*
* The five bundles keep registration grouped by surface area for
* readability. Each is a pure function of {@code api} and the owning
- * {@code CoreExtension} instance — no static state.
+ * {@code CoreExtension} instance — no static state.
*/
public final class CoreExtension implements MenuExtension {
+ /**
+ * Captured at {@link #onEnable} time from {@link AbstractMenusApi#apiVersion()}
+ * so {@link #version()} no longer reports {@code null} for the core
+ * extension. Resolved lazily because at {@link #onLoad} time the API
+ * is already wired but version-string resolution is cheap regardless.
+ */
+ private String version;
+
@Override
public String name() {
return "AbstractMenus-Core";
@@ -22,12 +30,14 @@ public String name() {
@Override
public String version() {
- // Populated from plugin version at runtime via api.apiVersion().
- return null;
+ // Null between construction and onEnable - fall through to the
+ // default-style "unknown" so /am addons list never shows "vnull".
+ return version != null ? version : "unknown";
}
@Override
public void onEnable(AbstractMenusApi api) {
+ this.version = api.apiVersion();
new CoreActionsBundle().register(api, this);
new CoreRulesBundle().register(api, this);
new CoreItemPropsBundle().register(api, this);
diff --git a/plugin/src/main/java/ru/abstractmenus/core/CoreProvidersBundle.java b/plugin/src/main/java/ru/abstractmenus/core/CoreProvidersBundle.java
index aa63fc6..2de9473 100644
--- a/plugin/src/main/java/ru/abstractmenus/core/CoreProvidersBundle.java
+++ b/plugin/src/main/java/ru/abstractmenus/core/CoreProvidersBundle.java
@@ -38,7 +38,7 @@ void register(AbstractMenusApi api, MenuExtension owner) {
Bukkit.getServicesManager().getRegistration(Economy.class);
if (economyProvider != null) {
- api.providers().registerEconomy(
+ api.providers().economy().register(
"vault",
new EconomyVaultHandler(economyProvider.getProvider()),
CORE_PRIORITY,
@@ -57,7 +57,7 @@ void register(AbstractMenusApi api, MenuExtension owner) {
Bukkit.getServicesManager().getRegistration(LuckPerms.class);
if (provider != null) {
- api.providers().registerPermissions(
+ api.providers().permissions().register(
"luckperms",
new LuckPermsHandler(provider.getProvider()),
CORE_PRIORITY,
@@ -67,7 +67,7 @@ void register(AbstractMenusApi api, MenuExtension owner) {
Logger.severe("Cannot find registered LuckPerms service");
}
} else {
- api.providers().registerPermissions(
+ api.providers().permissions().register(
"default",
new PermissionDefaultHandler(plugin),
CORE_PRIORITY,
@@ -77,7 +77,7 @@ void register(AbstractMenusApi api, MenuExtension owner) {
}
// ---- Levels ---------------------------------------------------------
- api.providers().registerLevels(
+ api.providers().levels().register(
"default",
new LevelDefaultHandler(),
CORE_PRIORITY,
@@ -86,7 +86,7 @@ void register(AbstractMenusApi api, MenuExtension owner) {
// ---- Placeholders ---------------------------------------------------
if (AbstractMenus.checkDependency("PlaceholderAPI")) {
PlaceholderCustomHandler papiHandler = new PlaceholderCustomHandler();
- api.providers().registerPlaceholders(
+ api.providers().placeholders().register(
"placeholderapi",
papiHandler,
CORE_PRIORITY,
@@ -95,7 +95,7 @@ void register(AbstractMenusApi api, MenuExtension owner) {
Logger.info("Using PlaceholderAPI");
} else {
PlaceholderDefaultHandler defaultHandler = new PlaceholderDefaultHandler();
- api.providers().registerPlaceholders(
+ api.providers().placeholders().register(
"default",
defaultHandler,
CORE_PRIORITY,
@@ -106,7 +106,7 @@ void register(AbstractMenusApi api, MenuExtension owner) {
// ---- Skins ----------------------------------------------------------
if (AbstractMenus.checkDependency("SkinsRestorer")) {
- api.providers().registerSkins(
+ api.providers().skins().register(
"skinsrestorer",
new SkinsRestorerHandler(plugin.isProxyMode, plugin),
CORE_PRIORITY,
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionBookOpen.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionBookOpen.java
index 308f6aa..4273e0e 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionBookOpen.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionBookOpen.java
@@ -23,7 +23,7 @@ private ActionBookOpen(BookData bookData) {
@Override
public void activate(Player player, Menu menu, Item clickedItem) {
BookData data = bookData.clone();
- PlaceholderHandler handler = AbstractMenusApi.get().providers().placeholders();
+ PlaceholderHandler handler = AbstractMenusApi.get().providers().placeholders().resolve();
data.setAuthor(handler.replace(player, bookData.getAuthor()));
data.setTitle(handler.replace(player, bookData.getTitle()));
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionBroadcast.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionBroadcast.java
index c10e963..054fef1 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionBroadcast.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionBroadcast.java
@@ -70,7 +70,7 @@ private void setStay(TypeInt stay) {
public void activate(Player player, Menu menu, Item clickedItem) {
if (player != null) {
if (chatMessages != null) {
- List replaced = AbstractMenusApi.get().providers().placeholders().replace(player, chatMessages);
+ List replaced = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, chatMessages);
for (Player p : Bukkit.getOnlinePlayers()) {
MiniMessageUtil.sendParsed(replaced, p);
@@ -79,7 +79,7 @@ public void activate(Player player, Menu menu, Item clickedItem) {
if (json != null) {
BaseComponent[] component = ComponentSerializer.parse(
- AbstractMenusApi.get().providers().placeholders().replace(player, json));
+ AbstractMenusApi.get().providers().placeholders().resolve().replace(player, json));
if (component != null) {
for (Player p : Bukkit.getOnlinePlayers())
@@ -88,7 +88,7 @@ public void activate(Player player, Menu menu, Item clickedItem) {
}
if (actionbar != null) {
- String replaced = AbstractMenusApi.get().providers().placeholders().replace(player, actionbar);
+ String replaced = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, actionbar);
ActionBar bar = ActionBar.create();
for (Player p : Bukkit.getOnlinePlayers())
@@ -97,10 +97,10 @@ public void activate(Player player, Menu menu, Item clickedItem) {
if (!this.title.isEmpty() || !this.subtitle.isEmpty()) {
String title = MiniMessageUtil.parseToLegacy(
- AbstractMenusApi.get().providers().placeholders().replace(player, this.title)
+ AbstractMenusApi.get().providers().placeholders().resolve().replace(player, this.title)
);
String subtitle = MiniMessageUtil.parseToLegacy(
- AbstractMenusApi.get().providers().placeholders().replace(player, this.subtitle)
+ AbstractMenusApi.get().providers().placeholders().resolve().replace(player, this.subtitle)
);
Title t = new Title(
title, subtitle,
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionBungeeConnect.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionBungeeConnect.java
index 290e919..a037b05 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionBungeeConnect.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionBungeeConnect.java
@@ -20,7 +20,7 @@ private ActionBungeeConnect(String serverName) {
@Override
public void activate(Player player, Menu menu, Item clickedItem) {
- String replaced = AbstractMenusApi.get().providers().placeholders().replace(player, serverName);
+ String replaced = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, serverName);
BungeeManager.instance().sendPluginMessage(player, "Connect", replaced);
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionCommand.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionCommand.java
index bedb95c..8d054ca 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionCommand.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionCommand.java
@@ -3,7 +3,6 @@
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
-import ru.abstractmenus.AbstractMenus;
import ru.abstractmenus.api.Action;
import ru.abstractmenus.api.AbstractMenusApi;
import ru.abstractmenus.api.inventory.Item;
@@ -11,6 +10,8 @@
import ru.abstractmenus.hocon.api.ConfigNode;
import ru.abstractmenus.hocon.api.serialize.NodeSerializeException;
import ru.abstractmenus.hocon.api.serialize.NodeSerializer;
+import ru.abstractmenus.util.bukkit.BukkitTasks;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -35,18 +36,29 @@ private void setIgnorePlaceholder(boolean isIgnorePlaceholder) {
@Override
public void activate(Player player, Menu menu, Item clickedItem) {
- for (String command : playerCommands) {
- if (command != null) {
- String resultCommand = isIgnorePlaceholder ? command : AbstractMenusApi.get().providers().placeholders().replace(player, command);
- player.performCommand(resultCommand);
- }
+ if (!playerCommands.isEmpty()) {
+ // performCommand executes as the player; on Folia this requires
+ // the player's region thread.
+ BukkitTasks.runForEntity(player, () -> {
+ for (String command : playerCommands) {
+ if (command != null) {
+ String resultCommand = isIgnorePlaceholder ? command
+ : AbstractMenusApi.get().providers().placeholders().resolve().replace(player, command);
+ player.performCommand(resultCommand);
+ }
+ }
+ });
}
if (!consoleCommands.isEmpty()) {
- Bukkit.getServer().getGlobalRegionScheduler().execute(AbstractMenus.instance(), () -> {
+ // Console commands are not entity-scoped — global scheduler is
+ // correct on Folia. Route via BukkitTasks for consistency with
+ // the rest of the codebase.
+ BukkitTasks.runTask(() -> {
for (String command : consoleCommands) {
if (command != null) {
- String resultCommand = isIgnorePlaceholder ? command : AbstractMenusApi.get().providers().placeholders().replace(player, command);
+ String resultCommand = isIgnorePlaceholder ? command
+ : AbstractMenusApi.get().providers().placeholders().resolve().replace(player, command);
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), resultCommand);
}
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionGroupAdd.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionGroupAdd.java
index 464a29f..e54e974 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionGroupAdd.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionGroupAdd.java
@@ -19,8 +19,8 @@ private ActionGroupAdd(String group) {
@Override
public void activate(Player player, Menu menu, Item clickedItem) {
- String group = AbstractMenusApi.get().providers().placeholders().replace(player, this.group);
- AbstractMenusApi.get().providers().permissions().addGroup(player, group);
+ String group = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, this.group);
+ AbstractMenusApi.get().providers().permissions().resolve().addGroup(player, group);
}
public static class Serializer implements NodeSerializer {
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionGroupRemove.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionGroupRemove.java
index 2589caf..7b344b6 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionGroupRemove.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionGroupRemove.java
@@ -19,8 +19,8 @@ private ActionGroupRemove(String group) {
@Override
public void activate(Player player, Menu menu, Item clickedItem) {
- String groupName = AbstractMenusApi.get().providers().placeholders().replace(player, group);
- AbstractMenusApi.get().providers().permissions().removeGroup(player, groupName);
+ String groupName = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, group);
+ AbstractMenusApi.get().providers().permissions().resolve().removeGroup(player, groupName);
}
public static class Serializer implements NodeSerializer {
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionInputChat.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionInputChat.java
index ff4a5d3..440984b 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionInputChat.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionInputChat.java
@@ -34,7 +34,7 @@ public ActionInputChat(String varName, boolean global, String cancelWord,
@Override
public void activate(Player player, Menu m, Item clickedItem) {
- String name = AbstractMenusApi.get().providers().placeholders().replace(player, varName);
+ String name = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, varName);
InputAction action = new InputAction(player, name, global, cancelWord, onInput, onCancel);
MenuManager.instance().saveInputAction(action);
m.close(player);
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionItemRefresh.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionItemRefresh.java
index 9d19030..aaa9b6a 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionItemRefresh.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionItemRefresh.java
@@ -36,9 +36,9 @@ public void activate(Player player, Menu menu, Item clickedItem) {
if (ticks != null) {
Slot finalSlot = slot;
- BukkitTasks.runTaskLater(() ->
- menu.refreshItem(finalSlot, player), ticks.getInt(player, menu)
- );
+ BukkitTasks.runForEntityLater(player,
+ () -> menu.refreshItem(finalSlot, player),
+ ticks.getInt(player, menu));
return;
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionLevelGive.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionLevelGive.java
index 1a66c68..d24e1f4 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionLevelGive.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionLevelGive.java
@@ -20,7 +20,7 @@ private ActionLevelGive(TypeInt level) {
@Override
public void activate(Player player, Menu menu, Item clickedItem) {
- AbstractMenusApi.get().providers().levels().giveLevel(player, level.getInt(player, menu));
+ AbstractMenusApi.get().providers().levels().resolve().giveLevel(player, level.getInt(player, menu));
}
public static class Serializer implements NodeSerializer {
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionLevelTake.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionLevelTake.java
index 4cc2281..95e1273 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionLevelTake.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionLevelTake.java
@@ -21,7 +21,7 @@ private ActionLevelTake(TypeInt level) {
@Override
public void activate(Player player, Menu menu, Item clickedItem) {
- AbstractMenusApi.get().providers().levels().takeLevel(player, level.getInt(player, menu));
+ AbstractMenusApi.get().providers().levels().resolve().takeLevel(player, level.getInt(player, menu));
}
public static class Serializer implements NodeSerializer {
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionLog.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionLog.java
index 7ad1bcb..75a6117 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionLog.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionLog.java
@@ -20,7 +20,7 @@ private ActionLog(String message) {
@Override
public void activate(Player player, Menu menu, Item clickedItem) {
- Logger.info(AbstractMenusApi.get().providers().placeholders().replace(player, message));
+ Logger.info(AbstractMenusApi.get().providers().placeholders().resolve().replace(player, message));
}
public static class Serializer implements NodeSerializer {
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionLuckPermsMetaRemove.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionLuckPermsMetaRemove.java
index 88a1260..575dfcd 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionLuckPermsMetaRemove.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionLuckPermsMetaRemove.java
@@ -5,6 +5,8 @@
import org.bukkit.entity.Player;
import ru.abstractmenus.api.Action;
import ru.abstractmenus.api.AbstractMenusApi;
+import ru.abstractmenus.api.Logger;
+import ru.abstractmenus.api.handler.PermissionsHandler;
import ru.abstractmenus.api.inventory.Item;
import ru.abstractmenus.api.inventory.Menu;
import ru.abstractmenus.handlers.LuckPermsHandler;
@@ -21,11 +23,15 @@ public class ActionLuckPermsMetaRemove implements Action {
@Override
public void activate(Player player, Menu menu, Item clickedItem) {
- metaList.forEach(metaKey -> {
- if (AbstractMenusApi.get().providers().permissions() instanceof LuckPermsHandler handler) {
- handler.removeMeta(player, metaKey);
- }
- });
+ PermissionsHandler perms = AbstractMenusApi.get().providers().permissions().resolve();
+ if (!(perms instanceof LuckPermsHandler handler)) {
+ Logger.warning("lpMetaRemove skipped: active permissions provider "
+ + (perms == null ? "null" : perms.getClass().getSimpleName())
+ + " is not LuckPerms. "
+ + "Install LuckPerms or set 'providers.permissions = \"luckperms\"' in config.conf.");
+ return;
+ }
+ metaList.forEach(metaKey -> handler.removeMeta(player, metaKey));
}
public static class Serializer implements NodeSerializer {
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionLuckPermsMetaSet.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionLuckPermsMetaSet.java
index 377a474..c232116 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionLuckPermsMetaSet.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionLuckPermsMetaSet.java
@@ -5,6 +5,8 @@
import org.bukkit.entity.Player;
import ru.abstractmenus.api.Action;
import ru.abstractmenus.api.AbstractMenusApi;
+import ru.abstractmenus.api.Logger;
+import ru.abstractmenus.api.handler.PermissionsHandler;
import ru.abstractmenus.api.inventory.Item;
import ru.abstractmenus.api.inventory.Menu;
import ru.abstractmenus.data.properties.PropLPMeta;
@@ -23,11 +25,18 @@ public class ActionLuckPermsMetaSet implements Action {
@Override
public void activate(Player player, Menu menu, Item clickedItem) {
+ PermissionsHandler perms = AbstractMenusApi.get().providers().permissions().resolve();
+ if (!(perms instanceof LuckPermsHandler handler)) {
+ Logger.warning("lpMetaSet skipped: active permissions provider "
+ + (perms == null ? "null" : perms.getClass().getSimpleName())
+ + " is not LuckPerms. "
+ + "Install LuckPerms or set 'providers.permissions = \"luckperms\"' in config.conf.");
+ return;
+ }
metaList.forEach(meta -> {
- String replacedValue = isIgnorePlaceholder ? meta.getValue() : AbstractMenusApi.get().providers().placeholders().replace(player, meta.getValue());
- if (AbstractMenusApi.get().providers().permissions() instanceof LuckPermsHandler handler) {
- handler.addMeta(player, meta.getKey(), replacedValue);
- }
+ String replacedValue = isIgnorePlaceholder ? meta.getValue()
+ : AbstractMenusApi.get().providers().placeholders().resolve().replace(player, meta.getValue());
+ handler.addMeta(player, meta.getKey(), replacedValue);
});
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMenuClose.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMenuClose.java
index 74aca22..b22a283 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMenuClose.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMenuClose.java
@@ -29,8 +29,8 @@ private ActionMenuClose(TypeInt ticks) {
@Override
public void activate(Player player, Menu menu, Item clickedItem) {
if (ticks != null) {
- BukkitTasks.runTaskLater(() ->
- MenuManager.instance().closeMenu(player),
+ BukkitTasks.runForEntityLater(player,
+ () -> MenuManager.instance().closeMenu(player),
ticks.getInt(player, menu));
} else {
if (isClose.getBool(player, menu))
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMenuOpen.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMenuOpen.java
index aa39bbd..30c6fd5 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMenuOpen.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMenuOpen.java
@@ -21,7 +21,7 @@ private ActionMenuOpen(String menu) {
@Override
public void activate(Player player, Menu menu, Item clickedItem) {
- String name = AbstractMenusApi.get().providers().placeholders().replace(player, this.menu);
+ String name = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, this.menu);
Menu menuToOpen = MenuManager.instance().getMenu(name);
if (menuToOpen != null)
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMenuOpenCtx.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMenuOpenCtx.java
index f332513..047b008 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMenuOpenCtx.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMenuOpenCtx.java
@@ -22,7 +22,7 @@ private ActionMenuOpenCtx(String menu) {
@Override
public void activate(Player player, Menu menu, Item clickedItem) {
- String name = AbstractMenusApi.get().providers().placeholders().replace(player, this.menu);
+ String name = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, this.menu);
Menu menuToOpen = MenuManager.instance().getMenu(name);
if (menuToOpen != null) {
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMenuRefresh.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMenuRefresh.java
index d6190b6..17fc291 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMenuRefresh.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMenuRefresh.java
@@ -29,7 +29,9 @@ private ActionMenuRefresh(TypeInt ticks) {
@Override
public void activate(Player player, Menu menu, Item clickedItem) {
if (ticks != null) {
- BukkitTasks.runTaskLater(() -> MenuManager.instance().refreshMenu(player), ticks.getInt(player, menu));
+ BukkitTasks.runForEntityLater(player,
+ () -> MenuManager.instance().refreshMenu(player),
+ ticks.getInt(player, menu));
return;
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMessage.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMessage.java
index dddf207..a04bf72 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMessage.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMessage.java
@@ -69,29 +69,29 @@ private void setStay(TypeInt stay) {
public void activate(Player player, Menu menu, Item clickedItem) {
if (player != null) {
if (chatMessages != null) {
- List replaced = AbstractMenusApi.get().providers().placeholders().replace(player, chatMessages);
+ List replaced = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, chatMessages);
MiniMessageUtil.sendParsed(replaced, player);
}
if (json != null) {
BaseComponent[] component = ComponentSerializer.parse(
- AbstractMenusApi.get().providers().placeholders().replace(player, json));
+ AbstractMenusApi.get().providers().placeholders().resolve().replace(player, json));
if (component != null)
player.spigot().sendMessage(component);
}
if (actionbar != null) {
- String replaced = AbstractMenusApi.get().providers().placeholders().replace(player, actionbar);
+ String replaced = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, actionbar);
ActionBar.create().send(player, MiniMessageUtil.parseToLegacy(replaced));
}
if (!this.title.isEmpty() || !this.subtitle.isEmpty()) {
String title = MiniMessageUtil.parseToLegacy(
- AbstractMenusApi.get().providers().placeholders().replace(player, this.title)
+ AbstractMenusApi.get().providers().placeholders().resolve().replace(player, this.title)
);
String subtitle = MiniMessageUtil.parseToLegacy(
- AbstractMenusApi.get().providers().placeholders().replace(player, this.subtitle)
+ AbstractMenusApi.get().providers().placeholders().resolve().replace(player, this.subtitle)
);
new Title(
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMiniMessage.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMiniMessage.java
index 5cfbc30..1a10b4e 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMiniMessage.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMiniMessage.java
@@ -22,7 +22,7 @@ private ActionMiniMessage(String message) {
@Override
public void activate(Player player, Menu menu, Item clickedItem) {
- String replaced = AbstractMenusApi.get().providers().placeholders().replace(player, message);
+ String replaced = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, message);
MiniMessageUtil.sendParsed(Collections.singletonList(replaced), player);
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMoneyGive.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMoneyGive.java
index 6657241..f545ba3 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMoneyGive.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMoneyGive.java
@@ -25,8 +25,8 @@ private ActionMoneyGive(TypeDouble money, String provider) {
@Override
public void activate(Player player, Menu menu, Item clickedItem) {
EconomyHandler eco = provider != null
- ? AbstractMenusApi.get().providers().economy(provider)
- : AbstractMenusApi.get().providers().economy();
+ ? AbstractMenusApi.get().providers().economy().resolve(provider)
+ : AbstractMenusApi.get().providers().economy().resolve();
if (eco == null) {
return;
}
@@ -37,30 +37,8 @@ public static class Serializer implements NodeSerializer {
@Override
public ActionMoneyGive deserialize(Class type, ConfigNode node) throws NodeSerializeException {
- TypeDouble money;
- String provider = null;
-
- if (node.isMap()) {
- money = node.node("amount").getValue(TypeDouble.class);
- provider = node.node("provider").getString(null);
- } else {
- money = node.getValue(TypeDouble.class);
- }
-
- if (provider != null) {
- if (!AbstractMenusApi.get().providers().hasEconomy(provider)) {
- StringBuilder known = new StringBuilder();
- for (EconomyHandler h : AbstractMenusApi.get().providers().allEconomy()) {
- if (known.length() > 0) known.append(", ");
- known.append(h.getClass().getSimpleName());
- }
- throw new NodeSerializeException(node,
- "Unknown economy provider '" + provider + "'. Registered: ["
- + known + "]. Omit the 'provider' field for default selection.");
- }
- }
-
- return new ActionMoneyGive(money, provider);
+ MoneyAmountSpec spec = MoneyAmountSpec.parse(node);
+ return new ActionMoneyGive(spec.amount, spec.provider);
}
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMoneyTake.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMoneyTake.java
index 757c2fe..ce4ecc1 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMoneyTake.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionMoneyTake.java
@@ -25,8 +25,8 @@ private ActionMoneyTake(TypeDouble money, String provider) {
@Override
public void activate(Player player, Menu menu, Item clickedItem) {
EconomyHandler eco = provider != null
- ? AbstractMenusApi.get().providers().economy(provider)
- : AbstractMenusApi.get().providers().economy();
+ ? AbstractMenusApi.get().providers().economy().resolve(provider)
+ : AbstractMenusApi.get().providers().economy().resolve();
if (eco == null) {
return;
}
@@ -37,30 +37,8 @@ public static class Serializer implements NodeSerializer {
@Override
public ActionMoneyTake deserialize(Class type, ConfigNode node) throws NodeSerializeException {
- TypeDouble money;
- String provider = null;
-
- if (node.isMap()) {
- money = node.node("amount").getValue(TypeDouble.class);
- provider = node.node("provider").getString(null);
- } else {
- money = node.getValue(TypeDouble.class);
- }
-
- if (provider != null) {
- if (!AbstractMenusApi.get().providers().hasEconomy(provider)) {
- StringBuilder known = new StringBuilder();
- for (EconomyHandler h : AbstractMenusApi.get().providers().allEconomy()) {
- if (known.length() > 0) known.append(", ");
- known.append(h.getClass().getSimpleName());
- }
- throw new NodeSerializeException(node,
- "Unknown economy provider '" + provider + "'. Registered: ["
- + known + "]. Omit the 'provider' field for default selection.");
- }
- }
-
- return new ActionMoneyTake(money, provider);
+ MoneyAmountSpec spec = MoneyAmountSpec.parse(node);
+ return new ActionMoneyTake(spec.amount, spec.provider);
}
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionPermissionGive.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionPermissionGive.java
index f2e5667..bd3a675 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionPermissionGive.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionPermissionGive.java
@@ -27,8 +27,8 @@ public void setIgnorePlaceholder(boolean ignorePlaceholder) {
@Override
public void activate(Player player, Menu menu, Item clickedItem) {
permissions.forEach(perm -> {
- String replaced = isIgnorePlaceholder ? perm : AbstractMenusApi.get().providers().placeholders().replace(player, perm);
- AbstractMenusApi.get().providers().permissions().addPermission(player, replaced);
+ String replaced = isIgnorePlaceholder ? perm : AbstractMenusApi.get().providers().placeholders().resolve().replace(player, perm);
+ AbstractMenusApi.get().providers().permissions().resolve().addPermission(player, replaced);
});
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionPermissionRemove.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionPermissionRemove.java
index 4afe92b..c884603 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionPermissionRemove.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionPermissionRemove.java
@@ -23,8 +23,8 @@ private ActionPermissionRemove(List permissions) {
@Override
public void activate(Player player, Menu menu, Item clickedItem) {
permissions.forEach(perm -> {
- String replaced = AbstractMenusApi.get().providers().placeholders().replace(player, perm);
- AbstractMenusApi.get().providers().permissions().removePermission(player, replaced);
+ String replaced = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, perm);
+ AbstractMenusApi.get().providers().permissions().resolve().removePermission(player, replaced);
});
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionPlayerChat.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionPlayerChat.java
index 16aacbb..8985598 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionPlayerChat.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionPlayerChat.java
@@ -9,6 +9,7 @@
import ru.abstractmenus.api.inventory.Item;
import ru.abstractmenus.api.inventory.Menu;
import ru.abstractmenus.util.MiniMessageUtil;
+import ru.abstractmenus.util.bukkit.BukkitTasks;
import java.util.List;
@@ -22,7 +23,11 @@ private ActionPlayerChat(List messages) {
@Override
public void activate(Player player, Menu menu, Item clickedItem) {
- messages.forEach(player::chat);
+ // player.chat() requires the player's region thread on Folia. The
+ // activator that fires us may be running on the global scheduler
+ // (e.g., from a chained chat-input action), so dispatch via the
+ // entity scheduler.
+ BukkitTasks.runForEntity(player, () -> messages.forEach(player::chat));
}
public static class Serializer implements NodeSerializer {
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionSkinReset.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionSkinReset.java
index ae71ab0..b3d28a2 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionSkinReset.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionSkinReset.java
@@ -21,7 +21,7 @@ private ActionSkinReset(TypeBool reset) {
public void activate(Player player, Menu menu, Item clickedItem) {
if (reset.getBool(player, menu)) {
- AbstractMenusApi.get().providers().skins().resetSkin(player);
+ AbstractMenusApi.get().providers().skins().resolve().resetSkin(player);
}
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionSkinSet.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionSkinSet.java
index ef7631e..a221b2e 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionSkinSet.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionSkinSet.java
@@ -21,9 +21,9 @@ private ActionSkinSet(String texture, String signature) {
}
public void activate(Player player, Menu menu, Item clickedItem) {
- AbstractMenusApi.get().providers().skins().setSkin(player,
- AbstractMenusApi.get().providers().placeholders().replace(player, texture),
- AbstractMenusApi.get().providers().placeholders().replace(player, signature));
+ AbstractMenusApi.get().providers().skins().resolve().setSkin(player,
+ AbstractMenusApi.get().providers().placeholders().resolve().replace(player, texture),
+ AbstractMenusApi.get().providers().placeholders().resolve().replace(player, signature));
}
public static class Serializer implements NodeSerializer {
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionTeleport.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionTeleport.java
index 9aa3cce..6e7769f 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionTeleport.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionTeleport.java
@@ -20,7 +20,10 @@ private ActionTeleport(TypeLocation location) {
@Override
public void activate(Player player, Menu menu, Item clickedItem) {
if (location != null) {
- player.teleport(location.getLocation(player, menu));
+ // teleportAsync handles cross-region teleports on Folia (the sync
+ // teleport throws IllegalStateException when crossing regions) and
+ // is the recommended call on Paper 1.21+ regardless.
+ player.teleportAsync(location.getLocation(player, menu));
}
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionXpGive.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionXpGive.java
index 7641dc6..c873612 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionXpGive.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionXpGive.java
@@ -21,7 +21,7 @@ private ActionXpGive(TypeInt xp) {
@Override
public void activate(Player player, Menu menu, Item clickedItem) {
- AbstractMenusApi.get().providers().levels().giveXp(player, xp.getInt(player, menu));
+ AbstractMenusApi.get().providers().levels().resolve().giveXp(player, xp.getInt(player, menu));
}
public static class Serializer implements NodeSerializer {
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionXpTake.java b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionXpTake.java
index a3a9a10..099ed59 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/ActionXpTake.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/ActionXpTake.java
@@ -20,7 +20,7 @@ private ActionXpTake(TypeInt xp) {
@Override
public void activate(Player player, Menu menu, Item clickedItem) {
- AbstractMenusApi.get().providers().levels().takeXp(player, xp.getInt(player, menu));
+ AbstractMenusApi.get().providers().levels().resolve().takeXp(player, xp.getInt(player, menu));
}
public static class Serializer implements NodeSerializer {
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/MoneyAmountSpec.java b/plugin/src/main/java/ru/abstractmenus/data/actions/MoneyAmountSpec.java
new file mode 100644
index 0000000..aaa7feb
--- /dev/null
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/MoneyAmountSpec.java
@@ -0,0 +1,61 @@
+package ru.abstractmenus.data.actions;
+
+import ru.abstractmenus.api.AbstractMenusApi;
+import ru.abstractmenus.datatype.TypeDouble;
+import ru.abstractmenus.hocon.api.ConfigNode;
+import ru.abstractmenus.hocon.api.serialize.NodeSerializeException;
+
+/**
+ * Shared deserializer for HOCON shapes used by money-aware actions and
+ * rules ({@code takeMoney}, {@code giveMoney}, {@code hasMoney}).
+ *
+ * Accepts both forms:
+ *
+ *
{@code
+ * takeMoney: 100 # scalar - default provider
+ * takeMoney { amount: 100, provider: "vault" } # map - explicit provider
+ * }
+ *
+ * Validates the optional {@code provider} field at parse time so menus
+ * fail fast if they reference an unregistered economy provider, with an
+ * error that lists the actually-registered ids ({@code "vault"},
+ * {@code "playerpoints"}, etc.) instead of impl class names. Three
+ * call sites (TakeMoney, GiveMoney, RuleMoney) used to duplicate this
+ * 18-line block; consolidated here.
+ */
+public final class MoneyAmountSpec {
+
+ public final TypeDouble amount;
+ public final String provider;
+
+ private MoneyAmountSpec(TypeDouble amount, String provider) {
+ this.amount = amount;
+ this.provider = provider;
+ }
+
+ /**
+ * Parse a {@code TypeDouble amount} plus optional {@code provider} from
+ * either the scalar form or the map form. Throws on unknown provider
+ * with a helpful message.
+ */
+ public static MoneyAmountSpec parse(ConfigNode node) throws NodeSerializeException {
+ TypeDouble amount;
+ String provider = null;
+
+ if (node.isMap()) {
+ amount = node.node("amount").getValue(TypeDouble.class);
+ provider = node.node("provider").getString(null);
+ } else {
+ amount = node.getValue(TypeDouble.class);
+ }
+
+ if (provider != null && !AbstractMenusApi.get().providers().economy().has(provider)) {
+ String known = String.join(", ", AbstractMenusApi.get().providers().economy().ids());
+ throw new NodeSerializeException(node,
+ "Unknown economy provider '" + provider + "'. Registered: ["
+ + known + "]. Omit the 'provider' field for default selection.");
+ }
+
+ return new MoneyAmountSpec(amount, provider);
+ }
+}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/var/ActionVarDec.java b/plugin/src/main/java/ru/abstractmenus/data/actions/var/ActionVarDec.java
index 8fb3990..eb0b56f 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/var/ActionVarDec.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/var/ActionVarDec.java
@@ -24,14 +24,14 @@ private ActionVarDec(List dataList) {
public void activate(Player p, Menu menu, Item clickedItem) {
for (VarNumData data : dataList) {
- String varName = AbstractMenusApi.get().providers().placeholders().replace(p, data.getName());
+ String varName = AbstractMenusApi.get().providers().placeholders().resolve().replace(p, data.getName());
double value = data.getValue().getDouble(p, menu);
Function func = num -> num - value;
if (data.getPlayer() == null) {
VariableManagerImpl.instance().modifyNumericGlobal(varName, func);
} else {
- String playerName = AbstractMenusApi.get().providers().placeholders().replace(p, data.getPlayer());
+ String playerName = AbstractMenusApi.get().providers().placeholders().resolve().replace(p, data.getPlayer());
VariableManagerImpl.instance().modifyNumericPersonal(playerName, varName, func);
}
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/var/ActionVarDiv.java b/plugin/src/main/java/ru/abstractmenus/data/actions/var/ActionVarDiv.java
index c4131f5..fcefe9c 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/var/ActionVarDiv.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/var/ActionVarDiv.java
@@ -25,7 +25,7 @@ private ActionVarDiv(List dataList) {
public void activate(Player p, Menu menu, Item clickedItem) {
for (VarNumData data : dataList) {
- String varName = AbstractMenusApi.get().providers().placeholders().replace(p, data.getName());
+ String varName = AbstractMenusApi.get().providers().placeholders().resolve().replace(p, data.getName());
double value = data.getValue().getDouble(p, menu);
if (value == 0) {
@@ -38,7 +38,7 @@ public void activate(Player p, Menu menu, Item clickedItem) {
if (data.getPlayer() == null) {
VariableManagerImpl.instance().modifyNumericGlobal(varName, func);
} else {
- String playerName = AbstractMenusApi.get().providers().placeholders().replace(p, data.getPlayer());
+ String playerName = AbstractMenusApi.get().providers().placeholders().resolve().replace(p, data.getPlayer());
VariableManagerImpl.instance().modifyNumericPersonal(playerName, varName, func);
}
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/var/ActionVarInc.java b/plugin/src/main/java/ru/abstractmenus/data/actions/var/ActionVarInc.java
index 363a635..75ed52d 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/var/ActionVarInc.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/var/ActionVarInc.java
@@ -24,14 +24,14 @@ private ActionVarInc(List dataList) {
public void activate(Player p, Menu menu, Item clickedItem) {
for (VarNumData data : dataList) {
- String varName = AbstractMenusApi.get().providers().placeholders().replace(p, data.getName());
+ String varName = AbstractMenusApi.get().providers().placeholders().resolve().replace(p, data.getName());
double value = data.getValue().getDouble(p, menu);
Function func = num -> num + value;
if (data.getPlayer() == null) {
VariableManagerImpl.instance().modifyNumericGlobal(varName, func);
} else {
- String playerName = AbstractMenusApi.get().providers().placeholders().replace(p, data.getPlayer());
+ String playerName = AbstractMenusApi.get().providers().placeholders().resolve().replace(p, data.getPlayer());
VariableManagerImpl.instance().modifyNumericPersonal(playerName, varName, func);
}
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/var/ActionVarMul.java b/plugin/src/main/java/ru/abstractmenus/data/actions/var/ActionVarMul.java
index 44f1c0b..62f9a23 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/var/ActionVarMul.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/var/ActionVarMul.java
@@ -24,14 +24,14 @@ private ActionVarMul(List dataList) {
public void activate(Player p, Menu menu, Item clickedItem) {
for (VarNumData data : dataList) {
- String varName = AbstractMenusApi.get().providers().placeholders().replace(p, data.getName());
+ String varName = AbstractMenusApi.get().providers().placeholders().resolve().replace(p, data.getName());
double value = data.getValue().getDouble(p, menu);
Function func = num -> num * value;
if (data.getPlayer() == null) {
VariableManagerImpl.instance().modifyNumericGlobal(varName, func);
} else {
- String playerName = AbstractMenusApi.get().providers().placeholders().replace(p, data.getPlayer());
+ String playerName = AbstractMenusApi.get().providers().placeholders().resolve().replace(p, data.getPlayer());
VariableManagerImpl.instance().modifyNumericPersonal(playerName, varName, func);
}
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/var/ActionVarRem.java b/plugin/src/main/java/ru/abstractmenus/data/actions/var/ActionVarRem.java
index d881d35..df23c48 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/var/ActionVarRem.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/var/ActionVarRem.java
@@ -23,12 +23,12 @@ private ActionVarRem(List dataList) {
public void activate(Player p, Menu menu, Item clickedItem) {
for (VarData data : dataList) {
- String varName = AbstractMenusApi.get().providers().placeholders().replace(p, data.getName());
+ String varName = AbstractMenusApi.get().providers().placeholders().resolve().replace(p, data.getName());
if (data.getPlayer() == null) {
VariableManagerImpl.instance().deleteGlobal(varName);
} else {
- String playerName = AbstractMenusApi.get().providers().placeholders().replace(p, data.getPlayer());
+ String playerName = AbstractMenusApi.get().providers().placeholders().resolve().replace(p, data.getPlayer());
VariableManagerImpl.instance().deletePersonal(playerName, varName);
}
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/var/ActionVarSet.java b/plugin/src/main/java/ru/abstractmenus/data/actions/var/ActionVarSet.java
index 6d61a24..341ad98 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/var/ActionVarSet.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/var/ActionVarSet.java
@@ -25,10 +25,10 @@ private ActionVarSet(List dataList) {
public void activate(Player p, Menu menu, Item clickedItem) {
for (VarData data : dataList) {
- String varName = AbstractMenusApi.get().providers().placeholders().replace(p, data.getName());
- String varVal = AbstractMenusApi.get().providers().placeholders().replace(p, data.getValue());
+ String varName = AbstractMenusApi.get().providers().placeholders().resolve().replace(p, data.getName());
+ String varVal = AbstractMenusApi.get().providers().placeholders().resolve().replace(p, data.getValue());
- long time = TimeUtil.parseTime(AbstractMenusApi.get().providers().placeholders().replace(p, data.getTime()));
+ long time = TimeUtil.parseTime(AbstractMenusApi.get().providers().placeholders().resolve().replace(p, data.getTime()));
boolean replace = data.isReplace().getBool(p, menu);
Var var = VariableManagerImpl.instance().createBuilder()
@@ -40,7 +40,7 @@ public void activate(Player p, Menu menu, Item clickedItem) {
if (data.getPlayer() == null) {
VariableManagerImpl.instance().saveGlobal(var, replace);
} else {
- String playerName = AbstractMenusApi.get().providers().placeholders().replace(p, data.getPlayer());
+ String playerName = AbstractMenusApi.get().providers().placeholders().resolve().replace(p, data.getPlayer());
VariableManagerImpl.instance().savePersonal(playerName, var, replace);
}
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/varp/ActionVarpDec.java b/plugin/src/main/java/ru/abstractmenus/data/actions/varp/ActionVarpDec.java
index 8e52c7d..4dd0a9d 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/varp/ActionVarpDec.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/varp/ActionVarpDec.java
@@ -24,7 +24,7 @@ private ActionVarpDec(List dataList) {
public void activate(Player p, Menu menu, Item clickedItem) {
for (VarNumData data : dataList) {
- String varName = AbstractMenusApi.get().providers().placeholders().replace(p, data.getName());
+ String varName = AbstractMenusApi.get().providers().placeholders().resolve().replace(p, data.getName());
double value = data.getValue().getDouble(p, menu);
Function func = num -> num - value;
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/varp/ActionVarpDiv.java b/plugin/src/main/java/ru/abstractmenus/data/actions/varp/ActionVarpDiv.java
index 869cf3a..d9a02f1 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/varp/ActionVarpDiv.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/varp/ActionVarpDiv.java
@@ -25,7 +25,7 @@ private ActionVarpDiv(List dataList) {
public void activate(Player p, Menu menu, Item clickedItem) {
for (VarNumData data : dataList) {
- String varName = AbstractMenusApi.get().providers().placeholders().replace(p, data.getName());
+ String varName = AbstractMenusApi.get().providers().placeholders().resolve().replace(p, data.getName());
double value = data.getValue().getDouble(p, menu);
if (value == 0) {
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/varp/ActionVarpInc.java b/plugin/src/main/java/ru/abstractmenus/data/actions/varp/ActionVarpInc.java
index dbeb048..eb7934c 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/varp/ActionVarpInc.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/varp/ActionVarpInc.java
@@ -24,7 +24,7 @@ private ActionVarpInc(List dataList) {
public void activate(Player p, Menu menu, Item clickedItem) {
for (VarNumData data : dataList) {
- String varName = AbstractMenusApi.get().providers().placeholders().replace(p, data.getName());
+ String varName = AbstractMenusApi.get().providers().placeholders().resolve().replace(p, data.getName());
double value = data.getValue().getDouble(p, menu);
Function func = num -> num + value;
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/varp/ActionVarpMul.java b/plugin/src/main/java/ru/abstractmenus/data/actions/varp/ActionVarpMul.java
index 2ca0fbf..1f47a03 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/varp/ActionVarpMul.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/varp/ActionVarpMul.java
@@ -24,7 +24,7 @@ private ActionVarpMul(List dataList) {
public void activate(Player p, Menu menu, Item clickedItem) {
for (VarNumData data : dataList) {
- String varName = AbstractMenusApi.get().providers().placeholders().replace(p, data.getName());
+ String varName = AbstractMenusApi.get().providers().placeholders().resolve().replace(p, data.getName());
double value = data.getValue().getDouble(p, menu);
Function func = num -> num * value;
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/varp/ActionVarpRem.java b/plugin/src/main/java/ru/abstractmenus/data/actions/varp/ActionVarpRem.java
index 5256634..73cf5bd 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/varp/ActionVarpRem.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/varp/ActionVarpRem.java
@@ -23,7 +23,7 @@ private ActionVarpRem(List dataList) {
public void activate(Player p, Menu menu, Item clickedItem) {
for (VarData data : dataList) {
- String varName = AbstractMenusApi.get().providers().placeholders().replace(p, data.getName());
+ String varName = AbstractMenusApi.get().providers().placeholders().resolve().replace(p, data.getName());
VariableManagerImpl.instance().deletePersonal(p.getName(), varName);
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/varp/ActionVarpSet.java b/plugin/src/main/java/ru/abstractmenus/data/actions/varp/ActionVarpSet.java
index 1bb5fc2..d2e0fe1 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/varp/ActionVarpSet.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/varp/ActionVarpSet.java
@@ -25,10 +25,10 @@ private ActionVarpSet(List dataList) {
public void activate(Player p, Menu menu, Item clickedItem) {
for (VarData data : dataList) {
- String varName = AbstractMenusApi.get().providers().placeholders().replace(p, data.getName());
- String varVal = AbstractMenusApi.get().providers().placeholders().replace(p, data.getValue());
+ String varName = AbstractMenusApi.get().providers().placeholders().resolve().replace(p, data.getName());
+ String varVal = AbstractMenusApi.get().providers().placeholders().resolve().replace(p, data.getValue());
- long time = TimeUtil.parseTime(AbstractMenusApi.get().providers().placeholders().replace(p, data.getTime()));
+ long time = TimeUtil.parseTime(AbstractMenusApi.get().providers().placeholders().resolve().replace(p, data.getTime()));
boolean replace = data.isReplace().getBool(p, menu);
Var var = VariableManagerImpl.instance().createBuilder()
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/wrappers/ActionDelay.java b/plugin/src/main/java/ru/abstractmenus/data/actions/wrappers/ActionDelay.java
index 9750da8..5b63b85 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/wrappers/ActionDelay.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/wrappers/ActionDelay.java
@@ -25,7 +25,9 @@ private ActionDelay(TypeInt delay, Actions actions) {
@Override
public void activate(Player player, Menu menu, Item clickedItem) {
if (actions != null) {
- BukkitTasks.runTaskLater(() -> actions.activate(player, menu, clickedItem), delay.getInt(player, menu));
+ BukkitTasks.runForEntityLater(player,
+ () -> actions.activate(player, menu, clickedItem),
+ delay.getInt(player, menu));
}
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/actions/wrappers/ActionPlayerScope.java b/plugin/src/main/java/ru/abstractmenus/data/actions/wrappers/ActionPlayerScope.java
index d63fd85..1de374e 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/actions/wrappers/ActionPlayerScope.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/actions/wrappers/ActionPlayerScope.java
@@ -23,7 +23,7 @@ public ActionPlayerScope(String playerName, Actions actions) {
@Override
public void activate(Player player, Menu menu, Item clickedItem) {
- String replacedName = AbstractMenusApi.get().providers().placeholders().replace(player, playerName);
+ String replacedName = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, playerName);
Player target = Bukkit.getPlayerExact(replacedName);
if (target != null && target.isOnline()) {
diff --git a/plugin/src/main/java/ru/abstractmenus/data/activators/OpenChat.java b/plugin/src/main/java/ru/abstractmenus/data/activators/OpenChat.java
index 31f0102..15ee88a 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/activators/OpenChat.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/activators/OpenChat.java
@@ -23,7 +23,7 @@ private OpenChat(List messages) {
@EventHandler
public void onChat(AsyncChatEvent event) {
for (String str : messages) {
- String replaced = AbstractMenusApi.get().providers().placeholders().replace(event.getPlayer(), str);
+ String replaced = AbstractMenusApi.get().providers().placeholders().resolve().replace(event.getPlayer(), str);
if (event.signedMessage().message().equalsIgnoreCase(replaced)) {
BukkitTasks.runTask(() -> openMenu(null, event.getPlayer()));
diff --git a/plugin/src/main/java/ru/abstractmenus/data/activators/OpenChatContains.java b/plugin/src/main/java/ru/abstractmenus/data/activators/OpenChatContains.java
index 24b5ed2..d5944a6 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/activators/OpenChatContains.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/activators/OpenChatContains.java
@@ -24,7 +24,7 @@ private OpenChatContains(List messages) {
@EventHandler
public void onChat(AsyncChatEvent event) {
for (String str : messages) {
- String msg = AbstractMenusApi.get().providers().placeholders().replace(event.getPlayer(), str);
+ String msg = AbstractMenusApi.get().providers().placeholders().resolve().replace(event.getPlayer(), str);
if (event.signedMessage().message().contains(msg)) {
BukkitTasks.runTask(() -> openMenu(null, event.getPlayer()));
diff --git a/plugin/src/main/java/ru/abstractmenus/data/activators/OpenClickEntity.java b/plugin/src/main/java/ru/abstractmenus/data/activators/OpenClickEntity.java
index 0aad579..016514b 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/activators/OpenClickEntity.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/activators/OpenClickEntity.java
@@ -38,7 +38,7 @@ public void onEntityClick(PlayerInteractEntityEvent event) {
}
if (data.getName() != null) {
- String expectedName = AbstractMenusApi.get().providers().placeholders().replace(player, data.getName());
+ String expectedName = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, data.getName());
if (!clickedEntity.getName().equalsIgnoreCase(expectedName)) {
continue;
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/activators/OpenRegionEnter.java b/plugin/src/main/java/ru/abstractmenus/data/activators/OpenRegionEnter.java
index 34c10d2..5407a93 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/activators/OpenRegionEnter.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/activators/OpenRegionEnter.java
@@ -22,7 +22,7 @@ private OpenRegionEnter(List regions) {
@EventHandler
public void onRegionJoin(RegionEnterEvent event) {
- List regions = AbstractMenusApi.get().providers().placeholders().replace(event.getPlayer(), this.regions);
+ List regions = AbstractMenusApi.get().providers().placeholders().resolve().replace(event.getPlayer(), this.regions);
if (regions.contains(event.getRegion().getId())) {
openMenu(event.getRegion(), event.getPlayer());
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/activators/OpenRegionLeave.java b/plugin/src/main/java/ru/abstractmenus/data/activators/OpenRegionLeave.java
index e895719..74cc7d0 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/activators/OpenRegionLeave.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/activators/OpenRegionLeave.java
@@ -22,7 +22,7 @@ private OpenRegionLeave(List regions) {
@EventHandler
public void onRegionEnter(RegionLeaveEvent event) {
- List regions = AbstractMenusApi.get().providers().placeholders().replace(event.getPlayer(), this.regions);
+ List regions = AbstractMenusApi.get().providers().placeholders().resolve().replace(event.getPlayer(), this.regions);
if (regions.contains(event.getRegion().getId())) {
openMenu(event.getRegion(), event.getPlayer());
diff --git a/plugin/src/main/java/ru/abstractmenus/data/activators/OpenShiftClickEntity.java b/plugin/src/main/java/ru/abstractmenus/data/activators/OpenShiftClickEntity.java
index 092b4e0..ce8d688 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/activators/OpenShiftClickEntity.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/activators/OpenShiftClickEntity.java
@@ -39,7 +39,7 @@ public void onEntityClick(PlayerInteractEntityEvent event) {
}
if (data.getName() == null) {
- String name = AbstractMenusApi.get().providers().placeholders().replace(player, data.getName());
+ String name = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, data.getName());
if (clickedEntity.getName().equalsIgnoreCase(name)) {
openMenu(clickedEntity, player);
return;
diff --git a/plugin/src/main/java/ru/abstractmenus/data/activators/OpenSign.java b/plugin/src/main/java/ru/abstractmenus/data/activators/OpenSign.java
index 6977a20..cda4ff6 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/activators/OpenSign.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/activators/OpenSign.java
@@ -43,7 +43,7 @@ public void onTableClick(PlayerInteractEvent event) {
String[] lines = sign.getLines();
int compareLines = Math.min(text.size(), lines.length);
for (int i = 0; i < compareLines; i++) {
- String line = AbstractMenusApi.get().providers().placeholders().replace(player, text.get(i));
+ String line = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, text.get(i));
if (!line.equalsIgnoreCase(lines[i])) return;
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/catalogs/SliceCatalog.java b/plugin/src/main/java/ru/abstractmenus/data/catalogs/SliceCatalog.java
index 0720d63..c12151d 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/catalogs/SliceCatalog.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/catalogs/SliceCatalog.java
@@ -36,7 +36,7 @@ public SliceCatalog(String value, String separator, boolean trim) {
@Override
public Collection snapshot(Player player, Menu menu) {
- String replaced = AbstractMenusApi.get().providers().placeholders().replace(player, value);
+ String replaced = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, value);
String[] values = replaced.split(separator);
return Arrays.stream(values)
.filter(val -> !val.isEmpty())
diff --git a/plugin/src/main/java/ru/abstractmenus/data/comparator/LegacyValueComparator.java b/plugin/src/main/java/ru/abstractmenus/data/comparator/LegacyValueComparator.java
index 74eec4d..c569b52 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/comparator/LegacyValueComparator.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/comparator/LegacyValueComparator.java
@@ -49,11 +49,11 @@ public Comparator(String param) {
}
public boolean compare(Player player, Menu menu){
- String param = AbstractMenusApi.get().providers().placeholders().replace(player, getParam());
+ String param = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, getParam());
if(equals != null){
for(String str : equals){
- String val = AbstractMenusApi.get().providers().placeholders().replace(player, str);
+ String val = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, str);
try{
if(Double.parseDouble(param) == Double.parseDouble(val)) return true;
@@ -65,7 +65,7 @@ public boolean compare(Player player, Menu menu){
if(equalsIgnoreCase != null){
for(String str : equalsIgnoreCase){
- String val = AbstractMenusApi.get().providers().placeholders().replace(player, str);
+ String val = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, str);
if(param.equalsIgnoreCase(val)){
return true;
}
@@ -74,7 +74,7 @@ public boolean compare(Player player, Menu menu){
if(contains != null){
for(String str : contains){
- String val = AbstractMenusApi.get().providers().placeholders().replace(player, str);
+ String val = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, str);
if(param.toLowerCase().contains(val.toLowerCase())){
return true;
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/comparator/ModernValueComparator.java b/plugin/src/main/java/ru/abstractmenus/data/comparator/ModernValueComparator.java
index 65e472d..5f335c1 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/comparator/ModernValueComparator.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/comparator/ModernValueComparator.java
@@ -18,7 +18,7 @@ private ModernValueComparator(String expression) {
@Override
public boolean compare(Player player, Menu menu) {
- String replaced = AbstractMenusApi.get().providers().placeholders().replace(player, expression);
+ String replaced = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, expression);
String result = EVALUATOR.evaluate(replaced);
return Boolean.parseBoolean(result);
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/properties/PropBookData.java b/plugin/src/main/java/ru/abstractmenus/data/properties/PropBookData.java
index f2461d4..de62e10 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/properties/PropBookData.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/properties/PropBookData.java
@@ -35,9 +35,9 @@ public boolean isApplyMeta() {
@Override
public void apply(ItemStack itemStack, ItemMeta meta, Player player, Menu menu) {
if (meta instanceof BookMeta) {
- String author = AbstractMenusApi.get().providers().placeholders().replace(player, data.getAuthor());
- String title = AbstractMenusApi.get().providers().placeholders().replace(player, data.getTitle());
- List pages = AbstractMenusApi.get().providers().placeholders().replace(player, data.getPages());
+ String author = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, data.getAuthor());
+ String title = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, data.getTitle());
+ List pages = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, data.getPages());
((BookMeta) meta).setAuthor(author);
((BookMeta) meta).setTitle(title);
diff --git a/plugin/src/main/java/ru/abstractmenus/data/properties/PropEquipItem.java b/plugin/src/main/java/ru/abstractmenus/data/properties/PropEquipItem.java
index 44c4d31..4bf0237 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/properties/PropEquipItem.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/properties/PropEquipItem.java
@@ -60,7 +60,7 @@ public void apply(ItemStack item, ItemMeta meta, Player player, Menu menu) {
Player target = player;
if (playerName != null) {
- String replaced = AbstractMenusApi.get().providers().placeholders().replace(player, playerName);
+ String replaced = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, playerName);
Player found = Bukkit.getPlayerExact(replaced);
if (found == null || !found.isOnline()) {
diff --git a/plugin/src/main/java/ru/abstractmenus/data/properties/PropHDB.java b/plugin/src/main/java/ru/abstractmenus/data/properties/PropHDB.java
index 9e7f37c..8066990 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/properties/PropHDB.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/properties/PropHDB.java
@@ -48,7 +48,7 @@ public boolean isApplyMeta() {
@Override
public void apply(ItemStack item, ItemMeta meta, Player player, Menu menu) {
- String replaced = AbstractMenusApi.get().providers().placeholders().replace(player, id);
+ String replaced = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, id);
ItemStack headItem = api().getItemHead(replaced);
if (headItem == null)
diff --git a/plugin/src/main/java/ru/abstractmenus/data/properties/PropItemsAdder.java b/plugin/src/main/java/ru/abstractmenus/data/properties/PropItemsAdder.java
index 7b2ae92..89466c9 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/properties/PropItemsAdder.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/properties/PropItemsAdder.java
@@ -33,7 +33,7 @@ public boolean isApplyMeta() {
@Override
public void apply(ItemStack item, ItemMeta meta, Player player, Menu menu) {
- String id = AbstractMenusApi.get().providers().placeholders().replace(player, this.id);
+ String id = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, this.id);
CustomStack stack = CustomStack.getInstance(id);
if (stack == null)
diff --git a/plugin/src/main/java/ru/abstractmenus/data/properties/PropLore.java b/plugin/src/main/java/ru/abstractmenus/data/properties/PropLore.java
index c21760f..fb8912b 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/properties/PropLore.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/properties/PropLore.java
@@ -68,7 +68,7 @@ public void apply(ItemStack itemStack, ItemMeta meta, Player player, Menu menu)
meta.setLore(preFormatted);
return;
}
- List replaced = AbstractMenusApi.get().providers().placeholders().replace(player, lore);
+ List replaced = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, lore);
meta.setLore(MiniMessageUtil.parseToLegacy(replaced));
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/properties/PropLoreLight.java b/plugin/src/main/java/ru/abstractmenus/data/properties/PropLoreLight.java
index 663f650..b1d5933 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/properties/PropLoreLight.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/properties/PropLoreLight.java
@@ -33,7 +33,7 @@ public boolean isApplyMeta() {
@Override
public void apply(ItemStack itemStack, ItemMeta meta, Player player, Menu menu) {
- List replaced = AbstractMenusApi.get().providers().placeholders().replace(player, lore);
+ List replaced = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, lore);
meta.setLore(replaced);
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/properties/PropMmoItem.java b/plugin/src/main/java/ru/abstractmenus/data/properties/PropMmoItem.java
index cc54614..21010fa 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/properties/PropMmoItem.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/properties/PropMmoItem.java
@@ -36,7 +36,7 @@ public boolean isApplyMeta() {
@Override
public void apply(ItemStack item, ItemMeta meta, Player player, Menu menu) {
- String[] arr = AbstractMenusApi.get().providers().placeholders().replace(player, id).split(":");
+ String[] arr = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, id).split(":");
if(arr.length >= 2) {
ItemStack mmoItem = MMOItems.plugin.getItem(MMOItems.plugin.getTypes().get(arr[0]), arr[1]);
diff --git a/plugin/src/main/java/ru/abstractmenus/data/properties/PropName.java b/plugin/src/main/java/ru/abstractmenus/data/properties/PropName.java
index bda8de0..dd97b9f 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/properties/PropName.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/properties/PropName.java
@@ -58,7 +58,7 @@ public void apply(ItemStack itemStack, ItemMeta meta, Player player, Menu menu)
meta.setDisplayName(preFormatted);
return;
}
- String replaced = AbstractMenusApi.get().providers().placeholders().replace(player, name);
+ String replaced = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, name);
meta.setDisplayName(MiniMessageUtil.parseToLegacy(replaced));
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/properties/PropNameLight.java b/plugin/src/main/java/ru/abstractmenus/data/properties/PropNameLight.java
index 85100f6..b56dbdd 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/properties/PropNameLight.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/properties/PropNameLight.java
@@ -31,7 +31,7 @@ public boolean isApplyMeta() {
@Override
public void apply(ItemStack itemStack, ItemMeta meta, Player player, Menu menu) {
- String replaced = AbstractMenusApi.get().providers().placeholders().replace(player, name);
+ String replaced = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, name);
meta.setDisplayName(replaced);
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/properties/PropOraxen.java b/plugin/src/main/java/ru/abstractmenus/data/properties/PropOraxen.java
index 8ef134b..aae5a55 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/properties/PropOraxen.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/properties/PropOraxen.java
@@ -62,7 +62,7 @@ public void apply(ItemStack item, ItemMeta meta, Player player, Menu menu) {
}
private ItemStack getItem(Player player) {
- String id = AbstractMenusApi.get().providers().placeholders().replace(player, this.id);
+ String id = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, this.id);
try {
Object builder = getItemByIdMethod.invoke(id);
if (builder == null)
diff --git a/plugin/src/main/java/ru/abstractmenus/data/properties/PropSerialized.java b/plugin/src/main/java/ru/abstractmenus/data/properties/PropSerialized.java
index 77e4ca3..40323f1 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/properties/PropSerialized.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/properties/PropSerialized.java
@@ -33,7 +33,7 @@ public boolean isApplyMeta() {
@Override
public void apply(ItemStack item, ItemMeta meta, Player player, Menu menu) {
- String base64 = AbstractMenusApi.get().providers().placeholders().replace(player, value);
+ String base64 = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, value);
ItemStack deserialized = ItemUtil.decodeStack(base64);
if (deserialized == null) {
diff --git a/plugin/src/main/java/ru/abstractmenus/data/properties/PropSkullOwner.java b/plugin/src/main/java/ru/abstractmenus/data/properties/PropSkullOwner.java
index 5ba3521..8d1f8e7 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/properties/PropSkullOwner.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/properties/PropSkullOwner.java
@@ -34,7 +34,7 @@ public boolean isApplyMeta() {
@Override
public void apply(ItemStack item, ItemMeta meta, Player player, Menu menu) {
- String replaced = AbstractMenusApi.get().providers().placeholders().replace(player, owner);
+ String replaced = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, owner);
ItemStack skullItem = Skulls.getPlayerSkull(replaced);
if (skullItem == null) {
diff --git a/plugin/src/main/java/ru/abstractmenus/data/properties/PropTexture.java b/plugin/src/main/java/ru/abstractmenus/data/properties/PropTexture.java
index 7044b8a..2733e1d 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/properties/PropTexture.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/properties/PropTexture.java
@@ -43,7 +43,7 @@ public boolean isApplyMeta() {
@Override
public void apply(ItemStack item, ItemMeta meta, Player player, Menu menu) {
- String replaced = AbstractMenusApi.get().providers().placeholders().replace(player, texture);
+ String replaced = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, texture);
ItemStack skullItem = Skulls.getCustomSkull(fetchTextureUrl(replaced));
if (skullItem != null) {
diff --git a/plugin/src/main/java/ru/abstractmenus/data/rules/RuleBungeeIsOnline.java b/plugin/src/main/java/ru/abstractmenus/data/rules/RuleBungeeIsOnline.java
index 469fdae..ac2e2ff 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/rules/RuleBungeeIsOnline.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/rules/RuleBungeeIsOnline.java
@@ -20,7 +20,7 @@ private RuleBungeeIsOnline(String server){
@Override
public boolean check(Player player, Menu menu, Item clickedItem) {
- return BungeeManager.instance().isOnline(AbstractMenusApi.get().providers().placeholders().replace(player, server));
+ return BungeeManager.instance().isOnline(AbstractMenusApi.get().providers().placeholders().resolve().replace(player, server));
}
public static class Serializer implements NodeSerializer {
diff --git a/plugin/src/main/java/ru/abstractmenus/data/rules/RuleBungeeOnline.java b/plugin/src/main/java/ru/abstractmenus/data/rules/RuleBungeeOnline.java
index 60be6fd..2c2378e 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/rules/RuleBungeeOnline.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/rules/RuleBungeeOnline.java
@@ -24,7 +24,7 @@ private RuleBungeeOnline(String server, TypeInt online){
@Override
public boolean check(Player player, Menu menu, Item clickedItem) {
- return BungeeManager.instance().getOnline(AbstractMenusApi.get().providers().placeholders().replace(player, server)) >= online.getInt(player, menu);
+ return BungeeManager.instance().getOnline(AbstractMenusApi.get().providers().placeholders().resolve().replace(player, server)) >= online.getInt(player, menu);
}
public static class Serializer implements NodeSerializer {
diff --git a/plugin/src/main/java/ru/abstractmenus/data/rules/RuleExistVar.java b/plugin/src/main/java/ru/abstractmenus/data/rules/RuleExistVar.java
index d368571..1cf5448 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/rules/RuleExistVar.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/rules/RuleExistVar.java
@@ -22,12 +22,12 @@ private RuleExistVar(String player, String name){
@Override
public boolean check(Player p, Menu menu, Item clickedItem) {
- String varName = AbstractMenusApi.get().providers().placeholders().replace(p, this.name);
+ String varName = AbstractMenusApi.get().providers().placeholders().resolve().replace(p, this.name);
if(this.player == null) {
return VariableManagerImpl.instance().getGlobal(varName) != null;
} else {
- String varPlayer = AbstractMenusApi.get().providers().placeholders().replace(p, this.player);
+ String varPlayer = AbstractMenusApi.get().providers().placeholders().resolve().replace(p, this.player);
return VariableManagerImpl.instance().getPersonal(varPlayer, varName) != null;
}
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/rules/RuleExistVarp.java b/plugin/src/main/java/ru/abstractmenus/data/rules/RuleExistVarp.java
index dc44f77..fed1f42 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/rules/RuleExistVarp.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/rules/RuleExistVarp.java
@@ -20,7 +20,7 @@ private RuleExistVarp(String name){
@Override
public boolean check(Player p, Menu menu, Item clickedItem) {
- String varName = AbstractMenusApi.get().providers().placeholders().replace(p, this.name);
+ String varName = AbstractMenusApi.get().providers().placeholders().resolve().replace(p, this.name);
return VariableManagerImpl.instance().getPersonal(p.getName(), varName) != null;
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/rules/RuleGroup.java b/plugin/src/main/java/ru/abstractmenus/data/rules/RuleGroup.java
index 95de37f..5f3c14a 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/rules/RuleGroup.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/rules/RuleGroup.java
@@ -22,8 +22,8 @@ private RuleGroup(List groups) {
@Override
public boolean check(Player player, Menu menu, Item clickedItem) {
for (String group : groups) {
- String replaced = AbstractMenusApi.get().providers().placeholders().replace(player, group);
- if (!AbstractMenusApi.get().providers().permissions().hasGroup(player, replaced)) return false;
+ String replaced = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, group);
+ if (!AbstractMenusApi.get().providers().permissions().resolve().hasGroup(player, replaced)) return false;
}
return true;
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/rules/RuleJS.java b/plugin/src/main/java/ru/abstractmenus/data/rules/RuleJS.java
index f9d41de..823c088 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/rules/RuleJS.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/rules/RuleJS.java
@@ -41,7 +41,7 @@ private RuleJS(String js){
@Override
public boolean check(Player player, Menu menu, Item clickedItem) {
try{
- Object result = ENGINE.eval(AbstractMenusApi.get().providers().placeholders().replace(player, js), bindings);
+ Object result = ENGINE.eval(AbstractMenusApi.get().providers().placeholders().resolve().replace(player, js), bindings);
return result.toString().equals("true");
} catch (ScriptException e){
Logger.severe("Cannot execute JavaScript code: " + e.getMessage());
diff --git a/plugin/src/main/java/ru/abstractmenus/data/rules/RuleLevel.java b/plugin/src/main/java/ru/abstractmenus/data/rules/RuleLevel.java
index 86dcd06..32424aa 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/rules/RuleLevel.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/rules/RuleLevel.java
@@ -20,7 +20,7 @@ private RuleLevel(TypeInt level){
@Override
public boolean check(Player player, Menu menu, Item clickedItem) {
- return AbstractMenusApi.get().providers().levels().getLevel(player) >= level.getInt(player, menu);
+ return AbstractMenusApi.get().providers().levels().resolve().getLevel(player) >= level.getInt(player, menu);
}
public static class Serializer implements NodeSerializer {
diff --git a/plugin/src/main/java/ru/abstractmenus/data/rules/RuleMoney.java b/plugin/src/main/java/ru/abstractmenus/data/rules/RuleMoney.java
index 01ec2b5..2278247 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/rules/RuleMoney.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/rules/RuleMoney.java
@@ -9,6 +9,7 @@
import ru.abstractmenus.api.inventory.Menu;
import ru.abstractmenus.api.inventory.Item;
import ru.abstractmenus.api.AbstractMenusApi;
+import ru.abstractmenus.data.actions.MoneyAmountSpec;
import ru.abstractmenus.datatype.TypeDouble;
public class RuleMoney implements Rule {
@@ -24,8 +25,8 @@ private RuleMoney(TypeDouble money, String provider){
@Override
public boolean check(Player player, Menu menu, Item clickedItem) {
EconomyHandler eco = provider != null
- ? AbstractMenusApi.get().providers().economy(provider)
- : AbstractMenusApi.get().providers().economy();
+ ? AbstractMenusApi.get().providers().economy().resolve(provider)
+ : AbstractMenusApi.get().providers().economy().resolve();
if (eco == null) {
return false;
}
@@ -36,30 +37,8 @@ public static class Serializer implements NodeSerializer {
@Override
public RuleMoney deserialize(Class type, ConfigNode node) throws NodeSerializeException {
- TypeDouble money;
- String provider = null;
-
- if (node.isMap()) {
- money = node.node("amount").getValue(TypeDouble.class);
- provider = node.node("provider").getString(null);
- } else {
- money = node.getValue(TypeDouble.class);
- }
-
- if (provider != null) {
- if (!AbstractMenusApi.get().providers().hasEconomy(provider)) {
- StringBuilder known = new StringBuilder();
- for (EconomyHandler h : AbstractMenusApi.get().providers().allEconomy()) {
- if (known.length() > 0) known.append(", ");
- known.append(h.getClass().getSimpleName());
- }
- throw new NodeSerializeException(node,
- "Unknown economy provider '" + provider + "'. Registered: ["
- + known + "]. Omit the 'provider' field for default selection.");
- }
- }
-
- return new RuleMoney(money, provider);
+ MoneyAmountSpec spec = MoneyAmountSpec.parse(node);
+ return new RuleMoney(spec.amount, spec.provider);
}
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/rules/RulePermission.java b/plugin/src/main/java/ru/abstractmenus/data/rules/RulePermission.java
index b843268..f462c2e 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/rules/RulePermission.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/rules/RulePermission.java
@@ -22,8 +22,8 @@ private RulePermission(List permission){
@Override
public boolean check(Player player, Menu menu, Item clickedItem) {
for (String perm : permission){
- String replaced = AbstractMenusApi.get().providers().placeholders().replace(player, perm);
- if(!AbstractMenusApi.get().providers().permissions().hasPermission(player, replaced)) return false;
+ String replaced = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, perm);
+ if(!AbstractMenusApi.get().providers().permissions().resolve().hasPermission(player, replaced)) return false;
}
return true;
}
diff --git a/plugin/src/main/java/ru/abstractmenus/data/rules/RulePlayerIsOnline.java b/plugin/src/main/java/ru/abstractmenus/data/rules/RulePlayerIsOnline.java
index 939ce1c..3e79b6f 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/rules/RulePlayerIsOnline.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/rules/RulePlayerIsOnline.java
@@ -20,7 +20,7 @@ private RulePlayerIsOnline(String playerName){
@Override
public boolean check(Player player, Menu menu, Item clickedItem) {
- String replaced = AbstractMenusApi.get().providers().placeholders().replace(player, playerName);
+ String replaced = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, playerName);
Player foundPlayer = Bukkit.getPlayerExact(replaced);
return foundPlayer != null && foundPlayer.isOnline();
diff --git a/plugin/src/main/java/ru/abstractmenus/data/rules/RuleRegion.java b/plugin/src/main/java/ru/abstractmenus/data/rules/RuleRegion.java
index 5c47cd7..8394da7 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/rules/RuleRegion.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/rules/RuleRegion.java
@@ -24,7 +24,7 @@ private RuleRegion(List regions){
@Override
public boolean check(Player player, Menu menu, Item clickedItem) {
for (String reg : regions){
- String replaced = AbstractMenusApi.get().providers().placeholders().replace(player, reg);
+ String replaced = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, reg);
ProtectedRegion region = RegionUtils.getRegion(player.getWorld(), replaced);
if(region != null && region.contains(
diff --git a/plugin/src/main/java/ru/abstractmenus/data/rules/RuleXp.java b/plugin/src/main/java/ru/abstractmenus/data/rules/RuleXp.java
index f9784d0..43e40d3 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/rules/RuleXp.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/rules/RuleXp.java
@@ -20,7 +20,7 @@ private RuleXp(TypeFloat xp) {
@Override
public boolean check(Player player, Menu menu, Item clickedItem) {
- return AbstractMenusApi.get().providers().levels().getXp(player) >= xp.getFloat(player, menu);
+ return AbstractMenusApi.get().providers().levels().resolve().getXp(player) >= xp.getFloat(player, menu);
}
public static class Serializer implements NodeSerializer {
diff --git a/plugin/src/main/java/ru/abstractmenus/data/rules/logical/RulePlayerScope.java b/plugin/src/main/java/ru/abstractmenus/data/rules/logical/RulePlayerScope.java
index 7578af7..a25ac34 100644
--- a/plugin/src/main/java/ru/abstractmenus/data/rules/logical/RulePlayerScope.java
+++ b/plugin/src/main/java/ru/abstractmenus/data/rules/logical/RulePlayerScope.java
@@ -22,7 +22,7 @@ public RulePlayerScope(String playerName, Rule rules) {
@Override
public boolean check(Player player, Menu menu, Item clickedItem) {
- String replacedName = AbstractMenusApi.get().providers().placeholders().replace(player, playerName);
+ String replacedName = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, playerName);
Player target = Bukkit.getPlayerExact(replacedName);
if (target != null && target.isOnline()) {
diff --git a/plugin/src/main/java/ru/abstractmenus/datatype/DataType.java b/plugin/src/main/java/ru/abstractmenus/datatype/DataType.java
index 3fd668d..afe9b41 100644
--- a/plugin/src/main/java/ru/abstractmenus/datatype/DataType.java
+++ b/plugin/src/main/java/ru/abstractmenus/datatype/DataType.java
@@ -15,7 +15,7 @@ public abstract class DataType implements Cloneable {
}
public String replaceFor(Player player, Menu menu) {
- return AbstractMenusApi.get().providers().placeholders().replace(player, value);
+ return AbstractMenusApi.get().providers().placeholders().resolve().replace(player, value);
}
public static boolean hasPlaceholder(String string) {
diff --git a/plugin/src/main/java/ru/abstractmenus/extractors/PlayerExtractor.java b/plugin/src/main/java/ru/abstractmenus/extractors/PlayerExtractor.java
index 4f21728..ed08d50 100644
--- a/plugin/src/main/java/ru/abstractmenus/extractors/PlayerExtractor.java
+++ b/plugin/src/main/java/ru/abstractmenus/extractors/PlayerExtractor.java
@@ -12,7 +12,7 @@ public class PlayerExtractor implements ValueExtractor {
@Override
public String extract(Object obj, String placeholder) {
return (obj instanceof Player player && player.isOnline())
- ? AbstractMenusApi.get().providers().placeholders().replacePlaceholder(player, placeholder)
+ ? AbstractMenusApi.get().providers().placeholders().resolve().replacePlaceholder(player, placeholder)
: StringUtils.EMPTY;
}
}
diff --git a/plugin/src/main/java/ru/abstractmenus/api/AbstractMenusApiImpl.java b/plugin/src/main/java/ru/abstractmenus/impl/AbstractMenusApiImpl.java
similarity index 91%
rename from plugin/src/main/java/ru/abstractmenus/api/AbstractMenusApiImpl.java
rename to plugin/src/main/java/ru/abstractmenus/impl/AbstractMenusApiImpl.java
index 922ab3a..a982dc9 100644
--- a/plugin/src/main/java/ru/abstractmenus/api/AbstractMenusApiImpl.java
+++ b/plugin/src/main/java/ru/abstractmenus/impl/AbstractMenusApiImpl.java
@@ -1,8 +1,15 @@
-package ru.abstractmenus.api;
+package ru.abstractmenus.impl;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import ru.abstractmenus.AbstractMenus;
+import ru.abstractmenus.api.AbstractMenusApi;
+import ru.abstractmenus.api.Action;
+import ru.abstractmenus.api.Activator;
+import ru.abstractmenus.api.Catalog;
+import ru.abstractmenus.api.ProviderRegistry;
+import ru.abstractmenus.api.Rule;
+import ru.abstractmenus.api.TypeRegistry;
import ru.abstractmenus.api.inventory.ItemProperty;
import ru.abstractmenus.api.inventory.Menu;
import ru.abstractmenus.api.variables.VariableManager;
diff --git a/plugin/src/main/java/ru/abstractmenus/impl/ProviderRegistryImpl.java b/plugin/src/main/java/ru/abstractmenus/impl/ProviderRegistryImpl.java
new file mode 100644
index 0000000..3eac0c4
--- /dev/null
+++ b/plugin/src/main/java/ru/abstractmenus/impl/ProviderRegistryImpl.java
@@ -0,0 +1,186 @@
+package ru.abstractmenus.impl;
+
+import ru.abstractmenus.api.MenuExtension;
+import ru.abstractmenus.api.ProviderRegistry;
+import ru.abstractmenus.api.ProviderSection;
+import ru.abstractmenus.api.handler.EconomyHandler;
+import ru.abstractmenus.api.handler.LevelHandler;
+import ru.abstractmenus.api.handler.PermissionsHandler;
+import ru.abstractmenus.api.handler.PlaceholderHandler;
+import ru.abstractmenus.api.handler.SkinHandler;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+
+/**
+ * Default {@link ProviderRegistry} implementation. Five sections backed by
+ * the inner {@link SectionImpl} class. Insertion-ordered per section so
+ * that equal-priority ties resolve to the first-registered entry.
+ *
+ * Sections are constructed once in this class's constructor with their
+ * "kind" string ({@code "economy"}, {@code "permissions"}, ...) so each
+ * one knows which {@code config.conf providers.} entry applies.
+ *
+ * Thread-safe for registration/unregistration via per-section
+ * {@code synchronized} methods; production use expects all mutation on the
+ * main server thread during plugin / extension enable / disable.
+ */
+public final class ProviderRegistryImpl implements ProviderRegistry {
+
+ /** section kind → configured-default id (e.g. "economy" → "playerpoints"). */
+ private Function configDefaults = kind -> null;
+
+ private final SectionImpl economy = new SectionImpl<>("economy", this);
+ private final SectionImpl permissions = new SectionImpl<>("permissions", this);
+ private final SectionImpl levels = new SectionImpl<>("levels", this);
+ private final SectionImpl placeholders = new SectionImpl<>("placeholders", this);
+ private final SectionImpl skins = new SectionImpl<>("skins", this);
+
+ /** Wire up the config-backed default source. Called once from AbstractMenusApiImpl. */
+ public void setConfigDefaults(Function lookup) {
+ this.configDefaults = lookup;
+ }
+
+ @Override
+ public ProviderSection economy() {
+ return economy;
+ }
+
+ @Override
+ public ProviderSection permissions() {
+ return permissions;
+ }
+
+ @Override
+ public ProviderSection levels() {
+ return levels;
+ }
+
+ @Override
+ public ProviderSection placeholders() {
+ return placeholders;
+ }
+
+ @Override
+ public ProviderSection skins() {
+ return skins;
+ }
+
+ /**
+ * Wipe every provider registered by {@code owner} across all five
+ * sections. Intentionally NOT on the public {@link ProviderRegistry}
+ * interface so an addon cannot wipe another extension's providers.
+ * Called only by AbstractMenus' internal addon manager.
+ */
+ public void unregisterAll(MenuExtension owner) {
+ economy.unregisterAll(owner);
+ permissions.unregisterAll(owner);
+ levels.unregisterAll(owner);
+ placeholders.unregisterAll(owner);
+ skins.unregisterAll(owner);
+ }
+
+ /**
+ * Union of every extension that has registered a provider in any of
+ * the five sections. Used by {@code /am addons list} to discover
+ * Path 1 plugin-as-addons whose only fingerprint is in the registry
+ * owner-tracking map.
+ */
+ public Set seenOwners() {
+ Set all = new HashSet<>();
+ all.addAll(economy.seenOwners());
+ all.addAll(permissions.seenOwners());
+ all.addAll(levels.seenOwners());
+ all.addAll(placeholders.seenOwners());
+ all.addAll(skins.seenOwners());
+ return Collections.unmodifiableSet(all);
+ }
+
+ // -----------------------------------------------------------------
+ // SectionImpl - one instance per provider type
+ // -----------------------------------------------------------------
+
+ private static final class SectionImpl implements ProviderSection {
+
+ private final String kind;
+ private final ProviderRegistryImpl owner;
+ private final Map> byId = new LinkedHashMap<>();
+ private final Map> keysByExtension = new IdentityHashMap<>();
+
+ SectionImpl(String kind, ProviderRegistryImpl owner) {
+ this.kind = kind;
+ this.owner = owner;
+ }
+
+ @Override
+ public synchronized void register(String id, T handler, int priority, MenuExtension extOwner) {
+ String k = id.toLowerCase();
+ byId.put(k, new Entry<>(handler, priority));
+ keysByExtension.computeIfAbsent(extOwner, o -> new HashSet<>()).add(k);
+ }
+
+ @Override
+ public synchronized T resolve() {
+ // configDefaults is mutated only once (during plugin startup)
+ // so reading it without our lock is fine.
+ String configured = owner.configDefaults.apply(kind);
+ if (configured != null && !configured.equalsIgnoreCase("auto")) {
+ Entry e = byId.get(configured.toLowerCase());
+ if (e != null) return e.handler;
+ }
+ Entry best = null;
+ for (Entry e : byId.values()) {
+ if (best == null || e.priority > best.priority) best = e;
+ }
+ return best == null ? null : best.handler;
+ }
+
+ @Override
+ public synchronized T resolve(String id) {
+ Entry e = byId.get(id.toLowerCase());
+ return e == null ? null : e.handler;
+ }
+
+ @Override
+ public synchronized Collection all() {
+ List list = new ArrayList<>(byId.size());
+ for (Entry e : byId.values()) list.add(e.handler);
+ return Collections.unmodifiableList(list);
+ }
+
+ @Override
+ public synchronized Set ids() {
+ // Snapshot - byId keys may mutate after this returns.
+ return Collections.unmodifiableSet(new java.util.LinkedHashSet<>(byId.keySet()));
+ }
+
+ @Override
+ public synchronized boolean has(String id) {
+ return byId.containsKey(id.toLowerCase());
+ }
+
+ synchronized void unregisterAll(MenuExtension extOwner) {
+ Set keys = keysByExtension.remove(extOwner);
+ if (keys == null) return;
+ for (String k : keys) byId.remove(k);
+ }
+
+ synchronized Set seenOwners() {
+ return new HashSet<>(keysByExtension.keySet());
+ }
+
+ private static final class Entry {
+ final T handler;
+ final int priority;
+ Entry(T handler, int priority) { this.handler = handler; this.priority = priority; }
+ }
+ }
+}
diff --git a/plugin/src/main/java/ru/abstractmenus/impl/TypeRegistryImpl.java b/plugin/src/main/java/ru/abstractmenus/impl/TypeRegistryImpl.java
new file mode 100644
index 0000000..e75efad
--- /dev/null
+++ b/plugin/src/main/java/ru/abstractmenus/impl/TypeRegistryImpl.java
@@ -0,0 +1,170 @@
+package ru.abstractmenus.impl;
+
+import ru.abstractmenus.api.MenuExtension;
+import ru.abstractmenus.api.TypeRegistry;
+import ru.abstractmenus.hocon.api.serialize.NodeSerializer;
+import ru.abstractmenus.hocon.api.serialize.NodeSerializers;
+
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * In-memory {@link TypeRegistry} implementation. Not thread-safe for
+ * concurrent registration; consumers must register/unregister from the main
+ * server thread.
+ *
+ * Note on {@code NodeSerializers.unregister}: hocon 1.0.6 does NOT expose
+ * an {@code unregister(Class)} method. We therefore reach into its private
+ * backing map via reflection (one-time {@link Field} lookup, cached) so
+ * {@link #unregisterAll(MenuExtension)} can drop stale {@code Class} keys.
+ * Without this the {@code Class} object keeps the addon's now-closed
+ * classloader alive forever (native FDs, jar handle, all loaded classes).
+ */
+public final class TypeRegistryImpl implements TypeRegistry {
+
+ private static final Logger LOG = Logger.getLogger(TypeRegistryImpl.class.getName());
+
+ /**
+ * Reflective handle to {@link NodeSerializers}'s private
+ * {@code serializers} map field. Resolved once at class init; if hocon
+ * ever renames it, we log a warning and fall through to the harmless
+ * "leave the entry" behaviour.
+ */
+ private static final Field NODE_SERIALIZERS_MAP_FIELD;
+ static {
+ Field f = null;
+ try {
+ f = NodeSerializers.class.getDeclaredField("serializers");
+ f.setAccessible(true);
+ } catch (NoSuchFieldException e) {
+ LOG.log(Level.WARNING,
+ "NodeSerializers.serializers field not found - bundled hocon lib has "
+ + "changed shape since AbstractMenus was built. Not an AbstractMenus "
+ + "bug; needs an upstream hocon change to expose unregister(Class). "
+ + "Side effect: every addon disable/reload from now on leaks its "
+ + "classloader until a full server restart.",
+ e);
+ }
+ NODE_SERIALIZERS_MAP_FIELD = f;
+ }
+
+ private final NodeSerializers serializers;
+
+ /** key (lowercased) → registered class */
+ private final Map> byKey = new HashMap<>();
+
+ /** class → key (reverse index, kept in sync with {@link #byKey}) */
+ private final Map, String> byType = new IdentityHashMap<>();
+
+ /** owner → set of keys they registered (for unregisterAll) */
+ private final Map> keysByOwner = new IdentityHashMap<>();
+
+ public TypeRegistryImpl(NodeSerializers serializers) {
+ this.serializers = serializers;
+ }
+
+ @Override
+ public synchronized void register(String key,
+ Class type,
+ NodeSerializer serializer,
+ MenuExtension owner) {
+ String k = key.toLowerCase();
+
+ Class extends T> existing = byKey.get(k);
+ if (existing != null) {
+ LOG.warning("TypeRegistry: overwriting existing entry '" + k
+ + "' (" + existing.getName() + " -> " + type.getName() + ")");
+ byType.remove(existing);
+ // Strip k from the old owner's set so their later unregisterAll
+ // doesn't wipe the new owner's entry. Without this the *new*
+ // owner's class would silently vanish from the registry the
+ // first time the *old* owner gets disabled.
+ for (Set ownerKeys : keysByOwner.values()) {
+ ownerKeys.remove(k);
+ }
+ }
+
+ byKey.put(k, type);
+ byType.put(type, k);
+ serializers.register(type, serializer);
+
+ keysByOwner.computeIfAbsent(owner, o -> new HashSet<>()).add(k);
+ }
+
+ @Override
+ public synchronized Class extends T> get(String key) {
+ return byKey.get(key.toLowerCase());
+ }
+
+ @Override
+ public synchronized String name(Class extends T> type) {
+ return byType.get(type);
+ }
+
+ @Override
+ public synchronized Set keys() {
+ return Collections.unmodifiableSet(new HashSet<>(byKey.keySet()));
+ }
+
+ /**
+ * Snapshot of every extension that has ever registered (and not since
+ * fully unregistered) at least one entry in this registry. Used by
+ * {@code /am addons list} to surface Path 1 plugin-as-addons that
+ * don't sit in the AddonManager's loaded map.
+ */
+ public synchronized Set seenOwners() {
+ return Collections.unmodifiableSet(new HashSet<>(keysByOwner.keySet()));
+ }
+
+ /**
+ * Wipe every entry registered by {@code owner}. Intentionally NOT on the
+ * public {@link TypeRegistry} interface so that addons cannot use it to
+ * unregister another extension's entries. Called only by AbstractMenus'
+ * internal addon manager via a cast on the impl reference.
+ */
+ public synchronized void unregisterAll(MenuExtension owner) {
+ Set keys = keysByOwner.remove(owner);
+ if (keys == null) return;
+
+ for (String k : keys) {
+ Class extends T> type = byKey.remove(k);
+ if (type != null) {
+ byType.remove(type);
+ removeSerializerEntry(type);
+ }
+ }
+ }
+
+ /**
+ * Drop a {@code Class -> NodeSerializer} entry from the backing
+ * {@link NodeSerializers}. Done via reflection because hocon 1.0.6 does
+ * not expose an unregister method. Failure is non-fatal: we log and
+ * leave the entry, accepting the classloader-leak cost rather than
+ * crashing the disable path.
+ */
+ private void removeSerializerEntry(Class> type) {
+ if (NODE_SERIALIZERS_MAP_FIELD == null) return;
+ try {
+ @SuppressWarnings("unchecked")
+ Map, NodeSerializer>> backing =
+ (Map, NodeSerializer>>) NODE_SERIALIZERS_MAP_FIELD.get(serializers);
+ backing.remove(type);
+ } catch (Throwable t) {
+ LOG.log(Level.WARNING,
+ "Cannot remove NodeSerializer for " + type.getName()
+ + ": bundled hocon lib has no public unregister(Class), and "
+ + "our reflection workaround failed. Not an AbstractMenus bug - "
+ + "needs an upstream hocon change. Side effect: this addon's "
+ + "classloader stays in memory until a full server restart; the "
+ + "leak compounds across reloads.",
+ t);
+ }
+ }
+}
diff --git a/plugin/src/main/java/ru/abstractmenus/listeners/ChatListener.java b/plugin/src/main/java/ru/abstractmenus/listeners/ChatListener.java
index 2b8d8e6..518e5b5 100644
--- a/plugin/src/main/java/ru/abstractmenus/listeners/ChatListener.java
+++ b/plugin/src/main/java/ru/abstractmenus/listeners/ChatListener.java
@@ -3,7 +3,6 @@
import io.papermc.paper.event.player.AsyncChatEvent;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
-import org.bukkit.event.player.AsyncPlayerChatEvent;
import ru.abstractmenus.data.actions.ActionInputChat;
import ru.abstractmenus.services.MenuManager;
import ru.abstractmenus.util.bukkit.BukkitTasks;
@@ -16,7 +15,12 @@ public void onChat(AsyncChatEvent event) {
.getAndRemoveInputAction(event.getPlayer());
if (action != null) {
- BukkitTasks.runTask(() -> action.input(event.signedMessage().message()));
+ // input() invokes onInput/onCancel actions on the player —
+ // open menus, give items, etc. On Folia those operations
+ // require the player's region thread; runTask routes to the
+ // global scheduler which is forbidden from touching entity state.
+ BukkitTasks.runForEntity(event.getPlayer(),
+ () -> action.input(event.signedMessage().message()));
event.setCancelled(true);
}
}
diff --git a/plugin/src/main/java/ru/abstractmenus/listeners/InventoryListener.java b/plugin/src/main/java/ru/abstractmenus/listeners/InventoryListener.java
index 6c9e2f4..61490b0 100644
--- a/plugin/src/main/java/ru/abstractmenus/listeners/InventoryListener.java
+++ b/plugin/src/main/java/ru/abstractmenus/listeners/InventoryListener.java
@@ -93,7 +93,9 @@ public void onInventoryClick(InventoryClickEvent event) {
}
menu.click(event.getSlot(), player, event.getClick());
- BukkitTasks.runTaskLater(player::updateInventory, 2L);
+ // Pin to player's region: updateInventory writes to the player's
+ // inventory which on Folia is owned by the player's region thread.
+ BukkitTasks.runForEntityLater(player, player::updateInventory, 2L);
}
@EventHandler
@@ -146,7 +148,9 @@ public void onInventoryClose(InventoryCloseEvent event) {
}
if (current != null && current.equals(closed)) {
- BukkitTasks.runTaskLater(
+ // Pin to player's region: closeMenu calls player.closeInventory
+ // which on Folia must run on the player's region thread.
+ BukkitTasks.runForEntityLater(player,
() -> MenuManager.instance().closeMenu(player, false),
3L
);
diff --git a/plugin/src/main/java/ru/abstractmenus/listeners/PlayerListener.java b/plugin/src/main/java/ru/abstractmenus/listeners/PlayerListener.java
index 637fb53..9e63aef 100644
--- a/plugin/src/main/java/ru/abstractmenus/listeners/PlayerListener.java
+++ b/plugin/src/main/java/ru/abstractmenus/listeners/PlayerListener.java
@@ -19,6 +19,12 @@ public void onPlayerJoin(PlayerJoinEvent event) {
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
+ // Drop any tracked menu before the entity is gone. InventoryListener
+ // also closes via runForEntityLater(player, ..., 3L), but on Folia
+ // that task is silently dropped if the player quits inside the 3-tick
+ // window - leaving openedMenus with a stale entry. Removing here
+ // guarantees cleanup regardless of timing.
+ MenuManager.instance().removePlayerMenu(event.getPlayer());
MenuManager.instance().getAndRemoveInputAction(event.getPlayer());
ProfileStorage storage = ProfileStorage.instance();
if (storage != null) {
diff --git a/plugin/src/main/java/ru/abstractmenus/menu/AbstractMenu.java b/plugin/src/main/java/ru/abstractmenus/menu/AbstractMenu.java
index 6bddde4..7d05bc5 100644
--- a/plugin/src/main/java/ru/abstractmenus/menu/AbstractMenu.java
+++ b/plugin/src/main/java/ru/abstractmenus/menu/AbstractMenu.java
@@ -358,7 +358,7 @@ protected int getFreeSlot() {
}
protected void createInventory(Player player, InventoryHolder holder) {
- String title = AbstractMenusApi.get().providers().placeholders().replace(player, this.title);
+ String title = AbstractMenusApi.get().providers().placeholders().resolve().replace(player, this.title);
title = MiniMessageUtil.parseToLegacy(title);
if (this.type != null) {
diff --git a/plugin/src/main/java/ru/abstractmenus/services/MenuManager.java b/plugin/src/main/java/ru/abstractmenus/services/MenuManager.java
index b8d9d50..a53d4e8 100644
--- a/plugin/src/main/java/ru/abstractmenus/services/MenuManager.java
+++ b/plugin/src/main/java/ru/abstractmenus/services/MenuManager.java
@@ -291,7 +291,11 @@ public boolean serve() throws IOException {
if (Files.isRegularFile(file) && System.currentTimeMillis() > lastUpdated + 100) {
Logger.info("Detected changes in " + filename + ". Loading ...");
- // Bukkit API / menu map mutation must happen on main thread.
+ // We are on the WatchService thread - hop back into the
+ // server scheduler before parsing. On Folia BukkitTasks.runTask
+ // routes to the global region scheduler (loadFile mutates the
+ // menus map, which is a ConcurrentHashMap, so the scheduler
+ // affinity here is about ordering with /am reload, not safety).
BukkitTasks.runTask(() -> loadFile(file));
lastUpdated = System.currentTimeMillis();
}
@@ -430,11 +434,20 @@ private class UpdateTask implements Runnable {
public void run() {
// openedMenus stores a Player ref alongside each Menu; avoids a
// Bukkit.getPlayer(uuid) lookup per entry per tick.
+ //
+ // The outer iteration runs on the global region scheduler on
+ // Folia (or main thread on Spigot/Paper). menu.update() touches
+ // the player's inventory, which on Folia is owned by the
+ // player's region thread - so each per-player update is
+ // dispatched via the entity scheduler. On non-Folia,
+ // runForEntity falls through to direct invocation, no overhead.
for (OpenedMenu entry : openedMenus.values()) {
Player player = entry.player();
- if (player.isOnline()) {
- entry.menu().update(player);
- }
+ if (!player.isOnline()) continue;
+ Menu menu = entry.menu();
+ BukkitTasks.runForEntity(player, () -> {
+ if (player.isOnline()) menu.update(player);
+ });
}
}
diff --git a/plugin/src/main/java/ru/abstractmenus/util/bukkit/BukkitTasks.java b/plugin/src/main/java/ru/abstractmenus/util/bukkit/BukkitTasks.java
index c2b1cf0..8756000 100644
--- a/plugin/src/main/java/ru/abstractmenus/util/bukkit/BukkitTasks.java
+++ b/plugin/src/main/java/ru/abstractmenus/util/bukkit/BukkitTasks.java
@@ -3,9 +3,27 @@
import com.tcoded.folialib.FoliaLib;
import com.tcoded.folialib.wrapper.task.WrappedTask;
import lombok.Setter;
+import org.bukkit.entity.Entity;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitRunnable;
+/**
+ * Scheduling facade.
+ *
+ *