Releases: 1robie/paper-dispatch
Release list
1.0.3
Correctness pass over command registration and the flag tree, clearer naming on the two APIs that
were easiest to confuse, and the first real test suite (16 → 103 tests).
Build requirement changed: building now needs JDK 25, because paper-api is compiled for
Java 25 (class file major 69) and an older javac cannot read it. The published jars are still
built with --release 21, so consumers still only need Java 21+.
⚠️ Breaking changes
CommandDispatch and FlagContext are now final. Both are documented as immutable
snapshots; subclassing them was never supported. Subclasses will no longer compile.
ICommandManager gained several methods (trackCommand, flushRegistrations,
unregisterReloadableCommands, unregisterCommands(Collection), unregisterAll(),
unregisterAll(Plugin), getCommands, getCommand). If you only use CommandManager you are
unaffected. If you wrote your own ICommandManager implementation, you must implement them.
Argument type mismatches now throw instead of being swallowed. Brigadier reports both "no such
argument" and "wrong type" with the same IllegalArgumentException, so
CommandDispatch.getOptionalArgument and getArgument(name, type, default) previously turned a
caller's type mistake into an empty Optional or a silent default. They now let a genuine type
mismatch propagate; absence still returns empty/default as before. Use the new
hasArgument(String) if you need a presence check.
FlagContext.getValue and CommandDispatch.getFlagValue are now @Nullable.
Flag.defaultTo(null) is legal, so a flag can legitimately hold null. Java callers are unaffected;
Kotlin and null-checked toolchains will want to adjust.
SubCommand.setFlagValuePrefix throws if called after arguments are added. A late change would
retroactively invalidate argument names that already passed validation. Set it first.
EnumArgument.convert returns E instead of Enum<E>. Source-compatible; binary-incompatible
if you compiled against the old descriptor.
Deprecations
All old names still work and delegate to the new ones. They are marked forRemoval, so expect a
removal in a future release.
| Deprecated | Use instead | Why |
|---|---|---|
registerCommand(cmd) |
trackCommand(cmd) |
It only tracks; nothing reaches the server until the flush |
registerCommand(builder) |
trackCommand(builder) |
same |
registerCommands() |
flushRegistrations() |
One letter from registerCommand, entirely different job |
unregisterCommands() |
unregisterReloadableCommands() |
Removed only reloadable commands despite the name, and overloaded unregisterCommands(Collection) with unrelated semantics |
getPlayer() |
getSenderAsPlayer() |
Returns the sender, not an argument |
getOptionalPlayer(name) |
resolvePlayer(name) |
Resolves an argument; read as a pair with getPlayer() and wasn't one |
getOptionalPlayers/Entity/Entities/PlayerProfiles/BlockPosition/FinePosition |
resolvePlayers, resolveEntity, … |
same |
Note forRemoval = true emits a removal warning, which @SuppressWarnings("deprecation") does
not silence - you need "removal".
🐛 Fixed
requiresConfirmationwas a no-op unless another requirement was also present.
Commands.restricted(...)sat behind anisEmpty()check, so a command asking only for
confirmation kept Brigadier's default predicate. Security-relevant:restrictedis what stops
sensitive commands running from chat click events.- Pre-built
Stringarguments lost their children. Re-wrapping the type to make it flag-aware
dropped.then(...)subtrees, redirects and forks - silently, and only forStringArgumentType. - Commands vanished on a datapack reload. Each
COMMANDSlifecycle event supplies a fresh
dispatcher, but registration state persisted across events, making every firing after the first
a no-op. - Lifecycle registration credited the wrong plugin. Commands hosted on behalf of another plugin
landed under the host's namespace, disagreeing with the dynamic path. unregisterCommands()was a silent no-op on the lifecycle path. Removals were derived inside
the handler, after the bookkeeping they came from had already been purged.OfflinePlayerCacheleaked threads. A redundantinstall()built and discarded a cache that
still owned a liveHttpClientand a queued scan; nothing ever closed the client. The cache is
nowAutoCloseable, anduninstall()closes it.- A flag with an explicit null default was reported present by one accessor and absent by
another. EnumArgumentconcatenated player input into a MiniMessage source string. Now inserted as a
literal component, so tags like<click:run_command:…>are shown, not parsed.- Aliases the server refuses are now reported. Paper drops an alias already claimed by another
command and only returns what it took; that used to pass unnoticed. - Reloadable-command bookkeeping is dropped consistently whether or not the server-side removal
succeeded, soisRegisteredno longer reports commands the manager has released. syncCommands()hops to the main thread instead of mutating the dispatcher from a worker.- Built commands log the throwable rather than just
getMessage(), which is usuallynullon an
NPE.
✨ Added
CommandDispatch.getExecutor()andgetLocation()- the entity and position/execute as
and/execute positionedsubstitute, which differ from the sender.unregisterAll()- every tracked command whatever plugin owns it, alongside
unregisterAll(Plugin). The right call for a host plugin'sonDisable, since its lifecycle
handler dies with it.hasArgument(String)- presence check that distinguishes absent from wrong-type.- Suggestion tooltips:
Flag.suggests(Map<String, Component>). - Seven new flag factories -
resourceFlag,resourceKeyFlag,signedMessageFlag,
blockInWorldPredicateFlag,columnBlockPositionFlag,columnFinePositionFlag(×2).
Flagsnow covers everyArgumentTypesentry. - Configurable flag-tree warning:
setFlagCountWarnThreshold(int)/
flagCountWarnThreshold(int), withFLAG_COUNT_WARN_DISABLEDto silence it. PluginLogger- routable, assertable diagnostics instead ofplugin.getLogger()directly.OfflinePlayerCache:close(),getPlugin(), andmaxSuggestions(int)/logger(...)
builder options.- Reloadable commands, async pre-population, and the optional player/entity/position resolvers
(carried over from earlier in this cycle).
⚡ Performance
- Case-insensitive UUID lookup is now O(1) via a lower-cased index. It previously scanned the
entire unbounded name index while holding the lock, on the command-parsing hot path. - Player-name suggestions are capped (
maxSuggestions, default 50). An empty prefix used to
serialize every known name on every keystroke. - Mojang name refreshes are de-duplicated per UUID.
removeCommandreflection is cached, as thePaperCommandshandles already were.
📖 Documentation
- The flag tree materialises one node per ordered subset of the flag set, so it grows faster than
n!. Measured, per command: 3 flags → 33 nodes, 4 → 131, 5 → 653, 6 → ~3900. That tree is sent
to every client on join.addFlagnow warns past 4 flags. - Flags must come last:
/cmd sub --verboseparses,/cmd --verbose subdoes not. - README rewritten with actual usage documentation, shade-and-relocate guidance, and the known
limitation that bootstrap registration is unsupported (PluginBootstrap#bootstrapruns before
createPlugin, so no plugin instance exists yet).
🔧 Internal
- Tests: 16 → 103. New suites for
CommandManager(the module had none),SubCommand.build()
tree shape, andCommandDispatchargument accessors.testThreadSafetycould never fail -
assertions ran on worker threads and the futures were discarded. - Sources and Javadoc jars are now published.
- Dependabot watched a directory that pins no versions; now watches the root plus GitHub Actions.
- Resource filtering scoped to the plugin descriptors, so binary resources can't be corrupted.
- Dropped
-Dnet.bytebuddy.experimental, which forced byte-buddy onto an unsupported JDK instead
of fixing the cause; Mockito bumped and attached as a proper agent. - New build workflow with Discord notifications.
1.0.2 - OfflinePlayerCache & OfflinePlayer argument
✨ New
- OfflinePlayerCache: two-tier player name cache, with Mojang stale-name refresh, fluent Builder for customization,
thread-safe global instance, cache stats, and clear/reset - OfflinePlayerArgument: Brigadier argument type resolving player names to
UUID via the cache, with tab-completion.
📦 Usage
// Quick setup
OfflinePlayerCache.install(this);
// Or with custom settings
OfflinePlayerCache.builder(this)
.maximumSize(5000)
.expireAfterwrite(Duration.ofMinutes(10))
.recordStats(true)
.buildAndRegister();
// In commands
.addRequiredArgument("target", new OfflinePlayerArgument())1.0.1 - Fix JitPack publish
chore: add JitPack configuration for OpenJDK 25
1.0.0 - Command Framework Overhaul
🚀 Features
- Introduced command result types and enhanced command dispatching with flag context
- Added EnumArgument class for custom enum argument handling
- Enhanced SubCommand with confirmation requirement and argument handling
🔧 Improvements & Refactors
- Added null checks for parameters across command classes
- Fixed usage of anyMatch → allMatch
- Added @SuppressWarnings annotations to EnumArgument and SubCommand
🛡️ CI & Quality
- Added CodeQL analysis and Dependabot dependency management
- Updated CodeQL Java version to 25
- Updated maven-compiler-plugin to 3.9.0 and maven-surefire-plugin to 3.5.6
- Updated Maven dependencies and resources plugin
📄 Documentation & Licensing
- Added README with project overview, installation, and requirements
- Added MIT License