Skip to content

oraxen/hopper

Repository files navigation

Hopper

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.

Features

  • 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

Installation

Gradle (Kotlin DSL)

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")
}

Gradle (Groovy)

repositories {
    maven { url 'https://repo.oraxen.com/releases' }
}

dependencies {
    implementation 'md.thomas.hopper:hopper-bukkit:1.4.0'
}

Maven

<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>

Quick Start

Bukkit/Spigot Plugin

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
    }
}

Auto-Load (No Restart Required)

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 Plugin (with Bootstrap)

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 Sources

Hangar (PaperMC)

Dependency.hangar("ProtocolLib")
    .minVersion("5.0.0")
    .minecraftVersion("1.21")  // Filter by MC version
    .build()

Modrinth

Dependency.modrinth("packetevents")
    .version("2.11.0")
    .minecraftVersion("1.21")
    .build()

SpigotMC (via Spiget)

Dependency.spiget(1997)  // ProtocolLib resource ID
    .minVersion("5.0.0")
    .build()

GitHub Releases

Dependency.github("dmulloy2/ProtocolLib")
    .minVersion("5.0.0")
    .assetPattern("ProtocolLib.jar")  // Match release asset
    .build()

Direct URL

Dependency.url("https://example.com/plugin.jar")
    .sha256("abc123...")  // Optional checksum
    .fileName("my-plugin.jar")
    .build()

Version Constraints

Exact Version

.version("5.4.0")

Minimum Version

.minVersion("5.0.0")  // >= 5.0.0, prefer latest

Version Range

.versionRange(">=5.0.0 <6.0.0")  // 5.x only

Latest

.latest()  // Always newest

Update Policies

Control 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

Failure Policies

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

Log Levels

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)

Platform Detection

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

Minecraft Version Filtering

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()).

Multi-Plugin Coordination

When multiple plugins shade Hopper, they automatically coordinate:

  1. Shared Lockfile: All plugins share plugins/.hopper/hopper.lock
  2. Constraint Merging: If Plugin A needs >=5.0.0 and Plugin B needs >=5.2.0, the merged constraint is >=5.2.0
  3. Single Download: Dependencies are downloaded once and shared

Conflict Resolution

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)

Files Created

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

Non-Standard Version Support

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]

API Reference

Core Classes

  • Hopper - Main entry point with static registration
  • Dependency - Builder for dependencies
  • DependencyCollector - Collects dependencies during registration
  • DownloadResult - Result of download operation
  • LogLevel - Output verbosity control (VERBOSE, NORMAL, QUIET, SILENT)
  • Platform - Server platform detection (Folia, Paper, Spigot, etc.)
  • MinecraftVersion - Global Minecraft version configuration

Version Classes

  • Version - Flexible version parser
  • VersionConstraint - Version constraints (exact, range, etc.)
  • UpdatePolicy - How to handle updates

Platform Modules

  • BukkitHopper - Bukkit/Spigot convenience wrapper
  • BukkitHopper.DownloadAndLoadResult - Combined download + auto-load result
  • PluginLoader - Runtime plugin loading utility
  • HopperBootstrap - Paper bootstrap support

License

MIT License - see LICENSE file.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages