Minecraft-API (NexusAPI)

Die JuByte NexusAPI ist das Entwickler-SDK des JuByteNexus-Ökosystems — eine kleine, klassische, multi-plattform API, die dir das Boilerplate beim Plugin-Schreiben abnimmt: Config, Nachrichten, Befehle, Menüs, Items, ein Scheduler und die Panel-Bridge. Dein Code läuft identisch auf einem einzelnen Paper-Server, hinter BungeeCord oder Velocity und standalone — ganz ohne Proxy und ohne Panel.

  • groupId / Version: com.jubyte.nexusapi : 2.0.0
  • Stil: klassisch und explizit (typisierte Getter, fluente Builder) — keine Annotationen, keine Reflection.
  • Design-Grundsatz: die fehleranfällige Logik ist plattformneutral und unit-getestet; jeder Plattform-Adapter ist nur eine dünne, wohlbekannte Anbindung.

Module

ModulWas es ist
nexusapi-apidie öffentliche Oberfläche — nur Interfaces + unveränderliche Typen
nexusapi-platform-commondie neutrale Engine (Config, Nachrichten, Befehle, Menüs, Items, Scheduler, Bridge-Dispatch)
nexusapi-platform-paperPaper/Spigot-Adapter + das installierbare NexusAPI-Plugin
nexusapi-platform-bungeeBungeeCord-Adapter
nexusapi-platform-velocityVelocity-Adapter
nexusapi-annotationsAPI-Stabilitätsmarker (@Stable/@Experimental/@Internal)

Dein Plugin kompiliert ausschließlich gegen nexusapi-api — dort finden sich nur Interfaces und unveränderliche DTOs. Es leaken keine Plattform-Typen durch öffentliche Signaturen; Spieler/Sender, Scheduler und Logger sind API-Interfaces. So bleibt derselbe Code auf allen Plattformen gültig, und SemVer auf der API bleibt ehrlich.

Installation

Installiere das NexusAPI-Plugin einmal auf dem Server, dann hängst du dein Plugin per Dependency daran.

1. Maven-Dependency (Scope provided — die Klassen liefert zur Laufzeit das installierte Plugin):

<dependency>
    <groupId>com.jubyte.nexusapi</groupId>
    <artifactId>nexusapi-api</artifactId>
    <version>2.0.0</version>
    <scope>provided</scope>
</dependency>

2. Abhängigkeit deklarieren — Paper plugin.yml: depend: [NexusAPI] (oder softdepend, wenn optional):

name: NexusAPIExample
version: '2.0.0'
main: com.jubyte.nexusapi.example.ExamplePlugin
api-version: '1.21'
depend: [NexusAPI]
# No 'commands:' block needed — commands are registered dynamically by the API.

Hinweis zum Build: nexusapi-annotations, nexusapi-api und nexusapi-platform-common (der neutrale Kern) bauen und testen überall. Die Paper-/Bungee-/Velocity-Adapter benötigen ihre jeweiligen Server-APIs (paper-api, bungeecord-api, velocity-api).

Quickstart pro Plattform

Jeder Einstiegspunkt liefert dieselbe NexusApi, gescoped auf den Datenordner und die Ressourcen deines Plugins.

// Paper / single server
NexusApi api = NexusPaper.of(this);                          // in onEnable

// BungeeCord
NexusApi api = NexusBungee.of(this);                         // in onEnable

// Velocity (constructor injection)
NexusApi api = NexusVelocity.of(this, proxy, dataDir, logger);

// Standalone / embedded / tests — no server types needed
NexusApi api = Nexus.create(dataFolder, name -> getClass().getResourceAsStream("/" + name), logger);
TopologieEinstiegspunktRessourcen-Quelle
Paper / einzelner ServerNexusPaper.of(plugin)plugin::getResource
BungeeCord-ProxyNexusBungee.of(plugin)plugin::getResourceAsStream
Velocity-ProxyNexusVelocity.of(...)Plugin-Classloader
Standalone / eingebettet / TestsNexus.create(...)vom Aufrufer geliefert

Sowohl eine fehlende Bridge als auch ein null-Ressourcen-Provider werden toleriert — derselbe Code läuft unverändert auf einem blanken Server. Die Standalone-Fabrik Nexus.create(...) ist zugleich dein Werkzeug für Unit-Tests: kein Server, kein Mock-Framework nötig.

Config — klassische typisierte Getter

config.yml wird beim ersten Zugriff aus deiner mitgelieferten Ressource erzeugt. Der Zugriff ist der vertraute Bukkit-Stil, plus ein generisches getEnum, das das übliche resolveSound/resolveMaterial-Boilerplate ersetzt.

Config config = api.config().file("config.yml");
int min       = config.getInt("MinItems", 5);
boolean force = config.getBoolean("ForceCloseChestAfterWin");
List<String> worlds = config.getStringList("Worlds");
Sound sound   = config.getEnum("ChestSound", Sound.class, Sound.UI_BUTTON_CLICK);   // invalid -> warn + fallback
Config section = config.section("CaseParticles").orElseThrow();

config.set("MinItems", 7);
config.save();      // atomic, UTF-8
config.reload();

Lesezugriffe sind null-sicher und fallen auf deinen Default zurück; eine kaputte Datei schlägt laut fehl (niemals ein stillschweigend fabriziertes leeres Objekt). Bei einem ungültigen Enum-Wert wird einmal gewarnt und der deklarierte Fallback verwendet. Das Speichern erfolgt atomar (Temp-Datei + Move) und immer in UTF-8 — kein Mojibake durch Plattform-Charsets.

Nachrichten / i18n

Messages msg = api.messages().bundle("messages.yml");
String line  = msg.get("Welcome", Map.of("player", "Steve"));   // {prefix} + {player} + & / #RRGGBB colours
List<String> help = msg.getList("Help");

Benannte Platzhalter ({player}), das globale {prefix} sowie &- und #RRGGBB-Farbcodes werden in einem Aufruf aufgelöst. Ein fehlender Schlüssel liefert ein lautes, sichtbares missing: <key> statt eines stummen Leerstrings — Tippfehler in messages.yml fallen sofort auf.

Sprachen pro Spieler

messages.yml ist das Default-/Fallback-Bundle. Varianten namens messages_<lang>.yml (im Jar mitgeliefert oder in den Datenordner gelegt) werden pro Schlüssel von den Locale-Gettern aufgelöst — Teilübersetzungen fallen auf die Basisdatei zurück, und die Abfrage einer unbekannten Sprache legt niemals Dateien an:

Messages msg = api.messages().bundle("messages.yml");
sender.send(msg.get(sender.locale().orElse(null), "Welcome", Map.of("player", name)));
// Engine texts (no-permission, usage, …) localize the same way:
api.commands().messages(sender -> buildCommandMessagesFor(sender.locale().orElse(null)));

Sender.locale() trägt die Client-Sprache auf jeder Plattform (Paper, BungeeCord, Velocity); die Konsole hat keine und erhält das Default-Bundle.

Befehle — fluent, ohne Subcommand-Klassen

Das Framework übernimmt Permissions, das Player-only-Flag, minArgs → generierte Usage, einen „did you mean“-Vorschlag bei Tippfehlern sowie Tab-Completion (nur die Subcommands, die der Sender benutzen darf).

api.commands().command("case")
    .aliases("cases")
    .permission("case.use")
    .sub("add", s -> s
        .permission("case.admin")
        .usage("<player> <case> <amount>")
        .minArgs(3)
        .completer(ctx -> onlinePlayerNames())          // framework filters by prefix
        .executes(ctx -> {
            String player = ctx.args().get(0).orElseThrow();   // safe: minArgs(3) enforced
            int amount    = ctx.args().getInt(2, 1);           // typed, bounds-safe
            ctx.sender().send("§aGave " + amount + " to " + player);
        }))
    .sub("reload", s -> s.permission("case.admin").executes(ctx -> reload()))
    .executes(ctx -> ctx.sender().send("§e/case <add|reload>"))   // no-subcommand fallback
    .register();

Die typsichere Arguments-Hilfe (get, getInt mit Default, bounds-safe) ersetzt args[0], Integer.parseInt und manuelle Index-Prüfungen — dieselbe Philosophie wie bei den Config-Gettern. Ein commands:-Block in der plugin.yml ist nicht nötig; die API registriert Befehle dynamisch.

Menüs — config-getrieben mit Pagination (Paper)

Slots, Größe und die Content-Region kommen aus einer Config-Section (einmal validiert). Das Framework besitzt das Inventar, den Click-Listener, die Pagination pro Betrachter und eingebaute next/prev/close-Buttons.

Menu.fromConfig(api.config().file("menus/cases.yml"))
    .paginate(repo.all(),
        c -> PaperItems.toItemStack(c.icon()),               // render each entry
        (c, click) -> openCase(click.player(), c))           // click an entry
    .button("next", nextIcon)
    .button("prev", prevIcon)
    .button("close", closeIcon)
    .open(player);

Das Layout definierst du deklarativ in YAML — Titel, Zeilenzahl, Button-Slots und die Slots für den paginierten Inhalt:

Menu:
  Title: "&8Online Players"
  Rows: 6
  Buttons:
    prev: 45
    next: 53
    close: 49
  ContentSlots: [10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34]

Alternativ baust du ein MenuLayout direkt aus einer Section und öffnest es mit Menu.of(layout):

MenuLayout layout = MenuLayout.fromConfig(config.section("Menu").orElseThrow());
Menu.of(layout)
    .button("close", PaperItems.toItemStack(ItemSpec.builder("BARRIER").name("&cClose").build()))
    .paginate(online,
        player -> PaperItems.toItemStack(ItemSpec.builder("PLAYER_HEAD")
            .name("&b" + player.getName())
            .headOwner(player.getName())
            .build()),
        (player, click) -> click.player().sendMessage("§7You clicked §b" + player.getName()))
    .open(viewer);

Items / Köpfe

ItemSpec spec = ItemSpec.fromConfig(section).withPlaceholders(Map.of("owner", name));
ItemStack item = PaperItems.toItemStack(spec);   // name/lore/glow/model-data/flags/heads

ItemSpec ist die unveränderliche, plattformneutrale Beschreibung eines Items (Name, Lore, Glow, Model-Data, Flags, Köpfe) — aus der Config geparst oder per ItemSpec.builder(...) gebaut; PaperItems.toItemStack(spec) erzeugt daraus den Bukkit-ItemStack.

Spielerköpfe nutzen Papers offizielle Profil-API (per Besitzer oder Base64-Textur) — keine Reflection. withPlaceholders ersetzt Platzhalter in Name, Lore und im Kopf-Besitzer/der Textur, sodass eine Config Head: { Owner: "{player}" } schreiben kann und der Skin des Zielspielers gerendert wird.

Scheduler — mit verwaltetem Lebenszyklus

NexusScheduler scheduler = api.scheduler();            // Paper: platform scheduler; standalone: thread-based
Task task = scheduler.runTimer(() -> tick(), 0, 20);   // every second on the main thread
// onDisable:
scheduler.shutdown();                                  // cancels everything it owns

api.scheduler() liefert automatisch die richtige Implementierung: Auf Paper erkennt er Folia und nutzt dessen Region-/Async-Scheduler (sonst den Bukkit-Scheduler); standalone ist es ein Thread-basierter Scheduler. Dein Code ist in beiden Fällen identisch. Jeder Task ist ein abbrechbares Task-Handle, und shutdown() in onDisable cancelt alles, was die API besitzt — Task-Leaks nach dem Deaktivieren sind strukturell ausgeschlossen.

Bridge — Verbindung zum Panel

api.bridge() ist immer sicher aufrufbar: live, wenn das Bridge-Plugin verbunden ist, sonst eine No-op-Instanz im getrennten Zustand (Sends werden verworfen, request schlägt kontrolliert fehl — niemals ein Crash auf einem Standalone-Server).

NexusBridge bridge = api.bridge();
if (bridge.isConnected()) {
    bridge.send("shop.delivery", Map.of("gameId", uuid.toString(), "item", "vip"));
}
Subscription sub = bridge.subscribe("punishment", m ->
        getLogger().info(m.getString("type").orElse("?") + " -> " + m.getString("gameId").orElse("?")));
// later: sub.unsubscribe();

// Typed read-only queries (futures; fail fast when disconnected):
bridge.queries().isOnline(uuid).thenAccept(online -> ...);
bridge.queries().activePunishment(uuid).thenAccept(opt -> opt.ifPresent(p -> ...));

Ist das NexusAPI-Plugin zusammen mit dem JuByteNexus-Bridge-Plugin installiert, leitet die Bridge ihre Events (connected, punishment, message, broadcast, …) automatisch an deine Subscriber weiter. subscribe(...) liefert eine Subscription mit unsubscribe(); mehrere Plugins können dieselben Events beobachten (Fan-out).

Ein Muster für saubere Degradation, wenn das Panel offline ist:

api.bridge().queries().networkPlayerCount()
    .thenAccept(count -> ctx.sender().send("§7Network players: §b" + count))
    .exceptionally(error -> {
        ctx.sender().send("§7Bridge offline — local only: §b" + Bukkit.getOnlinePlayers().size());
        return null;
    });

API-Stabilität

Jeder öffentliche Typ trägt einen Stabilitätsmarker aus nexusapi-annotations:

AnnotationBedeutung
@StableTeil des SemVer-Vertrags — bricht nur mit einem Major-Release
@Experimentalkann sich in Minor-Releases noch ändern; Feedback erwünscht
@InternalImplementierungsdetail in ...internal.*-Paketen — niemals benutzen

Auf nexusapi-api und nexusapi-annotations gilt striktes SemVer (MAJOR = Bruch, MINOR = additiv, PATCH = Fix) — das sind die einzigen Artefakte, gegen die Dritte kompilieren. @Internal-Typen sind vom Javadoc und vom Kompatibilitäts-Gate ausgenommen; verlasse dich nicht auf sie.

Best Practices

  • Kompiliere nur gegen nexusapi-api (Scope provided). Implementierungsmodule niemals pinnen oder shaden — die Klassen liefert das installierte NexusAPI-Plugin.
  • Räume in onDisable auf: eigene Tasks canceln und api.scheduler().shutdown() aufrufen; Bridge-Subscriptions mit unsubscribe() lösen, wenn du sie nicht mehr brauchst.
  • Blockiere nie den Main-Thread: Bridge-Queries liefern CompletableFuture — verarbeite Ergebnisse asynchron und behandle den getrennten Zustand über exceptionally/isConnected().
  • Schreibe standalone-fähig: behandle die Bridge als optional. Die No-op-Bridge garantiert, dass dein Plugin ohne Panel und Proxy vollständig funktioniert; Features, die das Panel brauchen, sollten sich sauber degradieren statt zu crashen.
  • Verlasse dich auf lautes Scheitern: kaputtes YAML, fehlende Message-Keys und ungültige Enum-Werte machen sich sichtbar bemerkbar. Verstecke diese Signale nicht — sie sind während der Entwicklung deine Freunde.
  • Teste gegen den neutralen Kern: Nexus.create(dataFolder, resourceProvider, logger) erstellt eine voll funktionsfähige API ohne jeden Server — ideal für JUnit-Tests von Config-, Message-, Command- und Menü-Logik. Die Engine selbst ist bereits umfassend unit-getestet.
  • Baseline: Java 21, Paper 1.21.4, api-version: '1.21'. Kein NMS, kein Versions-String-Sniffing.
  • Ein Referenz-Plugin findest du im Modul nexusapi-example — es nutzt jeden Teil der API genau so, wie es ein echtes Plugin täte, und wird ausschließlich gegen die öffentliche API gebaut.

Siehe auch: Minecraft-Bridge · Modul-SDK · API-Vertrag v1