Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
4e498a8
fix: round 1 of code-review fixes (P0-3 / P1-4 / P1-5+P1-7 / P1-6 / P…
BrainRTP Apr 27, 2026
d7af30a
fix: round 1 of code-review fixes - Folia correctness (P0-1, P0-2, P1…
BrainRTP Apr 27, 2026
1f83137
release: bump to 2.0.0-alpha.2
BrainRTP Apr 28, 2026
ae2199c
fix: round 2 of code-review fixes (P1-9 / P1-10 / P1-11 / P1-12)
BrainRTP Apr 28, 2026
3163ffc
refactor(api)!: ProviderRegistry → ProviderSection<T> parametric shape
BrainRTP Apr 28, 2026
d1349e0
style: replace fully-qualified names with imports, drop column alignment
BrainRTP Apr 28, 2026
ef86e36
fix: P2 batch A - AddonManager polish (P2-7 / P2-8 / P2-9 / P2-17)
BrainRTP Apr 28, 2026
245c099
fix: P2 batch B - security defence-in-depth (P2-1 / P2-2 / P2-3 / P2-…
BrainRTP Apr 28, 2026
ab55c38
build(plugin): drop bundled Adventure - rely on Paper's bundled copy
BrainRTP Apr 28, 2026
e4da289
fix: P2 batch C - money/provider polish (P2-6 / P2-13 / P2-16)
BrainRTP Apr 28, 2026
bffc7b0
fix: P2 batch D - misc tactical cleanup (P2-10 / P2-11 / P2-12 / P2-1…
BrainRTP Apr 28, 2026
422026a
fix: P2 batch E - lifecycle and scheduling (P2-15 / P2-18 / P2-20)
BrainRTP Apr 28, 2026
e44c082
fix: P3 nits (P3-1 / P3-2 / P3-5 / P3-6)
BrainRTP Apr 28, 2026
2eda19d
fix: round-4 P0+P1 from re-review (Folia + addon lifecycle)
BrainRTP Apr 28, 2026
d1a9557
fix: round-5 P2+P3 (security hardening + small cleanups)
BrainRTP Apr 28, 2026
b9bbad5
feat(commands): show Path 1 + built-in extensions in /am addons list
BrainRTP Apr 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# AbstractMenus

<a href="https://github.com/AbstractMenus/plugin/blob/master/LICENSE"><img src="https://img.shields.io/badge/License-MIT-red.svg" alt="license"/></a>
<a href="https://github.com/AbstractMenus/plugin/blob/master/LICENSE"><img src="https://img.shields.io/badge/version-1.18.0-blue" alt="version"/></a>
<a href="https://github.com/AbstractMenus/minecraft-plugin/blob/master/LICENSE"><img src="https://img.shields.io/badge/License-MIT-red.svg" alt="license"/></a>
<a href="https://github.com/AbstractMenus/minecraft-plugin/releases"><img src="https://img.shields.io/badge/version-2.0.0--alpha.2-blue" alt="version"/></a>
<img src="https://img.shields.io/badge/minecraft-1.20.6+-brightgreen" alt="minecraft"/>
<img src="https://img.shields.io/badge/paper-1.21.11-brightgreen" alt="paper"/>
<img src="https://img.shields.io/badge/java-21-orange" alt="java"/>
Expand Down Expand Up @@ -79,7 +79,9 @@ cd AbstractMenus
./gradlew shadowJar
```

Output: `build/libs/AbstractMenus-<version>.jar`.
Output: `build/dist/AbstractMenus-<version>.jar` (the aggregator also drops the
api jar + sources + javadoc next to it; per-module jars stay in
`plugin/build/libs/` and `api/build/libs/`).

Other useful tasks:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ static AbstractMenusApi get() {
* diagnostic &mdash; logged on startup and surfaced in
* {@code /am addons list}.
*
* @return the API version (e.g. {@code "2.0.0-alpha"})
* @return the API version (e.g. {@code "2.0.0-alpha.2"})
*/
String apiVersion();
}
11 changes: 8 additions & 3 deletions api/src/main/java/ru/abstractmenus/api/Logger.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,18 @@ private Logger(){}
* Install the backing JUL logger. Called once by AbstractMenus core during
* plugin {@code onEnable}.
*
* <p>Addons should not call this &mdash; replacing the logger would
* redirect <em>all</em> subsequent log output (including core's) away from
* the plugin's channel.
* <p>Set-once: subsequent calls are silently skipped so an addon cannot
* replace the logger to silence or capture core's output. The skip
* (rather than throw) keeps Bukkit {@code /reload} working - on reload
* core's {@code onEnable} runs again and re-invokes this method, which
* is now a no-op.
*
* @param log the JUL logger to delegate to; never {@code null}
* @throws NullPointerException if {@code log} is null
*/
public static void set(java.util.logging.Logger log){
if (log == null) throw new NullPointerException("logger");
if (logger != null) return;
logger = log;
}

Expand Down
26 changes: 16 additions & 10 deletions api/src/main/java/ru/abstractmenus/api/MenuExtension.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,25 +80,31 @@ default void onDisable(AbstractMenusApi api) {
}

/**
* Display name. Used by {@code /am addons list} and log messages. May
* return {@code null}; for AM-loaded addons (Path 2) the name from
* {@code addon.conf} is used as a fallback.
* Display name. Used by {@code /am addons list} and log messages.
*
* @return the extension display name, or {@code null} to defer
* <p>Default falls back to {@code getClass().getSimpleName()} so a
* Path 1 plugin-as-addon that forgets to override does not render
* as "null" in operator output. Path 2 implementations should
* override this to match the {@code name} field in {@code addon.conf}.
*
* @return the extension display name; never {@code null} by default
*/
default String name() {
return null;
return getClass().getSimpleName();
}

/**
* Extension version string. Used for diagnostics. May return {@code null}
* &mdash; Path 2 falls back to the {@code version} from
* {@code addon.conf}.
* Extension version string. Used for diagnostics.
*
* @return the version, or {@code null} to defer
* <p>Default returns {@code "unknown"} so {@code /am addons list}
* does not render "vnull". Path 1 plugins should override to return
* their plugin.yml version; Path 2 implementations should match the
* {@code version} field in {@code addon.conf}.
*
* @return the version; never {@code null} by default
*/
default String version() {
return null;
return "unknown";
}

/**
Expand Down
91 changes: 26 additions & 65 deletions api/src/main/java/ru/abstractmenus/api/ProviderRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,93 +6,54 @@
import ru.abstractmenus.api.handler.PlaceholderHandler;
import ru.abstractmenus.api.handler.SkinHandler;

import java.util.Collection;

/**
* Registry of pluggable handler providers (economy, permissions, levels,
* placeholders, skins). Replaces the old static {@code Handlers.set*()/get*()}
* facade with an owner-aware registry that supports multiple providers per
* section plus priority-based auto-resolution.
* placeholders, skins). Replaces the old static {@code Handlers} facade with
* an owner-aware registry that supports multiple providers per section plus
* priority-based auto-resolution and a configurable default.
*
* <p>Each section ({@link #economy()}, {@link #permissions()},
* {@link #levels()}, {@link #placeholders()}, {@link #skins()}) returns a
* typed {@link ProviderSection} you register on and resolve from. The
* registry itself is just five getters - all per-type behaviour lives on
* {@link ProviderSection}, so adding a sixth provider type later means
* adding one method here, not five.
*
* <h2>Registration</h2>
*
* <pre>{@code
* public final class MyEconomyAddon implements MenuExtension {
* @Override public void onEnable(AbstractMenusApi api) {
* api.providers().registerEconomy(
* api.providers().economy().register(
* "playerpoints",
* new PlayerPointsEconomy(pp),
* 100, // priority higher wins in auto-resolve
* this); // owner for unregisterAll on reload
* 100, // priority - higher wins in auto-resolve
* this); // owner - AbstractMenus uses this for cleanup
* }
* }
* }</pre>
*
* <h2>Resolution</h2>
*
* <ul>
* <li>{@link #economy()} &mdash; highest-priority registered handler, or
* first-registered on ties. Returns {@code null} if none registered.</li>
* <li>{@link #economy(String)} &mdash; explicit lookup by id.</li>
* <li>{@link #allEconomy()} &mdash; every registration, for introspection.</li>
* <li>{@link #hasEconomy(String)} &mdash; validation helper (used by
* menu-serializers to fail-at-load when a HOCON file references an
* unknown provider).</li>
* </ul>
* <h2>Lookup</h2>
*
* <p>Same shape for permissions / levels / placeholders / skins.
* <pre>{@code
* EconomyHandler eco = api.providers().economy().resolve();
* EconomyHandler vault = api.providers().economy().resolve("vault");
* boolean hasPP = api.providers().economy().has("playerpoints");
* Collection<EconomyHandler> all = api.providers().economy().all();
* }</pre>
*
* @see ProviderSection
* @see AbstractMenusApi#providers()
*/
public interface ProviderRegistry {

// ---- Economy ---------------------------------------------------------

void registerEconomy(String id, EconomyHandler handler, int priority, MenuExtension owner);
EconomyHandler economy();
EconomyHandler economy(String id);
Collection<EconomyHandler> allEconomy();
boolean hasEconomy(String id);

// ---- Permissions -----------------------------------------------------

void registerPermissions(String id, PermissionsHandler handler, int priority, MenuExtension owner);
PermissionsHandler permissions();
PermissionsHandler permissions(String id);
Collection<PermissionsHandler> allPermissions();
boolean hasPermissions(String id);

// ---- Levels ----------------------------------------------------------

void registerLevels(String id, LevelHandler handler, int priority, MenuExtension owner);
LevelHandler levels();
LevelHandler levels(String id);
Collection<LevelHandler> allLevels();
boolean hasLevels(String id);

// ---- Placeholders ----------------------------------------------------

void registerPlaceholders(String id, PlaceholderHandler handler, int priority, MenuExtension owner);
PlaceholderHandler placeholders();
PlaceholderHandler placeholders(String id);
Collection<PlaceholderHandler> allPlaceholders();
boolean hasPlaceholders(String id);
ProviderSection<EconomyHandler> economy();

// ---- Skins -----------------------------------------------------------
ProviderSection<PermissionsHandler> permissions();

void registerSkins(String id, SkinHandler handler, int priority, MenuExtension owner);
SkinHandler skins();
SkinHandler skins(String id);
Collection<SkinHandler> allSkins();
boolean hasSkins(String id);
ProviderSection<LevelHandler> levels();

// ---- Cleanup ---------------------------------------------------------
ProviderSection<PlaceholderHandler> placeholders();

/**
* Remove every provider registration (across all sections) owned by
* {@code owner}. Called by AddonManager when an addon is disabled.
*
* @param owner the extension whose providers should be cleared
*/
void unregisterAll(MenuExtension owner);
ProviderSection<SkinHandler> skins();
}
106 changes: 106 additions & 0 deletions api/src/main/java/ru/abstractmenus/api/ProviderSection.java
Original file line number Diff line number Diff line change
@@ -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).
*
* <p>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<EconomyHandler>}, and so on. There is no per-type
* boilerplate on the registry interface.
*
* <h2>Resolution</h2>
*
* <ul>
* <li>{@link #resolve()} returns the highest-priority registered handler,
* overridden by the {@code config.conf providers.<kind>} value if it
* names a registered handler. Returns {@code null} if the section is
* empty.</li>
* <li>{@link #resolve(String)} returns the explicitly-named handler, or
* {@code null} if no handler with that id is registered.</li>
* </ul>
*
* <h2>Example</h2>
*
* <pre>{@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");
* }</pre>
*
* <h2>Thread-safety</h2>
*
* 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<T> {

/**
* 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.<kind>} 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<T> 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<String> ids();

/**
* @param id case-insensitive identifier
* @return whether a handler with this id is registered
*/
boolean has(String id);
}
21 changes: 7 additions & 14 deletions api/src/main/java/ru/abstractmenus/api/TypeRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
* {@link AbstractMenusApi#itemProperties()}, and
* {@link AbstractMenusApi#catalogs()} &mdash; one per extension surface.
*
* <p>Registration takes a {@link MenuExtension} "owner" so
* {@link #unregisterAll(MenuExtension)} can wipe every entry an addon
* contributed when that addon is disabled or reloaded.
* <p>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.
*
* <h2>Example</h2>
*
Expand Down Expand Up @@ -61,9 +62,9 @@ public interface TypeRegistry<T> {
* @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.
*/
<S extends T> void register(String key,
Class<S> type,
Expand Down Expand Up @@ -92,12 +93,4 @@ <S extends T> void register(String key,
* @return all registered keys (lowercased)
*/
Set<String> 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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
* <h2>Registration</h2>
*
* 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}:
*
* <h2>Example &mdash; bridging PlayerPoints</h2>
Expand Down Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
*
* Multiple handlers may coexist &mdash; 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}.
*
* <h2>Example &mdash; bridging MMOCore</h2>
Expand Down Expand Up @@ -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
*/
Expand Down
Loading
Loading