Plugin Dependency Loader for Minecraft Servers
Hopper downloads plugin dependencies from Hangar, Modrinth, SpigotMC, and GitHub Releases at runtime so you don't need to shade them.
- Multiple Sources: Download from Hangar, Modrinth, Spiget (SpigotMC), GitHub Releases, or direct URLs
- Smart Version Resolution: Supports exact versions, minimum versions, ranges, and update policies
- Auto-Load Support: Automatically load downloaded plugins at runtime—no server restart required!
- Platform-Aware: Auto-detects Folia, Paper, Purpur, Spigot, Bukkit, Velocity, BungeeCord, and Waterfall
- Auto Minecraft Version Detection: Automatically filters dependencies for your server's Minecraft version
- Configurable Logging: Control output verbosity (VERBOSE, NORMAL, QUIET, SILENT)
- Non-Standard Versions: Handles formats like
R4.0.9,5.4.0-SNAPSHOT,build-123,2024.12.20 - Multi-Plugin Coordination: Multiple plugins can shade Hopper and share the same downloaded dependencies
- Lockfile Support: Reproducible builds with
hopper.lock - Minimal Dependencies: Uses only JDK built-ins (no Gson, no external HTTP libraries)
- Shade-Friendly: Single package for easy relocation
repositories {
maven("https://repo.oraxen.com/releases")
}
dependencies {
// Core (platform-agnostic)
implementation("md.thomas.hopper:hopper-core:1.4.0")
// Bukkit/Spigot
implementation("md.thomas.hopper:hopper-bukkit:1.4.0")
// Paper (with bootstrap support)
implementation("md.thomas.hopper:hopper-paper:1.4.0")
}
// Shade and relocate
tasks.shadowJar {
relocate("md.thomas.hopper", "your.package.hopper")
}repositories {
maven { url 'https://repo.oraxen.com/releases' }
}
dependencies {
implementation 'md.thomas.hopper:hopper-bukkit:1.4.0'
}<repository>
<id>oraxen</id>
<url>https://repo.oraxen.com/releases</url>
</repository>
<dependency>
<groupId>md.thomas.hopper</groupId>
<artifactId>hopper-bukkit</artifactId>
<version>1.4.0</version>
</dependency>public class MyPlugin extends JavaPlugin {
// Step 1: Register dependencies in constructor
public MyPlugin() {
BukkitHopper.register(this, deps -> {
deps.require(Dependency.hangar("ProtocolLib")
.minVersion("5.0.0")
.updatePolicy(UpdatePolicy.MINOR)
.build());
deps.require(Dependency.modrinth("packetevents")
.version("2.11.0")
// minecraftVersion auto-detected, but can override:
// .minecraftVersion("1.21")
.onFailure(FailurePolicy.WARN_SKIP) // Optional dependency
.build());
});
}
// Step 2: Download in onLoad (before onEnable)
@Override
public void onLoad() {
DownloadResult result = BukkitHopper.download(this);
BukkitHopper.logResult(this, result);
if (result.requiresRestart()) {
getLogger().severe("New dependencies downloaded. Please restart!");
}
}
// Step 3: Check readiness in onEnable
@Override
public void onEnable() {
if (!BukkitHopper.isReady(this)) {
getLogger().severe("Dependencies not loaded. Disabling.");
getServer().getPluginManager().disablePlugin(this);
return;
}
// Safe to use ProtocolLib and PacketEvents here
}
}Use downloadAndLoad() to automatically load downloaded plugins at runtime:
public class MyPlugin extends JavaPlugin {
public MyPlugin() {
BukkitHopper.register(this, deps -> {
deps.require(Dependency.hangar("ProtocolLib")
.minVersion("5.0.0")
.build());
});
}
@Override
public void onLoad() {
// Download AND auto-load in one step
var result = BukkitHopper.downloadAndLoad(this);
if (result.isFullySuccessful()) {
getLogger().info("All dependencies ready!");
} else if (!result.loadResult().isSuccess()) {
getLogger().warning("Some plugins couldn't be auto-loaded.");
}
}
@Override
public void onEnable() {
// Dependencies are available immediately!
}
}Note: While most plugins can be hot-loaded, some plugins with complex initialization may still require a restart. The loadResult will indicate which plugins were successfully loaded.
Paper 1.19.4+ supports early loading via PluginLoader, which runs before your plugin class is even loaded.
paper-plugin.yml:
name: MyPlugin
version: 1.0.0
main: com.example.myplugin.MyPlugin
loader: com.example.myplugin.MyPluginBootstrap
api-version: "1.19"MyPluginBootstrap.java:
public class MyPluginBootstrap implements PluginLoader {
@Override
public void classloader(PluginClasspathBuilder builder) {
DownloadResult result = HopperBootstrap.create(builder.getContext())
.require(Dependency.hangar("ProtocolLib").minVersion("5.0.0").build())
.require(Dependency.modrinth("packetevents").version("2.11.0").build())
.download();
if (result.requiresRestart()) {
throw new RuntimeException(
"Dependencies downloaded: " + result.downloaded() + ". Restart required!");
}
}
}MyPlugin.java:
public class MyPlugin extends JavaPlugin {
@Override
public void onEnable() {
// Dependencies are guaranteed present!
}
}Dependency.hangar("ProtocolLib")
.minVersion("5.0.0")
.minecraftVersion("1.21") // Filter by MC version
.build()Dependency.modrinth("packetevents")
.version("2.11.0")
.minecraftVersion("1.21")
.build()Dependency.spiget(1997) // ProtocolLib resource ID
.minVersion("5.0.0")
.build()Dependency.github("dmulloy2/ProtocolLib")
.minVersion("5.0.0")
.assetPattern("ProtocolLib.jar") // Match release asset
.build()Dependency.url("https://example.com/plugin.jar")
.sha256("abc123...") // Optional checksum
.fileName("my-plugin.jar")
.build().version("5.4.0").minVersion("5.0.0") // >= 5.0.0, prefer latest.versionRange(">=5.0.0 <6.0.0") // 5.x only.latest() // Always newestControl how aggressively Hopper updates dependencies:
.version("5.4.0")
.updatePolicy(UpdatePolicy.PATCH) // Only 5.4.X updates| Policy | Allowed Updates | Example |
|---|---|---|
NONE |
Exact version only | 5.4.0 only |
PATCH |
Patch updates | 5.4.0 → 5.4.1, 5.4.2 |
MINOR |
Minor + patch | 5.4.0 → 5.5.0, 5.9.0 |
MAJOR |
Any newer | 5.4.0 → 6.0.0, 7.0.0 |
Handle failures per-dependency:
.onFailure(FailurePolicy.WARN_SKIP) // Optional dependency| Policy | Behavior |
|---|---|
FAIL |
Throw exception (default) |
WARN_USE_LATEST |
Try latest available |
WARN_SKIP |
Skip this dependency |
Control output verbosity:
// Quiet mode - only show final result
DownloadResult result = BukkitHopper.download(this, LogLevel.QUIET);
// Or with downloadAndLoad
var result = BukkitHopper.downloadAndLoad(this, LogLevel.VERBOSE);| Level | Output |
|---|---|
VERBOSE |
All processing steps, version fetching, downloads, and summaries |
NORMAL |
Download progress and final loaded plugins summary (default) |
QUIET |
Only the final result (which plugins were loaded) |
SILENT |
No output at all |
Example output at each level:
VERBOSE:
[MyPlugin] [Hopper] Detected Minecraft version: 1.21.1
[MyPlugin] Processing 2 dependency(ies) for MyPlugin
[MyPlugin] Processing dependency: CommandAPI
[MyPlugin] Fetching versions from MODRINTH...
[MyPlugin] Selected version: 11.1.0
[MyPlugin] Downloading from: https://cdn.modrinth.com/...
[MyPlugin] [Hopper] Loaded: CommandAPI 11.1.0, PacketEvents 2.11.1
NORMAL:
[MyPlugin] [Hopper] Downloading CommandAPI 11.1.0...
[MyPlugin] [Hopper] Loaded: CommandAPI 11.1.0, PacketEvents 2.11.1
QUIET:
[MyPlugin] [Hopper] Loaded: CommandAPI 11.1.0, PacketEvents 2.11.1
SILENT:
(no output)
Hopper auto-detects your server platform and downloads platform-specific builds when available:
// Automatic detection (recommended)
Dependency.modrinth("packetevents").build() // Gets Paper build on Paper, Spigot on Spigot
// Force specific platform
Dependency.modrinth("packetevents")
.platform(Platform.PAPER)
.build()Supported platforms:
| Platform | Detection |
|---|---|
FOLIA |
Regionized multithreading classes |
PURPUR |
PurpurConfig class |
PAPER |
Paper configuration classes |
SPIGOT |
SpigotConfig class |
BUKKIT |
Fallback default |
VELOCITY |
Velocity proxy classes |
WATERFALL |
Waterfall configuration |
BUNGEECORD |
BungeeCord proxy classes |
Hopper automatically detects your server's Minecraft version and filters dependencies accordingly:
// Automatic - Hopper detects 1.21.1 from server
Dependency.modrinth("packetevents").build() // Only gets 1.21.1-compatible versions
// Manual override for specific dependency
Dependency.modrinth("old-plugin")
.minecraftVersion("1.19.4")
.build()
// Manual global override
BukkitHopper.setMinecraftVersion("1.20.4");The detection works automatically with both Paper (Bukkit.getMinecraftVersion()) and Spigot (parsing Bukkit.getBukkitVersion()).
When multiple plugins shade Hopper, they automatically coordinate:
- Shared Lockfile: All plugins share
plugins/.hopper/hopper.lock - Constraint Merging: If Plugin A needs
>=5.0.0and Plugin B needs>=5.2.0, the merged constraint is>=5.2.0 - Single Download: Dependencies are downloaded once and shared
If constraints are incompatible (e.g., A wants <5.0 and B wants >=5.0):
- Hopper logs a warning
- Uses the higher version (may break the older plugin)
plugins/
├── .hopper/
│ ├── hopper.lock # Resolved versions (shared)
│ ├── registry.json # Which plugin wants what
│ └── .coordination.lock # File lock for thread safety
├── ProtocolLib-5.4.0.jar # Downloaded dependencies
└── packetevents-2.11.0.jar
Hopper handles various version formats:
| Format | Example | Parsed As |
|---|---|---|
| Standard | 5.4.0 |
[5, 4, 0] |
| Prefixed | R4.0.9 |
prefix="R", [4, 0, 9] |
| Snapshot | 5.4.0-SNAPSHOT |
[5, 4, 0], qualifier="SNAPSHOT" |
| Build number | build-123 |
buildNumber=123 |
| Calendar | 2024.12.20 |
[2024, 12, 20] |
Hopper- Main entry point with static registrationDependency- Builder for dependenciesDependencyCollector- Collects dependencies during registrationDownloadResult- Result of download operationLogLevel- Output verbosity control (VERBOSE, NORMAL, QUIET, SILENT)Platform- Server platform detection (Folia, Paper, Spigot, etc.)MinecraftVersion- Global Minecraft version configuration
Version- Flexible version parserVersionConstraint- Version constraints (exact, range, etc.)UpdatePolicy- How to handle updates
BukkitHopper- Bukkit/Spigot convenience wrapperBukkitHopper.DownloadAndLoadResult- Combined download + auto-load resultPluginLoader- Runtime plugin loading utilityHopperBootstrap- Paper bootstrap support
MIT License - see LICENSE file.