Skip to content

Rework API#22

Merged
BrainRTP merged 72 commits intomasterfrom
task/api-merge
Apr 28, 2026
Merged

Rework API#22
BrainRTP merged 72 commits intomasterfrom
task/api-merge

Conversation

@BrainRTP
Copy link
Copy Markdown
Collaborator

The old AbstractMenus/api repo is now part of this one as the :api Gradle subproject. That repo will be archived shortly and no longer maintained. Going forward all API code lives here, alongside the plugin.

What changed

  • API moved into this repo as :api. Maven coord stays the same: ru.abstractmenus:api:<ver>.
  • New addon system. Plugins can extend AbstractMenus either as a regular Bukkit plugin (Path 1, drop into plugins/) or as a lightweight jar AM loads itself (Path 2, drop into plugins/AbstractMenus/addons/).
  • New ProviderRegistry for economy / permissions / levels / placeholders / skins. Multiple providers can register at once; menus pick one with provider: "..." per action, or via a providers{} block in config.conf.
  • The plugin now uses its own SPI for built-in types (CoreExtension). If something works for core, it works for addons.

Reference addon: PlayerPointsAddon (separate repo, two branches showing both paths).

Module layout

api/      - public API, published as ru.abstractmenus:api:<ver>
plugin/   - plugin implementation, ships as the shadow jar

Build outputs in build/dist/ (aggregated) plus per-module build/libs/.

New API basics

  • AbstractMenusApi.get() - entry point, replaces Types.* and Handlers.* statics.
  • MenuExtension - the addon contract. onLoad / onEnable / onDisable plus name/version metadata.
  • TypeRegistry<T> - five of these (actions, rules, activators, item properties, catalogs). Replaces Types.register*.
  • ProviderRegistry - five sections (economy, permissions, levels, placeholders, skins). Replaces the Handlers static facade.

All public types have proper javadoc.

Addon system

Two ways to ship an addon. Same runtime behaviour, different load-path.

Path 1 (as-plugin) Path 2 (as-addon)
Drops into plugins/ plugins/AbstractMenus/addons/
Manifest plugin.yml addon.conf (HOCON)
Loaded by Bukkit AM AddonManager
Visible in /plugins /am addons

/am addons supports list, info <name>, reload <name>, load <name>, rescan. Tab completion works for all.

Provider system

Old shape: one global handler per kind. New shape: many can register, menus choose.

Resolution order:

  1. Per-action provider: "..." field in HOCON.
  2. Server default in config.conf providers{} block.
  3. Highest-priority registered provider.

Money actions accept both forms for backward compat:

takeMoney: 100                                    # default provider
takeMoney { amount: 100, provider: "vault" }      # explicit

config.conf:

providers {
  economy      = "vault"
  permissions  = "auto"
  levels       = "auto"
  placeholders = "auto"
  skins        = "auto"
}

Core providers register at priority 50; addons typically at 100.

Build outputs

  • AbstractMenus-<ver>.jar - shadowed plugin jar, drop into plugins/.
  • AbstractMenus-api-<ver>.jar (+ sources + javadoc) - api artifacts.
  • <root>/build/dist/ - all of the above in one place. ./gradlew shadowJar, ./gradlew assemble, and ./gradlew build all refresh it.

Breaking changes

Removed:

  • ru.abstractmenus.api.Handlers -> use AbstractMenusApi.get().providers().*
  • ru.abstractmenus.api.Types -> use AbstractMenusApi.get().{actions, rules, ...}()
  • AbstractMenusPlugin -> use AbstractMenusApi
  • AbstractMenusProvider -> use AbstractMenusApi.get()

In-tree call sites are migrated. Out-of-tree consumers need import updates.

Migration

- import ru.abstractmenus.api.Types;
- Types.registerAction("foo", FooAction.class, new FooAction.Serializer());
+ import ru.abstractmenus.api.AbstractMenusApi;
+ AbstractMenusApi.get().actions()
+     .register("foo", FooAction.class, new FooAction.Serializer(), this);
- import ru.abstractmenus.api.Handlers;
- Handlers.getEconomyHandler().takeBalance(player, 100);
+ import ru.abstractmenus.api.AbstractMenusApi;
+ AbstractMenusApi.get().providers().economy().takeBalance(player, 100);

register* now takes an extra MenuExtension owner parameter (this if your plugin implements MenuExtension).

BrainRTP and others added 30 commits April 22, 2026 14:36
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-module)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…fig)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…config)

Remove group/version, java toolchain block, and repos already declared in
root (mavenLocal, mavenCentral, paper, jitpack); drop redundant 'java' plugin
application and duplicate test { useJUnitPlatform() }; fix archiveFileName
to hardcode 'AbstractMenus-' prefix so the shadow artifact is not named
'plugin-<version>.jar'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…changes)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…plugin jar

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds class-level overview, registration/impl/menu examples, threading notes,
and per-method @implNote/@APinote guidance. Style template for future API
documentation passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Intermediate broken build state: references AbstractMenusApi which is added
in the next commit. api module re-compiles cleanly after Task 3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…statics

Build still fails pending AbstractMenusApi (next task).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…enusPlugin + Types accessors)

Resolves the intermediate broken build state from MenuExtension + TypeRegistry
commits. :api now compiles cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lugin bridge

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…via ServicesManager

Old Types.*.init() registration path continues unchanged. The new api field
is just sitting published; no caller yet. Cutover in the CoreExtension task.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ed yet)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…SPI)

Removes MenuActions/MenuRules/ItemProps/Activators/Catalogs — content lives
in their Core*Bundle counterparts, invoked through CoreExtension via the
new AbstractMenusApi SPI.

ItemProps.BINDINGS (a public constant co-located with the registrations)
was inlined as "bindings" literal in CoreItemPropsBundle during the earlier
migration; this commit also updates ActionPropertySet to use the literal
directly, removing the last remaining external dependency on ItemProps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers ~10 files under serializers/, data/, services/, placeholders/. Old
Types class in :api module is now unused by plugin code; deletion in next
task.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the "bindings" literal inlined during the CoreExtension cutover
with a typed constant referenced from both the registration bundle and
the single external consumer (actionClear). Keeps the registration and
consumption sites in sync at compile time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…istry

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BrainRTP and others added 27 commits April 23, 2026 09:11
Intermediate broken build state: plugin's AbstractMenusApiImpl doesn't
implement providers() yet. Fixed in the next task.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tests)

Tests will run once AbstractMenusApiImpl implements providers() in the
next task.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves intermediate broken build state from the two prior commits.
Also fixes two StubApi test doubles (CoreExtensionTest, AddonManagerIntegrationTest)
that needed to implement the new providers() abstract method.
All 237 tests (229 baseline + 8 new ProviderRegistryImplTest) pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…yet invoked)

Mirrors AbstractMenus.registerProviders exactly — same guards, same
handler constructors, same fallbacks. CoreExtension wires this in the
next task.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Transitional dual-path: old Handlers.set* (in AbstractMenus.registerProviders)
and new api.providers().register* (in this bundle) both run. Callers migrate
in the next task, old path deleted in the task after.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…roviders().*

Covers ~78 files across data/, services/, menu/, command/, extractors/,
datatype/. Handlers.set* calls in AbstractMenus.registerProviders() remain
for one more commit — removed in the next task.

Test suites that relied on mutating the static Handlers facade
(TestActionCommandBehavior, TestPropNameLorePrecompute, TestCompoundDataTypes,
TestPrimitiveDataTypes) now route through a new ApiTestSupport helper that
stubs Bukkit.getServicesManager() and installs their placeholder handler via
the ProviderRegistry so production code paths (AbstractMenusApi.get()...)
resolve correctly under unit tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All provider registration now flows through ProviderRegistry — core
providers via CoreProvidersBundle, external via addon MenuExtension.onEnable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…to-resolve

New setConfigDefaults(Function<String,String>) hook lets the impl prefer a
server-configured provider id per section. "auto" or null falls through to
priority-based auto-resolution.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Defaults to "auto" for every section when absent from user config.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Server-configured provider defaults now take effect at runtime: config.conf
`providers { economy = "playerpoints" }` makes .economy() prefer PP even
when another provider has higher priority.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* takeMoney / giveMoney / hasMoney read optional `provider` string
* Serializer fails-at-load if provider id is not registered
* Runtime resolution: explicit provider via providers().economy(id),
  else default via providers().economy() (respects config providers{})

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t-load

Covers explicit provider selection, auto-fallback on omission, scalar-form
backward compat, and fail-at-load on unknown provider id.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the style established by EconomyHandler — class-level overview,
registration example, impl bridge example, menu usage, threading notes,
per-method @param/@return/@implNote.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r/Catalog) + utilities

Matches the EconomyHandler / TypeRegistry style — class-level overview,
addon-impl example with inner Serializer, menu usage where applicable,
per-method @param / @return / @implNote.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matches EconomyHandler / TypeRegistry style — class-level overview,
addon-impl example where applicable, HOCON usage snippets, per-method
@param / @return / @implNote.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Stanislav Panchenko <brainrtp@yandex.ru>
Signed-off-by: Stanislav Panchenko <brainrtp@yandex.ru>
AddonManager:
- loadOne(name): load a single addon by addon.conf name from disk,
  respecting pluginDependencies and addonDependencies. Used by both
  /am addons load <name> and the rescan path.
- rescan(): scan addons/ and load anything not yet known. Existing
  addons are left untouched - use reload(name) to rebuild them.
- availableNotLoaded(): names of addons present on disk but not yet
  loaded. Powers tab completion for /am addons load <TAB>. Cost is
  one jar open + HOCON parse per .jar; acceptable at typical scale.
- enableSingle(): private helper extracted from loadAll, owns the
  pluginDeps + addonDeps + onLoad + onEnable sequence for one addon.

CommandAddons:
- new subcommands: load <name> and rescan
- override tabComplete to suggest:
  - args[0]: list/reload/info/load/rescan filtered by prefix
  - args[1] for reload|info: loaded-addon names
  - args[1] for load: availableNotLoaded() names

Command base:
- now also implements TabCompleter
- default tabComplete: subcommand-key matching at args[0], then
  recursive drill into the matched subcommand for args[1..]

AbstractMenus#registerCommands:
- wire setTabCompleter alongside setExecutor for /am, /var, /varp.
  /var and /varp inherit the default subcommand-key completion.
Brings in master commit 18a5627 (configurable click debounce floors +
cooldown refactor) on top of api-merge work.

Conflict resolutions:
- AbstractMenus.java: kept api/addonManager fields from HEAD; master had
  no awareness of the new API surface.
- MainConfig.java: kept providerDefaults map from HEAD AND added
  clickDebounceDefaultMs/ShiftMs from master. Both load() blocks merged.
- menu/AbstractMenu.java: kept HEAD's Handlers->AbstractMenusApi
  migration; pulled in master's debounce floor feature (slotClickExpiry,
  SlotClickKey record, debounceFloorMs helper, click() with cooldown
  bookkeeping) plus AbstractMenus + MainConfig imports.
- config.conf: kept providers{} block from HEAD AND added
  clickDebounce{} block from master.
Path 1 addons (plugin-as-addon style, e.g. PlayerPointsAddon on the
as-plugin branch) declare `depend: [AbstractMenus, ...]` in their
plugin.yml and Bukkit only enables them AFTER AbstractMenus' own
onEnable returns. If AM loaded menus inline within onEnable, HOCON
parse-time validation of provider:per-action references contributed by
those addons would fail with 'Unknown economy provider X' even though
the addon would register X seconds later.

Move the initial loadMenus() call out of onEnable and into a
ServerLoadEvent listener. The event fires after every plugin's
onEnable has completed (both at server startup and on /reload), so by
the time we parse menus all Path 1 contributions are present.

