A Velocity proxy plugin framework providing structured command systems, event utilities, and lifecycle integration built on the Hierarchy-Framework.
Velocity-Plugin-Framework bridges the Velocity proxy lifecycle with the component-based hierarchy architecture, automatically handling registration and teardown of listeners, commands, and subcommands as components are initialized and shut down.
- Automatic Velocity registration — listeners, commands, and subcommands are registered/unregistered through hierarchy lifecycle callbacks
- Type-safe command system with sender validation —
Player, console, or anyCommandSource - Built-in subcommand routing with automatic argument stripping and tab completion delegation
- Cancellable command events at every execution stage — execute and tab-complete
- Event dispatch utilities — fire-and-forget and blocking, result-returning dispatch with
CompletableFutureresolution - Task scheduling with
ChronoUnit-to-Durationconversion — immediate, scheduled, and repeating with cancellation suppliers - MiniMessage-based messaging — configurable prefixes, broadcasting, filtering, and ignore lists
- Adventure-native — built directly on Velocity's
Audience/Componentmodel - Custom event base classes with allow/deny cancellation semantics on top of Velocity's
ResultedEvent EventPriorityconstants mapped to Velocity's descendingPostOrdermodel- Designed for modern Java (Java 25+)
VelocityPlugin (implements Plugin)
└─ Manager
└─ BaseCommand (Node under the Manager)
└─ BaseSubCommand (Node under the command)
Commands and subcommands integrate directly into the hierarchy as Nodes, each with typed access to its parent:
| Component | Hierarchy Role | Velocity Integration |
|---|---|---|
VelocityPlugin |
Plugin | Proxy lifecycle bridge, component registration |
Manager |
Manager | Organizational grouping |
BaseCommand |
Node under a Manager | Registered with CommandManager |
BaseSubCommand |
Node under a command | Attached to parent command |
Velocity-Plugin-Framework requires Java 25+ and the Velocity API.
The following is only needed at compile time for annotation processing:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.46</version>
<scope>provided</scope>
</dependency>Velocity-Plugin-Framework depends on the following libraries, which are included automatically through Maven:
- Hierarchy-Framework – Plugin, Manager, and Node hierarchy with lifecycle management.
- Dependency Injector – Container management, classpath scanning, and component wiring.
- Utilities – Generic type resolution, string utilities, and casting helpers.
Add the dependency to your Maven project:
<dependencies>
<dependency>
<groupId>io.github.trae</groupId>
<artifactId>velocity-plugin-framework</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.velocitypowered</groupId>
<artifactId>velocity-api</artifactId>
<version>3.5.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>The Velocity API is resolved from the PaperMC repository:
<repositories>
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
</repositories>Unlike Bukkit, Velocity has no onEnable/onDisable. Plugins are constructed by Velocity's
dependency injector and notified of readiness through ProxyInitializeEvent. Your concrete
plugin class carries the @Plugin and @Inject annotations, extends VelocityPlugin, and
forwards the injected ProxyServer and data directory to super:
@Plugin(
id = "core",
name = "Core",
version = "0.0.1",
authors = {"Trae"}
)
public class CorePlugin extends VelocityPlugin {
@Inject
public CorePlugin(final ProxyServer proxyServer, final @DataDirectory Path dataDirectory) {
super(proxyServer, dataDirectory);
}
@Subscribe
public void onProxyInitialize(final ProxyInitializeEvent event) {
this.initializePlugin();
}
@Subscribe
public void onProxyShutdown(final ProxyShutdownEvent event) {
this.shutdownPlugin();
}
}Extend BaseCommand with the appropriate sender type. The second type parameter names the owning
Manager, which the command resolves through getParent(). Permission is passed via the constructor:
@Singleton
public class AccountCommand extends BaseCommand<CorePlugin, AccountManager, CommandSource> {
public AccountCommand() {
super("account", "Account management", List.of("acc", "client"), "core.commands.account");
}
@Override
public void execute(final CommandSource sender, final String[] args) {
UtilMessage.message(sender, "Account", "Account command executed!");
}
@Override
public List<String> getTabComplete(final CommandSource sender, final String[] args) {
return Collections.emptyList();
}
}The second type parameter names the parent command. Subcommands are attached to that parent automatically as each component is initialized, and the parent's Brigadier node tree is built once every subcommand has attached:
@Singleton
public class AdminSubCommand extends BaseSubCommand<CorePlugin, AccountCommand, Player> {
public AdminSubCommand() {
super("admin", "Toggle Admin Mode", Collections.emptyList(), "core.commands.account.admin");
}
@Override
public void execute(final Player player, final String[] args) {
this.getParent().getParent().getAccountByPlayer(player).ifPresent(account -> {
if (account.isAdministrating()) {
account.setAdministrating(false);
UtilMessage.message(player, "Account", UtilString.pair("Admin Mode", "<red>Disabled</red>"));
} else {
account.setAdministrating(true);
UtilMessage.message(player, "Account", UtilString.pair("Admin Mode", "<green>Enabled</green>"));
}
});
}
@Override
public List<String> getTabComplete(final Player player, final String[] args) {
return Collections.emptyList();
}
}This registers /account admin automatically — the parent AccountCommand routes the admin argument to AccountAdminSubCommand with the remaining args.
/account admin
│
├─ Sender type validation (Player)
├─ Permission check (core.commands.account.admin)
├─ CommandExecuteEvent (cancellable)
└─ AccountAdminSubCommand.execute(player, new String[0])
Use UtilEvent for event dispatch. Velocity's event bus is uniformly asynchronous-capable, so
there is no synchronous/asynchronous distinction — dispatch fires and forgets,
while supply fires and blocks until all handlers finish, returning the event for inspection:
// Fire and forget
UtilEvent.dispatch(new MyEvent());
// Fire and inspect after all handlers run
MyEvent event = UtilEvent.supply(new MyEvent());
if (event.isCancelled()) {
return;
}Use UtilTask for scheduling on Velocity's scheduler. Velocity runs all scheduled tasks on a
cached thread pool, so there is no main-thread concept — execute runs inline on the calling
thread, everything else schedules onto the pool:
// Execute inline on the calling thread
UtilTask.execute(() -> {
// immediate work
});
// Schedule onto the proxy's thread pool
UtilTask.executeAsynchronous(() -> {
// background work or I/O
});
// Delayed task
UtilTask.executeLaterAsynchronous(() -> {
player.sendMessage(Component.text("5 seconds later"));
}, 5, ChronoUnit.SECONDS);
// Repeating task with cancellation
UtilTask.scheduleAsynchronous(() -> {
// periodic work
}, 0, 5, ChronoUnit.SECONDS, () -> !player.isActive());Use UtilMessage for MiniMessage-formatted messaging with configurable prefixes:
// Prefixed message to an audience (player or console)
UtilMessage.message(player, "Network", "You connected to <aqua>%s</aqua>.".formatted(serverName));
// Prefixed message with MiniMessage tags
UtilMessage.message(player, "Shop", "<gold>+50 coins</gold> from daily reward!");
// Message a collection of players with an ignore list
UtilMessage.message(playerList, "Punish", "<yellow>%s</yellow> banned <yellow>%s</yellow>.".formatted(sender.getUsername(), target.getUsername()), List.of(targetUuid));
// Broadcast to all online players
UtilMessage.broadcast("Network", "<red><bold>Restarting</bold></red> in <yellow>5 minutes</yellow>.");
// Broadcast with ignore list
UtilMessage.broadcast("Alert", "<red>Maintenance mode enabled!</red>", List.of(excludedPlayerUuid));
// Log to console
UtilMessage.log("Core", "Plugin loaded successfully!");Velocity events are plain objects — they do not extend a base class. This framework provides
two base classes so custom events integrate with UtilEvent and gain optional cancellation:
- Extend
CustomEventfor events that merely notify listeners and cannot be denied. - Extend
CustomCancellableEventfor events that support allow/deny cancellation.
@AllArgsConstructor
@Getter
public class NetworkJoinEvent extends CustomCancellableEvent {
private final Player player;
}Listen for it on any component implementing Listener:
@Singleton
public class NetworkListener implements Listener {
@Subscribe(order = EventPriority.HIGH)
public void onNetworkJoin(final NetworkJoinEvent event) {
if (someCondition) {
event.setCancelled(true);
}
}
}Fire it and inspect the result:
NetworkJoinEvent event = UtilEvent.supply(new NetworkJoinEvent(player));
if (event.isCancelled()) {
return;
}Note:
CustomCancellableEventadapts Velocity'sResultedEvent<GenericResult>to a boolean cancelled flag — a denied result is treated as cancelled. Cancellation is only observed by callers that useUtilEvent.supply(which blocks for handlers); adispatchfire-and-forget cannot observe cancellation.
EventPriority maps Bukkit-style priority names onto Velocity's PostOrder model. Note that
Velocity orders descending by value — a higher value runs earlier — so these constants are
assigned the opposite numeric values you would expect coming from Bukkit, while preserving the
familiar execution order:
| Constant | Executes | Notes |
|---|---|---|
BASELINE |
First | Observation/setup only — never modify the event |
LOWEST |
Early | |
LOW |
Early | |
NORMAL |
Default | Standard handler logic |
HIGH |
Late | Validation and filtering |
HIGHEST |
Near-last | Final say on cancellation |
MONITOR |
Last | Monitoring/logging only — never modify the event |
| Utility | Description |
|---|---|
UtilEvent |
Fire-and-forget and blocking, result-returning event dispatch |
UtilTask |
Task scheduling — immediate, scheduled, and repeating with ChronoUnit-to-Duration conversion |
UtilMessage |
MiniMessage-based messaging with configurable prefixes, broadcasting, filtering, and ignore lists |
UtilPlugin |
Plugin lookup — internal by name or class |
UtilSearch |
Audience-aware single-match search over a collection |
| Type | Sender | Use Case |
|---|---|---|
BaseCommand<Plugin, Manager, CommandSource> |
CommandSource |
Any sender |
BaseCommand<Plugin, Manager, Player> |
Player |
Player-only commands |
| SubCommand Type | Sender | Use Case |
|---|---|---|
BaseSubCommand<Plugin, Command, CommandSource> |
CommandSource |
Any sender |
BaseSubCommand<Plugin, Command, Player> |
Player |
Player-only subcommands |
| Event Type | Description |
|---|---|
CustomEvent |
Base non-cancellable framework event |
CustomCancellableEvent |
Framework event with allow/deny cancellation |
| Event | Fired When |
|---|---|
CommandExecuteEvent |
Any command or subcommand is about to execute |
CommandTabCompleteEvent |
Any command or subcommand tab completion is requested |
All command events are cancellable. Cancelling an execute event prevents execution; cancelling a tab complete event returns an empty list.
| Event | Fired When |
|---|---|
PluginInitializeEvent |
A plugin has completed hierarchy initialization |
PluginShutdownEvent |
A plugin is about to begin hierarchy teardown |
DefaultSuggestions provides reusable, case-insensitively filtered tab-complete providers:
| Provider | Suggests |
|---|---|
CUSTOM |
A provided list, filtered by the current argument |
SUB_COMMANDS |
Subcommand labels the sender is permitted to use |
PLAYERS |
Online players matching a predicate |
ALL_PLAYERS |
All online players |
INTERNAL_PLUGINS |
All registered framework plugins |
| Interface | Description |
|---|---|
Plugin |
Hierarchy root with automatic registration callbacks (provided by Hierarchy-Framework; VelocityPlugin implements it) |
Node |
Typed parent access for commands and subcommands (provided by Hierarchy-Framework) |
Listener |
Marker for components auto-registered with the proxy's EventManager |
Event |
Marker for all framework events |
SharedBaseCommand |
Shared contract between commands and subcommands — sender validation, permission, execution, and tab-complete |
IBaseCommand |
Command contract with subcommand management |
ICustomCancellableEvent |
Cancellable event adapting Velocity's ResultedEvent to a boolean flag |