Troubleshooting
Concrete failures — the symptom, what causes it, and the fix.
Grouped by what you actually see.
Build and startup
NoClassDefFoundError: fr/robie/paperdispatch/...
The library is not in your jar. It is not installed on the server, so
compileOnly (Gradle) or provided (Maven) is wrong for it — use
implementation / default scope and a shade step. See
Shading & relocation.
If you are already shading, you are probably shipping the plain jar output
instead of the shaded one. Check:
unzip -l target/yourplugin.jar | grep paperdispatchClassNotFoundException only from the built jar
Shadow's minimize() strips classes it cannot see being used, including ones
reached reflectively. Build once without it to confirm, then add explicit
excludes rather than dropping it entirely.
JitPack returns 404 for the version
Either the tag does not exist yet — the version in the project's pom.xml is
the next, unreleased one — or the group id is wrong. It is
com.github.1robie.paper-dispatch, because the project is multi-module;
com.github.1robie:paper-dispatch never resolves. Check the
build log before assuming the
coordinates are wrong: JitPack compiles a new tag on first request.
The server fails to start on Spigot
Expected. There is no Bukkit fallback — see Supported platforms.
IllegalArgumentException at command construction
Two causes, both from validation that is deliberately loud:
- The argument name uses the reserved flag prefix. Value flags generate
synthetic argument names sharing a prefix (
flag$by default). Rename the argument, or change the prefix withsetFlagValuePrefix— before adding any arguments, since a late change would invalidate names that already passed validation. - A flag name or alias collides with one already registered on that command.
Commands do not appear
Nothing registered at all
Check that you called both halves:
this.commands.trackCommand(new HealCommand(this));
this.commands.flushRegistrations(); // easy to forgettrackCommand only records the command; flushRegistrations is what reaches
the server. On the flush you should see
Registering N commands via lifecycle events... in the log.
Failed to register command '<name>'
The server rejected it, and the cause is on the same log line — usually a name the server will not accept, or a tree that was mutated after being built.
Aliases missing: the server declined these aliases: …
Another plugin or a vanilla command already claimed them. The command itself
registered fine, and the namespaced form (/yourplugin:name) still works.
Rename the alias if you want the short form.
Could not find the 'allowsLifecycleRegistration' field
The manager probes that field to choose between lifecycle and dynamic registration. Seeing this means Paper changed internals — commands may still register via the dynamic path, but treat it as a signal to update the library.
Nodes silently missing from a command
Anything added to a builder after it has been attached to a parent is
discarded without an error: Brigadier's then builds the child immediately and
stores an immutable node. Finish configuring a node before passing it to
addSubCommand.
Behaviour at runtime
A sub-command's alias does not work under the parent's alias
/g new is not built when /guild has alias g and create has alias new.
Aliases are one level deep on purpose — the full cross-product would be real
nodes sent to every client. See
Sub-commands.
/cmd --flag sub says unknown command
Flags must come last. Flag nodes branch only into other flag nodes, so
/cmd sub --flag parses and the reverse does not.
An optional argument is empty when a value was given
getOptionalArgument returns empty if the value matches a registered flag
name — that is what stops /heal --silent capturing silent as the optional
target. The side effect is that a player genuinely named like one of your flags
cannot be passed positionally. dispatch.isFlagLikeValue(...) exposes the same
check so you can explain it.
A sub-command shadows an argument value
Brigadier resolves literals before arguments, so a sub-command named list wins
over a string argument in the same position, and that value never reaches the
argument.
IllegalArgumentException from getArgument
Brigadier throws the same exception for "no such argument" and "wrong type", so
this is either a typo in the name or a mismatched class. Probe with
hasArgument(name), which checks presence only — getOptionalArgument
deliberately still throws on a type mismatch rather than hiding the bug.
The command runs but the player is null
getSenderAsPlayer() is @Nullable even with setPlayerOnly() set. Null-check
it rather than relying on a requirement staying in place. Console and command
blocks are the usual sources.
A player without permission sees "Unknown command"
That is how requirements work — they are Brigadier requires predicates
evaluated before parsing, so the command is hidden rather than refused. To
explain the refusal instead, check inside perform. See
Requirements & permissions.
Permission changes do not take effect immediately
The command tree is per-source and sent when the server decides to send it. A permission granted at runtime applies the next time that player receives the tree.
Tab-completion
Offline player names do not complete
No cache is installed. OfflinePlayerCache.install(this) in onEnable, and
uninstall in onDisable. Without one, OfflinePlayerArgument rejects every
value with a "no offline player cache is installed" error and suggests nothing.
A cache is built but the argument still ignores it
A cache from builder(...).build() is not the global instance, and
OfflinePlayerArgument reads the global one. Use install, or accept that the
argument will not see your cache.
Completion is truncated
maxSuggestions bounds it, on purpose: the name index is unbounded, so an empty
prefix would otherwise serialise every known player name on every keystroke.
Raise it via the builder if you must.
Mojang API rate-limited us while refreshing …
A rename refresh was throttled. The stale name is kept and the refresh retried
later; nothing is broken. The same applies to Failed to reach Mojang API …
when Mojang is unreachable.
Performance
Startup warning about flag count
More than four flags on one command. The warning reports the node count the flag
subtree will occupy — growth is faster than n!, and the tree ships to every
client. Collapse booleans into one value flag, or call
setFlagCountWarnThreshold(FLAG_COUNT_WARN_DISABLED) once you have measured and
accepted the cost.
Thread pool leak across reloads
A cache you built yourself was never closed. Each OfflinePlayerCache owns an
HttpClient with its own threads. Call close() in onDisable, or use
install / uninstall.
Still stuck
Open an issue on the repository with the server version, the library version and the relevant log lines.
Last updated on