Path 2 addons (jars in plugins/AbstractMenus/addons/) are unaffected -
AddonManager.loadAll() still runs inline within AM's onEnable, before
the listener is even registered.

Listener self-unregisters after firing once so any future hypothetical
re-fire of ServerLoadEvent does not double-load menus mid-session.
Default Gradle naming was producing 'plugin-<ver>.jar' for the regular
jar task because the subproject directory is named 'plugin'. That
name is misleading on disk - server admins encountering it have no
hint that it's part of AbstractMenus.

Set base.archivesName = 'AbstractMenus' on the :plugin module so both
the thin jar and the shadowJar share the canonical project name. Add
a 'thin' classifier on the regular jar task so it cannot be confused
with the user-facing fat jar:

  plugin/build/libs/
    AbstractMenus-2.0.0-alpha.jar         <- shaded, ships to plugins/
    AbstractMenus-2.0.0-alpha-thin.jar    <- dev-only, no bundled deps

shadowJar gets archiveClassifier = '' explicitly to drop the default
'all' classifier the shadow plugin would otherwise add.

CI workflows (build.yml + release.yml) updated to skip the -thin jar
when picking the artifact to publish. Without the filter, ASCII sort
order would have selected -thin first ('-' = 0x2D < '.' = 0x2E).
Local file in api/build/libs/ was 'api-<ver>.jar' (default from
project.name = 'api'). On CI artifact downloads or manual SCP it
was hard to tell at a glance which project the jar belonged to.

Set base.archivesName = 'AbstractMenus-api' on :api so the local file
is now 'AbstractMenus-api-<ver>.jar', visually grouping with the
'AbstractMenus-<ver>.jar' shadow jar from :plugin.

The Maven publication is unaffected: maven-publish renames artifacts
to <artifactId>-<version>.jar inside the repository regardless of the
local jar filename, so the Maven coord 'ru.abstractmenus:api:<ver>'
still resolves correctly. Verified via :api:publishToMavenLocal:
~/.m2/repository/ru/abstractmenus/api/2.0.0-alpha/ still contains
api-2.0.0-alpha.{jar,pom,module} as expected.

CI workflows updated to grep AbstractMenus-api-*.jar.
Multi-module layout left release artifacts scattered across two paths:
  plugin/build/libs/AbstractMenus-<ver>.jar         (fat shadow)
  api/build/libs/AbstractMenus-api-<ver>.jar        (api binary)
  api/build/libs/AbstractMenus-api-<ver>-sources.jar
  api/build/libs/AbstractMenus-api-<ver>-javadoc.jar

For ops, manual SCP, and CI artifact uploads it's nicer to have all
four files in one directory. Add a 'dist' Copy task at root that
gathers them into <root>/build/dist/ via the per-task archiveFile
providers (so the Gradle file resolution stays lazy and incremental
caching keeps working).

  ./gradlew dist          # explicit
  ./gradlew build         # implicit (auto-finalizer on root build)

Per-module build/libs/ paths remain unchanged so existing CI grep
patterns and devs' muscle memory keep working - dist is purely
additive, not a replacement.

Root needs the 'base' plugin to have its own 'build' lifecycle task
(otherwise tasks.named('build') has nothing to attach to).
build/dist/ was only refreshed via './gradlew build'. Devs running the
fast dev-loop command './gradlew :plugin:shadowJar' got the new jar in
plugin/build/libs/ but had to remember to also run :dist (or build)
before scp/cp. The two paths drifted.

Hook dist as a finalizer on:
  - :plugin:shadowJar  (dev-loop fast path)
  - assemble           (all artifacts, no tests)
  - build              (already; kept for clarity)

dist is a Copy task with proper input/output declarations so finalizing
it three times in a single invocation does no extra work; Gradle's
incremental build skips when sources are up-to-date.
@BrainRTP BrainRTP merged commit 4d37ab1 into master Apr 28, 2026
2 checks passed
@BrainRTP BrainRTP deleted the task/api-merge branch April 28, 2026 06:57
@BrainRTP BrainRTP mentioned this pull request Apr 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant