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
| Modul | Was es ist |
|---|---|
nexusapi-api | die öffentliche Oberfläche — nur Interfaces + unveränderliche Typen |
nexusapi-platform-common | die neutrale Engine (Config, Nachrichten, Befehle, Menüs, Items, Scheduler, Bridge-Dispatch) |
nexusapi-platform-paper | Paper/Spigot-Adapter + das installierbare NexusAPI-Plugin |
nexusapi-platform-bungee | BungeeCord-Adapter |
nexusapi-platform-velocity | Velocity-Adapter |
nexusapi-annotations | API-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-apiundnexusapi-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);
| Topologie | Einstiegspunkt | Ressourcen-Quelle |
|---|---|---|
| Paper / einzelner Server | NexusPaper.of(plugin) | plugin::getResource |
| BungeeCord-Proxy | NexusBungee.of(plugin) | plugin::getResourceAsStream |
| Velocity-Proxy | NexusVelocity.of(...) | Plugin-Classloader |
| Standalone / eingebettet / Tests | Nexus.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:
| Annotation | Bedeutung |
|---|---|
@Stable | Teil des SemVer-Vertrags — bricht nur mit einem Major-Release |
@Experimental | kann sich in Minor-Releases noch ändern; Feedback erwünscht |
@Internal | Implementierungsdetail 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(Scopeprovided). Implementierungsmodule niemals pinnen oder shaden — die Klassen liefert das installierte NexusAPI-Plugin. - Räume in
onDisableauf: eigene Tasks canceln undapi.scheduler().shutdown()aufrufen; Bridge-Subscriptions mitunsubscribe()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 überexceptionally/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