Skip to content

1.0.3

Latest

Choose a tag to compare

@1robie 1robie released this 31 Jul 19:27

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

  • requiresConfirmation was a no-op unless another requirement was also present.
    Commands.restricted(...) sat behind an isEmpty() check, so a command asking only for
    confirmation kept Brigadier's default predicate. Security-relevant: restricted is what stops
    sensitive commands running from chat click events.
  • Pre-built String arguments lost their children. Re-wrapping the type to make it flag-aware
    dropped .then(...) subtrees, redirects and forks - silently, and only for StringArgumentType.
  • Commands vanished on a datapack reload. Each COMMANDS lifecycle 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.
  • OfflinePlayerCache leaked threads. A redundant install() built and discarded a cache that
    still owned a live HttpClient and a queued scan; nothing ever closed the client. The cache is
    now AutoCloseable, and uninstall() closes it.
  • A flag with an explicit null default was reported present by one accessor and absent by
    another.
  • EnumArgument concatenated 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, so isRegistered no 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 usually null on an
    NPE.

✨ Added

  • CommandDispatch.getExecutor() and getLocation() - the entity and position /execute as
    and /execute positioned substitute, which differ from the sender.
  • unregisterAll() - every tracked command whatever plugin owns it, alongside
    unregisterAll(Plugin). The right call for a host plugin's onDisable, 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).
    Flags now covers every ArgumentTypes entry.
  • Configurable flag-tree warning: setFlagCountWarnThreshold(int) /
    flagCountWarnThreshold(int), with FLAG_COUNT_WARN_DISABLED to silence it.
  • PluginLogger - routable, assertable diagnostics instead of plugin.getLogger() directly.
  • OfflinePlayerCache: close(), getPlugin(), and maxSuggestions(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.
  • removeCommand reflection is cached, as the PaperCommands handles 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. addFlag now warns past 4 flags.
  • Flags must come last: /cmd sub --verbose parses, /cmd --verbose sub does not.
  • README rewritten with actual usage documentation, shade-and-relocate guidance, and the known
    limitation that bootstrap registration is unsupported (PluginBootstrap#bootstrap runs before
    createPlugin, so no plugin instance exists yet).

🔧 Internal

  • Tests: 16 → 103. New suites for CommandManager (the module had none), SubCommand.build()
    tree shape, and CommandDispatch argument accessors. testThreadSafety could 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.