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!
- 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.1.2")
// Bukkit/Spigot
implementation("md.thomas.hopper:hopper-bukkit:1.1.2")
// Paper (with bootstrap support)
implementation("md.thomas.hopper:hopper-paper:1.1.2")
}
// 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.1.2'
}<repository>
<id>oraxen</id>
<url>https://repo.oraxen.com/releases</url>
</repository>
<dependency>
<groupId>md.thomas.hopper</groupId>
<artifactId>hopper-bukkit</artifactId>
<version>1.1.2</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("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 |
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 operation
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.