Zum Inhalt springen

· Tutorial

Einen eigenen Befehl für deinen Minecraft-Server erstellen

Konsole eines Minecraft-Servers mit einem eigenen Befehl und der zugehörigen plugin.yml

Ein eigener Befehl ist bei fast allen das erste Plugin. Du willst /kit pvp tippen und es soll etwas passieren. Der Java-Code dafür sind dreißig Zeilen. Das Problem ist nie Java, sondern die Kette darum herum, und vor allem eine Datei mit sechs Zeilen, die beim ersten Mal wirklich jeder vergisst.

Dieser Guide geht die Kette in genau der Reihenfolge durch, in der der Server sie abarbeitet: Deklaration, Executor, Argumente, Tab-Vervollständigung, Rechte, Ablehnungsnachricht. Am Ende steht die Diagnose-Tabelle, die ich am ersten Tag gebraucht hätte: was der Spieler sieht, verglichen mit dem, was die Konsole sagt, für jede Art von Fehler.

Der stille Fehler, einmal komplett erklärt

Das passiert, wenn ein Befehl nicht in plugin.yml deklariert ist: Das Plugin lädt normal, es erscheint grün in /plugins, die Konsole sagt nichts, wirklich gar nichts. Der Spieler tippt den Befehl, der Server antwortet mit seiner Vanilla-Meldung für unbekannte Befehle. Dein Code wurde nie aufgerufen, und nirgendwo steht ein Hinweis darauf.

Das ist der entscheidende Unterschied zu allen anderen Plugin-Fehlern. Eine falsche Java-Version wirft dir einen UnsupportedClassVersionError hin, eine fehlende Abhängigkeit einen NoClassDefFoundError. Ein nicht deklarierter Befehl wirft nichts. Anfänger schließen daraus, dass ihr Code kaputt ist, und schreiben ihn dreimal neu, obwohl er von Anfang an richtig war.

Der Reflex, der dir zwei Stunden spart

Bevor du Java anfasst: tippe /help und den Namen deines Befehls. Kennt der Server ihn nicht, liegt das Problem in plugin.yml, Punkt. Keine Zeile Java kann einen Befehl retten, den der Server nie registriert hat.

Die ganze Kette, in der Reihenfolge des Servers

Schickt ein Spieler einen Befehl, geht der Server immer die gleichen Schritte durch. Wer sie in der richtigen Reihenfolge kennt, weiß sofort, welches Glied gerissen ist.

  1. Registrierung. Beim Laden liest der Server den commands:-Block aus plugin.yml und erzeugt pro Eintrag einen Befehl. Steht hier nichts, existiert nichts.
  2. Auflösung. Der Server sucht den getippten Namen oder einen seiner Aliase.
  3. Rechteprüfung. Ist das Feld permission: deklariert und hat der Absender es nicht, zeigt der Server die Ablehnung und bricht ab. Dein Code läuft nicht.
  4. Aufruf des Executors. Deine Methode onCommand bekommt den Absender, den Befehl, das benutzte Label und das Array der Argumente.
  5. Rückgabewert. true heißt, du hast die Anfrage erledigt. false löst die Anzeige der usage:-Zeile aus.

Die Tab-Vervollständigung läuft auf einem parallelen Weg: sie feuert bei jedem Tastendruck, noch vor dem Absenden, und geht über eine andere Methode. Die API-Referenzen stehen auf docs.papermc.io, das Javadoc auf jd.papermc.io.

Schritt 1: den Befehl in plugin.yml deklarieren

Hier eine vollständige plugin.yml für einen Befehl /kit mit Alias, usage-Zeile, Recht und Ablehnungsnachricht.

plugin.yml
name: MinaxKit
version: 1.0.0
main: fun.minax.kit.MinaxKit
api-version: '1.21'
description: Verteilt Kits an Spieler.

commands:
  kit:
    description: Gibt einem Spieler ein Kit.
    usage: /kit <starter|pvp|builder> [Spieler]
    aliases: [k]
    permission: minaxkit.use
    permission-message: Du hast keinen Zugriff auf die Kits.

permissions:
  minaxkit.use:
    description: Erlaubt /kit fuer dich selbst.
    default: true
  minaxkit.other:
    description: Erlaubt /kit <name> <anderer Spieler>.
    default: op

Drei Details, die alles kaputt machen und die man mit dem bloßen Auge nicht sieht. Der Befehlsname ist ein YAML-Schlüssel, also zählt die Einrückung: zwei Leerzeichen unter commands:, niemals ein Tabulator. Die Datei muss direkt in src/main/resources liegen, nicht neben der Klasse. Und api-version muss ein String in Anführungszeichen sein, sonst liest YAML 1.21 als Dezimalzahl.

Die Zahl, die über deine api-version entscheidet

Die bStats-Messung vom Juli 2026 über 62.948 Server ergibt 1.21.11 mit 36,4 %, 26.1.2 mit 14,9 %, 26.2 mit 7,4 % und 1.21.4 mit 7,2 %. Die 1.21-Familie ist also klar in der Mehrheit. Eine zu neue api-version sorgt dafür, dass mehr als jeder dritte Server dein Plugin ablehnt. Ich deklariere die 1.21-Familie und kompiliere gegen Java 21: ein .jar mit Java 21 lädt auf einer neueren Java-Version, umgekehrt gilt das nicht.

Schritt 2: der Executor und die getCommand-Falle

Der Standard-Executor eines Befehls ist das Plugin selbst. Schreibst du onCommand also direkt in deine Hauptklasse, funktioniert es ohne jede Registrierung. Das ist bequem, und genau das macht den stillen Fehler so gemein: ohne Aufruf von getCommand prüft nie etwas, ob der Befehl auf Serverseite überhaupt existiert.

Meine Meinung, und sie ist eindeutig: registriere immer explizit, auch in der Hauptklasse, und fange das null ab. Du tauschst einen stummen Fehler gegen einen lauten, und das ist ein riesiger Fortschritt.

Java
package fun.minax.kit;

import org.bukkit.command.PluginCommand;
import org.bukkit.plugin.java.JavaPlugin;

public final class MinaxKit extends JavaPlugin {

    @Override
    public void onEnable() {
        KitCommand handler = new KitCommand();
        PluginCommand command = getCommand("kit");

        if (command == null) {
            getLogger().severe("Befehl 'kit' fehlt in plugin.yml, das Plugin schaltet sich ab.");
            getServer().getPluginManager().disablePlugin(this);
            return;
        }

        command.setExecutor(handler);
        command.setTabCompleter(handler);
        getLogger().info("Befehl /kit registriert.");
    }
}

Das getLogger().info in der letzten Zeile ist keine Deko. Es ist dein Beweis beim Start: siehst du die Zeile nicht in der Konsole, liegt das Problem vor dem Java-Code.

Schritt 3: die Argumente, ein Array ohne Garantien

Das Argument-Array enthält alles, was der Spieler nach dem Befehlsnamen getippt hat, an Leerzeichen aufgeteilt. Es kann leer sein. Es kann alles enthalten. Nichts wird für dich geprüft, und ein args[0] auf einem leeren Array wirft eine Exception, die der Spieler als allgemeine interne Fehlermeldung zu sehen bekommt.

Java
package fun.minax.kit;

import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.bukkit.util.StringUtil;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;

public final class KitCommand implements CommandExecutor, TabCompleter {

    private static final List<String> KITS = List.of("starter", "pvp", "builder");

    @Override
    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
        if (args.length == 0) {
            return false; // der Server zeigt die usage:-Zeile aus plugin.yml
        }

        String kit = args[0].toLowerCase(Locale.ROOT);
        if (!KITS.contains(kit)) {
            sender.sendMessage(Component.text("Unbekanntes Kit: " + args[0], NamedTextColor.RED));
            return true;
        }

        Player ziel;
        if (args.length >= 2) {
            if (!sender.hasPermission("minaxkit.other")) {
                sender.sendMessage(Component.text("Du kannst nur dir selbst ein Kit geben.", NamedTextColor.RED));
                return true;
            }
            ziel = Bukkit.getPlayerExact(args[1]);
            if (ziel == null) {
                sender.sendMessage(Component.text(args[1] + " ist nicht online.", NamedTextColor.RED));
                return true;
            }
        } else if (sender instanceof Player selbst) {
            ziel = selbst;
        } else {
            sender.sendMessage(Component.text("Aus der Konsole: /kit <name> <Spieler>", NamedTextColor.RED));
            return true;
        }

        ziel.sendMessage(Component.text("Kit " + kit + " erhalten.", NamedTextColor.GREEN));
        return true;
    }

    // Die Klasse geht in Schritt 4 weiter: TabCompleter verlangt onTabComplete.

Zwei Dinge fallen auf. Das return false am Anfang ist kein Eingeständnis einer Panne, es ist die eingebaute Hilfe, die deine usage:-Zeile zeigt. Und der Absender ist nicht zwingend ein Spieler: die Konsole kann den Befehl schicken, ein Befehlsblock ebenfalls. Die Prüfung instanceof Player ist nicht optional, ohne sie stürzt dein Befehl beim ersten Aufruf aus der Konsole ab.

Schritt 4: die Tab-Vervollständigung

Das ist der Teil, den alle überspringen, und genau daran erkennt man den Unterschied zwischen einem Hobby-Plugin und einem, das man gerne benutzt. Ein Spieler, der Tab drückt und nichts sieht, schließt daraus, dass es den Befehl nicht gibt.

Java
    @Override
    public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
        if (args.length == 1) {
            List<String> treffer = new ArrayList<>();
            StringUtil.copyPartialMatches(args[0], KITS, treffer);
            Collections.sort(treffer);
            return treffer;
        }

        if (args.length == 2 && sender.hasPermission("minaxkit.other")) {
            return null; // Fallback des Servers: Namen der eingeloggten Spieler
        }

        return Collections.emptyList();
    }
}

copyPartialMatches filtert deine Liste nach dem, was der Spieler schon getippt hat, ohne auf Groß- und Kleinschreibung zu achten. Der Punkt, auf den es wirklich ankommt: der Unterschied zwischen null und leerer Liste. Bei null greift der Fallback des Servers und vervollständigt mit den Namen der eingeloggten Spieler. Beim zweiten Argument ist das praktisch, überall sonst gibst du damit Informationen preis. Standard ist die leere Liste, null nur bewusst.

Schritt 5: Rechte und die Ablehnungsnachricht

Der unerwartete Teil: sobald du permission: in plugin.yml deklarierst, prüft der Server das Recht vor dem Aufruf deines Executors. Ein hasPermission auf denselben Knoten innerhalb von onCommand ist damit toter Code. Für Rechte auf Argument-Ebene bleibt es dagegen unverzichtbar, wie beim minaxkit.other aus dem Beispiel.

Das hat direkte Folgen für die Ablehnungsnachricht. Behältst du permission: in plugin.yml, ist dein Text der aus permission-message, als einfacher Text. Willst du eine ordentlich mit Adventure gefärbte oder klickbare Ablehnung, musst du permission: aus plugin.yml nehmen und selbst prüfen.

Java
    // Variante: Recht NICHT in plugin.yml deklariert, Ablehnung komplett selbst gesteuert
    @Override
    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
        if (!sender.hasPermission("minaxkit.use")) {
            sender.sendMessage(Component.text("Kits gibt es nur fuer Mitglieder.", NamedTextColor.RED));
            return true;
        }
        // ... weitere Verarbeitung
        return true;
    }

Meine Entscheidung: bei Admin-Befehlen behalte ich permission: in plugin.yml, weil der Server den Befehl dann in der Hilfe versteckt und so früh wie möglich blockt. Bei Befehlen für Spieler nehme ich es heraus, denn dort gehört die Ablehnungsnachricht zum Spielgefühl. Und ich deklariere immer den Block permissions: mit einem ausdrücklichen default:, sonst zeigt ein Rechtesystem wie LuckPerms deinen Knoten erst an, wenn ihn jemand von Hand vergeben hat.

Die Diagnose-Tabelle

So klar aufgeschrieben habe ich das nirgends gefunden, und damit findest du den Fehler in zehn Sekunden: was der Spieler sieht, gegen das, was die Konsole sagt.

Was der Spieler sieht Was die Konsole sagt
Fehlt in plugin.yml Unbekannter Befehl (Vanilla-Text) Absolut nichts
Deklariert, kein Executor Die usage:-Zeile Nichts
getCommand null ignoriert Unbekannter Befehl Exception beim Start, Plugin aus
Recht fehlt Die permission-message Nichts
onCommand gibt false zurück Die usage:-Zeile Nichts
Exception in onCommand Allgemeiner interner Fehler Kompletter Stacktrace mit deiner Klasse
Name schon belegt Der Befehl des anderen Plugins Konfliktzeile beim Laden

Das Präfix, das Konflikte klärt

Deklarieren zwei Plugins denselben Befehlsnamen, bedient der Server den einen und macht den anderen als pluginname:befehl erreichbar. Tippe /minaxkit:kit: klappt das, während /kit etwas völlig anderes tut, hast du einen Namenskonflikt und keinen Bug.

Was nicht funktioniert, und was du kaputt machst

Vier Fallen, die speziell Befehle treffen. Drei davon haben mit dem Befehlscode selbst nichts zu tun.

  • Die Ablehnungsnachricht bricht beim Update. Paper 26.x bringt Adventure 5 mit, und dort wurden APIs entfernt, die vorher nur veraltet markiert waren. Es trifft vor allem alles, was Text vor Spielern anzeigt. Ein onCommand, das für 1.21 mit alten Text-Nachrichten geschrieben wurde, kompiliert unter Umständen nicht mehr. Das ist die Ironie: der trivialste Teil deines Befehls ist am stärksten betroffen.
  • Ein Befehl, der eine Datenbank liest, blockiert den Server. onCommand läuft im Hauptthread. Ein synchroner Datenbankzugriff darin drückt die TPS für alle nach unten. Den JDBC-Treiber für SQLite bringt Paper schon mit, du musst also nichts mitliefern, aber das Lesen gehört in einen eigenen Thread und muss zurück in den Server-Thread, bevor es die Welt anfasst.
  • Auf Folia darf dein Befehl nicht von einem einzigen Thread ausgehen. Folia führt Weltregionen auf mehreren Threads aus. Ein Befehl, der teleportiert, einen Block setzt oder ein Inventar ändert, muss über den RegionScheduler laufen, sonst crasht der Server.
  • Der Schlüssel für Spielerdaten ist eine Designfalle. 75,1 % der Server laufen im Offline-Modus, und dort wird die UUID eines Spielers aus seinem Namen abgeleitet. Speichert dein /kit eine Wartezeit pro UUID, verwaisen alle deine Einträge, sobald ein Spieler seinen Namen ändert oder der Server später auf den Online-Modus umsteigt. Plane das ab der ersten Version ein, nicht an dem Abend, an dem ein Spieler seinen Rang zurückhaben will.

Nicht neu laden, sondern neu starten

Der reload-Befehl des Servers hinterlässt verwaiste Aufgaben und Listener, und deine neue plugin.yml wird nicht immer sauber gelesen. Ein Befehl, der nach einem reload da ist und beim nächsten Neustart wieder verschwindet, ist genau dieser Mechanismus. Stoppe und starte den Server neu, wenn du eine Deklaration testest.

Meine Empfehlung: wann plugin.yml reicht

Den hier beschriebenen Weg, einen commands:-Block plus einen Executor, behalte ich bis zu drei Unterbefehlen und zwei Argumenten. Unter dieser Schwelle ist alles andere unnötige Komplexität: eine Klasse, keine Abhängigkeit, kein Lizenzrisiko.

Darüber, sobald du Bedingungsketten auf args[0] schreibst, um give, list, reload und help auseinanderzuhalten, wechsle ich auf ein Befehls-Framework wie Lamp, das unter einer permissiven Lizenz steht und damit problemlos weitergegeben werden darf. Der Auslöser ist nicht die Spielerzahl, sondern die Anzahl der Verzweigungen: beim dritten else if auf args[0] rechnet sich das Framework.

Wovon ich abrate: direkt in den tiefliegenden Befehlsbaum des Servers zu greifen, nur um farbige Argument-Hinweise zu bekommen. Es funktioniert, es sieht beeindruckend aus, und es bindet dich an eine interne Schicht, die sich von Version zu Version ändert. Minecraft ist seit 26.1 deobfuskiert und der Remapper von Paper ist weg, das vereinfacht viel, aber eine interne API bleibt eine interne API.

Ein kompletter Befehl, generiert und kompiliert

Beschreibe den Befehl, den du willst, seine Argumente und seine Rechte. Minax schreibt die plugin.yml, den Executor und die Tab-Vervollständigung und gibt dir ein getestetes .jar.

Befehl erstellen

Häufige Fragen

Warum reagiert mein Befehl überhaupt nicht, ohne jede Fehlermeldung?

Weil er nicht im commands-Block von plugin.yml steht. Der Server weiß nichts von deinem Befehl, antwortet mit der Vanilla-Meldung für unbekannte Befehle, und dein Plugin wird nie aufgerufen. In der Konsole steht keine einzige Zeile. Genau deshalb ist das der Fehler, den Anfänger am schwersten finden.

Brauche ich getCommand().setExecutor(), wenn onCommand in der Hauptklasse steht?

Technisch nein, der Standard-Executor eines Befehls ist das eigene Plugin. Ich empfehle es trotzdem: fehlt der Befehl in plugin.yml, liefert getCommand null, und du bekommst beim Start einen lauten Fehler statt kompletter Stille.

Was passiert, wenn onCommand false zurückgibt?

Der Server zeigt dem Spieler die usage-Zeile aus plugin.yml. Das ist kein technisches Fehlersignal, sondern die eingebaute Hilfe. Gib false zurück, wenn der Befehl falsch benutzt wurde, und true, sobald du deine eigene Nachricht verschickt hast.

Wie verhindere ich, dass die Tab-Vervollständigung alle Spieler auflistet?

Gib eine leere Liste statt null zurück. Bei null greift der Fallback des Servers und vervollständigt mit den Namen der eingeloggten Spieler. Damit sieht jeder, wer gerade online ist.

Wohin gehört die Nachricht, wenn ein Spieler kein Recht hat?

Zwei Wege. Entweder in das Feld permission-message in plugin.yml, dann blockt der Server schon vor deinem Code. Oder du nimmst permission aus plugin.yml heraus und prüfst hasPermission selbst, dann hast du Text und Formatierung vollständig in der Hand.

Welche api-version gehört im Juli 2026 in plugin.yml?

Die 1.21-Familie ist im echten Serverbestand weiter in der Mehrheit: 1.21.11 kommt auf 36,4 % der gemessenen Server, 26.1.2 auf 14,9 % und 26.2 auf 7,4 %. Eine zu neue api-version schließt die Mehrheit aus. Ich bleibe auf der 1.21-Familie, solange die Kalenderversionen kein Drittel des Bestands halten.

Weiterlesen: die Fehler, die ein Plugin am Laden hindern, der Guide zum minimalen Spigot-Plugin, und die API-Referenzen auf docs.papermc.io, hub.spigotmc.org und minecraft.wiki. Die genannten Versionsanteile kommen aus der öffentlichen bStats-Messung vom Juli 2026.

Articles connexes

Nous utilisons Google Analytics, Microsoft Clarity et Reddit Pixel pour analyser l'utilisation du site et améliorer votre expérience. En savoir plus