Minecraft Plugins: Das Spigot-Framework

Aus Jonas Notizen Webseite
Version vom 13. März 2019, 13:29 Uhr von Admin (Diskussion | Beiträge) (Kopiert von alten Wiki)
(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)
Zur Navigation springen Zur Suche springen

Einleitung

Spigot ist ein Framework zum erstellen von "Plugins" für Server.

Ein Plugin kann keine neue Items/Blöcke hinzufügen, weil dies eine Modifikation wäre von Sachen die der Client nicht implementiert hat.
Ein Plugin kann neue Befehle/Crafting-Rezepte hinzufügen, Aktionen bei gewissen Events ausführen / Events verändern oder aufhalten, die Tab-Liste verändern sowie so manch Einstellungen am Server vornehmen (MOTD, Whitelist, Tab-Anzeige, ...). Das klingt zwar wenig, aber die Aktionen die man mit zB. den Spielern/der Welt ausführen kann sind groß.

Auf YouTube existieren viele "Minecraft Plugins programmieren"-Videoreihen von Menschen, die meißt keine große Ahnung von der Programmiersprache Java an sich haben und deswegen auch schlechte Prinzipien beibringen, sowie sagen "Das muss man so machen" oder nichts genauer erklären und damit dann viele unnötige Fragen in manchen Foren usw. produzieren.

Diese Seite habe ich hauptsächlich für Freunde gemacht, oder falls ich wieder eine große Pause von Spigot mache (Um mich wieder zu erinnern).

Links

Spigot Downloaden
YouTube-Reihe Spigot-Wiki

"Tutorial"

Projekt aufsetzen (in IntelliJ)

Spigot.jar einfügen

Um das Spigot-Framework nutzen zu können, müssen wir es zuerst "einfügen"/"verknüpfen".

  1. Downloade deine gewünschte Version von der GetSpigot-Webseite.
  2. Öffne dein IntelliJ-Projekt
    1. ["File" (Oben Links) > "Project Structure"] oder [Strg+Alt+Umschalt+S]
    2. Wähle die Sektion ["Libraries"]
    3. ["+" (grün) > "Java"] > gedownloadete .jar-Datei auswählen > ["OK"]
    4. Und ["OK"] oder ["APPLY"] (unten Rechts) zum speichern.

Artifacts erstellen

Um aus unseren Projekt-Sourcecode eine .jar-Datei zu machen, müssen wir IntelliJ sog. "Artifakte" zum exportieren erstellen. Danach kümmert sich IntelliJ mit 3 einfachen Knopf-Klicks um die Kompilierung.

  1. Öffne dein IntelliJ-Projekt
    1. ["File" (Oben Links) > "Project Structure"] oder [Strg+Alt+Umschalt+S]
    2. Wähle die Sektion ["Artifacts"]
    3. ["+" (grün) > "JAR" > "From Modules with dependencies"] > ["OK"]

Nun kannst du bei "Output Directory" den Pfad angeben, wo es gespeichert werden soll. (zB. der plugins-Ordner deines Minecraft Servers)

plugin.yaml

Jedes Plugin hat eine Datei mit dem Namen plugin.yml, in der generelle Informationen über das Plugin geschrieben sind. Diese Datei muss im Projekt-Verzeichnis liegen!


Was alles drinn stehen kann/muss sieht man hier

Eine Beispiel plugin.yml würde dann so aussehen:

# All diese Informationen können nachher über Java erhalten werden.
name: Pixel's Tolles Plugin
description: Beispiels YAML
version: 0.0.1
# Die Hauptklasse, welche von 'JavaPlugin' erbt
main: at.pixeltutorials.tollesplugin.TollesPlugin

# Befehle "registrieren". (Was sie machen definieren wir dann natürlich in Java)
commands:
   clearchat:
        aliases: ["cc"]

Die "Hauptklasse"

Jedes Plugin braucht auch eine "Hauptklasse". Diese muss in der plugin.yml unter main: verlinkt werden, von JavaPlugin erben und einen leeren Konstruktor haben.

Ihre 3 Haupt Observer-methoden, welche für uns interessant sind, sind:

  • onEnable(): Wird beim aktivieren des Plugins ausgeführt. Hier sollten alle Befehle/Events/Rezepte des Plugins eingefügt werden.#
  • onDisable(): Wird beim abschalten des Plugins ausgeführt. Hier kann man zB. ge-cachte Objekte noch in eine Datei speichern, um sie beim nächsten Start zu laden.
  • getDescription(): Gibt die Daten der plugin.yml zurück

Hier ist eine Beispielklasse:

package at.pixeltutorials.pixelspigot;

import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;

public class PixelSpigot extends JavaPlugin {

    @Override
    public void onDisable() {
        super.onDisable();
        Bukkit.getConsoleSender().sendMessage("§cFahre herunter!");
    }
    
    @Override
    public void onEnable() {
        super.onEnable();
        Bukkit.getConsoleSender().sendMessage("§aPlugin startet, intialisiere Befehle und Events...");
    }
}

Artifacts bauen (IntelliJ)

Wenn man seinen Quellcode nun in eine .jar kompilieren will, damit es der Server nutzen kann, muss man nurnoch:

  1. ["Build" (Oben Links) > "Build Artifacts"] [(Dein Projekt) > "Build"]

und die Datei (Falls noch nicht getan) in den /plugins-Ordner seines Servers schieben. Um das Plugin neu zu laden muss man nurnoch einmal /reload als Administrator des Servers eingeben.

Befehle

Jeder Befehl muss in der plugin.yml unter der Listen-Sektion commands: eingetragen werden:

name: Pixel's Tolles Plugin
description: Beispiels YAML
version: 0.0.1
main: at.pixeltutorials.tollesplugin.TollesPlugin

commands:
   clearchat:
        aliases: ["cc"]
   heal:

Nun kann man beim starten des Plugins einen dazugehörigen CommandExecutor verlinken, der sich um das ausführen kümmert.
(Meist erstellt man eine seperate Klasse für jeden Befehl. (Nach dem Prinzip "Eine Klasse sollte 1en groben Zweck erfüllen"))


Hierfür gibt es 2 Funktionale Interface:

package org.bukkit.command;

public interface CommandExecutor {

    /**
     * Wird beim ausführen des registrierten Befehls ausgeführt.
     * @param sender Der, der den Befehl ausgeführt hat (ConsoleCommandSender oder Player)
     * @param command Die org.bukkit.Command-Instanz mit Informationen über den Befehl (Von der plugin.yaml)
     * @param label Was für einen Alias/Befehls-Name zum ausführen genutzt wurde
     * @param args Mitgesendete Argumente ('/heal Hallo du da' -> ["Hallo", "du", "da"])
     * @return Ob "der Befehl Korrekt" war. Wenn nichts wird die "usage" von der plugin.yml ausgegeben.
     */
    boolean onCommand(CommandSender sender, Command command, String label, String[] args);
}
package org.bukkit.command;

import java.util.List;

public interface TabCompleter {
    /**
     * Wird beim "Tab-drücken" ausgeführt für Autovervollständigung
     * Parameter sing gleich wie beim CommandExecutor (oben).
     * @return Eine Liste an Autovervollständigungs-Möglichkeiten für das derzeitige Argument
     */
    public List<String> onTabComplete(CommandSender sender, Command command, String label, String[] args);
}

Beispiel-Befehl: Heal

Hier ist ein Beispiel für einen Befehl, der den angegebenen Spieler heilt:

public class HealCMD implements CommandExecutor {
    @Override
    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
        if (args.length == 0) {
            // '/heal'
            if (sender instanceof Player) {
                Player player = (Player) sender;
                player.setHealth(player.getHealthScale());
                sender.sendMessage("§aDu wurdest geheilt.");
            } else {
                sender.sendMessage("§cNur ein Spieler kann sich selber heilen.");
            }
        } else if (args.length == 1) {
            // '/heal <Spielername>'
            String inputName = args[0];
            Player target = Bukkit.getPlayer(inputName);
            if (target != null) {
                target.setHealth(target.getHealthScale());
                sender.sendMessage("§a" + target.getName() + " wurde geheilt!");
                target.sendMessage("§aDu wurdest von " + sender.getName() + " geheilt!");
            } else {
                sender.sendMessage("§cDer angegebene Spieler (" + inputName + ") ist nicht online.");
            }
        } else {
            sender.sendMessage("§cSyntax: /heal (Spielername>");
        }
        return true;
    }
}

CommandExecutor-Instanz beim starten eintragen:

@Override
public void onEnable() {
    super.onEnable();
    Bukkit.getConsoleSender().sendMessage("§aPlugin startet, intialisiere Befehle und Events...");
    Bukkit.getPluginCommand("heal").setExecutor(new HelloCMD()); // Hiermit greift Spigot nun auf die 'onCommand'-Methode von 'HelloCMD' beim ausführen des Befehls zu
}

Befehl (an sich) in der plugin.yml eintragen:

name: Heal Plugin
version: 1.0
main: at.pixeltutorials.tollesplugin.HealPlugin

commands:
   heal:

Events

Mit Spigot kann man so ziemlich alles was im Spiel passieren kann abfangen und Sachen verändern.

Um auf Events zu hören, benötigt man zuerst eine Klasse welche von org.bukkit.event.Listener erbt.
Diese Klasse muss beim Starten des Plugins (onEnable()) registriert werden: getServer().getPluginManager().registerEvents(new DeineKlasse(), this);

Für jedes Event, auf welche die Klasse hören will, braucht man eine Funktion mit der Annotation @EventHandler sowie das gewünschte Event als einzigen Parameter.
Ein Beispiel, welches auf das PlayerJoinEvent hört und die Nachricht grün einfärbt.

import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;

public class MeinKrasserListener implements Listener {

    @EventHandler(priority = EventPriority.HIGHEST)
    public void onJoin(PlayerJoinEvent event){
        event.setJoinMessage("§a" + event.getJoinMessage());
    }
}

Alle möglichen Events, sowie deren Eigenschaften und wann sie aufgerufen werden, findet man in den Javadocs.

Eigenes Event erstellen

Referenz auf Englisch

Jedes Event muss von der Klasse Event erben. Zusätzlich braucht jede Event-Klasse noch die unten folgenden Eigenschaften/Methoden/Variablen.

import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;

public class ExampleEvent extends Event {
    
    public String test;
    
    // JEDES Event braucht exakt diese 2 Funktionen!
    private static final HandlerList HANDLERS = new HandlerList();
    public HandlerList getHandlers() {
        return HANDLERS;
    }
    public static HandlerList getHandlerList() {
        return HANDLERS;
    }

}

You need these methods because Spigot uses the HandlerList class to separate other EventHandlers from listening to other events.

Um das Event nun zu broadcasten, damit andere es verändern/darauf "hören" können, muss man nurnoch:

ExampleEvent exampleEvent = new ExampleEvent(); // Instanz für den Aufruf erstellen. (Diese kann durch ihren Broadcast von anderen Listenern auch verändert werden.)
Bukkit.getPluginManager().callEvent(exampleEvent); // Event broadcasten

// Nun, wo alle EventHandler fertig sind, können wir hier mit unserer (vllt. veränderten) Version des Events weiterarbeiten
Bukkit.getPlayer("PixelTutorials").sendMessage(exampleEvent.test);

Configs

Bukkit nutzt YAML zur Daten-(De)Serialisierung.

Bukkits "nutzt" hierfür die Simple-YAML API.

YAML is a human-readable data-oriented serialization language.

Serialization is the process of translating data structures or object state into a format that can be stored and reconstructed later in the same or another computer environment.

File configFile = new File("plugins/MeinKrassesPlugin", "Config.yml");
FileConfiguration config = YamlConfiguration.loadConfiguration(configFile);

config.set("path.to.something", 22);
// Wenn man etwas setzt, ist es erst nur im Java-Objekt (Cache) verändert. Um den Cache jetzt in die Datei zu schreiben, muss man nur noch:
config.save(configFile);

Alle Methoden für Config-Sektionen (Getters & Setters) findet man hier.
Hier sind Beispiele wie Simple-YAML funktioniert, und wie man seine eigene Objekte de/-serialisiert.

Scheduler

Jeder Scheduler hat eine ID, mit welcher er durch Bukkit.getScheduler().cancelTask(task.getTaskId()); von überall aus gestoppt werden kann.

Synchron vs. Asynchron

Bukkit hat einen Haupt-Thread, welcher sich an die '20-Ticks pro Sekunde' Rate von Minecraft bindet. Aus diesem Grund darf man nur in synchronen-Aufgaben auf die Bukkit-API zurückgreifen. Wenn Bukkit den Fehler nicht bemerkt können gröbere Korruptionen zur Welt (oA.) auftreten.

Asynchrone-Aufgaben haben aus diesem Grund keine Wichtigkeit für Bukkit und können auch eingefroren werden. Aufwändige/Länger dauernde Aufgaben (wie zB. Datenbank-Abfragen) sollten deswegen Asynchron gehandhabt werden, weil der Server (Haupt-Thread) während dieser Zeit ja nichts machen kann.

wiederholende Aufgaben

Führt den Task mit einem gewissen Delay fortlaufend aus, bis er gestoppt wird.

Beispiel:

BukkitTask task = Bukkit.getScheduler().runTaskTimer(this, () -> {
    // run()-Methode vom Runnable
    System.out.println("Hallo!");
},
 20, // Wiederholt jede Sekunde
 20 * 6); // Startet nach 6 Sekunden

// Kann später durch sich selbst oder wen anders gestoppt werden:
// Bukkit.getScheduler().cancelTask(task.getTaskId());

normale Aufgabe

verzögerte Aufgaben

Das gleiche gibt es auch mit einem "Start-Delay":

Welt

Ich würde jedem empfehlen einfach mal in den [1] nachzuschauen, was für FUnktionen zB. der Spieler oder die Welt hat.

Partikel und Sound

Diese Effekte können alle Spieler in einem gewissen Radius sehen/hören.

Die Welten-Klasse bietet hierzu die Funktionen:

Spieler

Namen ändern

Player#setDisplayName​(String name) ändert den "freundlichen" Namen, welcher nur im Chat und von anderen Plugins genutzt wird!
Player#setPlayerListName​(String name) ändert den Namen, welcher in der "Tab/Spieler"-Liste angezeigt wird.

Spieler-Liste (Tab) verändern

Früher musste man es mit Packeten machen, heute geht es ganz einfach durch: Player#setPlayerListHeaderFooter​(String header, String footer)

Spieler verstecken

Die Funktion Player#hidePlayer​(Plugin plugin, Player player) versteckt denn Spieler gegenüber anderen, und Player#showPlayer​(Plugin plugin, Player player) zeigt ihn wieder an.

Partikel und Sound (Nur für sich)

Diese Funktionen sind nur für den angewandten Spieler sichtbar/hörbar!

Player#playEffect​(Location loc, Effect effect, T data) für Effekte.
Player#playNote​(Location loc, Instrument instrument, Note note) und Player#playSound​(Location location, Sound sound, SoundCategory category, float volume, float pitch) für Sound.

Texturenpacket wechseln

Um einen Spieler aufzufordern, ein Texturenpacket von einer bestimmten Quelle (URL) zu downloaden und es zu nutzen, gibt es die Funktion [Player#setResourcePack​(String url)].

Skin ändern

Hier ist ein Code-Snippet welches ich auch Stackoverflow gefunden habe.

public static boolean setSkin(GameProfile profile, UUID uuid) {
    try {
        HttpsURLConnection connection = (HttpsURLConnection) new URL(String.format("https://sessionserver.mojang.com/session/minecraft/profile/%s?unsigned=false", UUIDTypeAdapter.fromUUID(uuid))).openConnection();
        if (connection.getResponseCode() == HttpsURLConnection.HTTP_OK) {
            String reply = new BufferedReader(new InputStreamReader(connection.getInputStream())).readLine();
            String skin = reply.split("\"value\":\"")[1].split("\"")[0];
            String signature = reply.split("\"signature\":\"")[1].split("\"")[0];
            profile.getProperties().put("textures", new Property("textures", skin, signature));
            return true;
        } else {
            System.out.println("Connection could not be opened (Response code " + connection.getResponseCode() + ", " + connection.getResponseMessage() + ")");
            return false;
        }
    } catch (IOException e) {
        e.printStackTrace();
        return false;
    }
}

ActionBar

Die "ActionBar" (oder ChatMessageType.GAME_INFO wie es Spigot nennt) ist ein Text, welcher über der Hotbar des Spielers (für eine gewisse Zeit) angezeigt wird.

Dies ist leider nur durch Packets möglich. Hier eine kleine Referenz-Funktion zum senden einer ActionBar.

public void sendActionBar(Player sendTo, String message){
    message = ChatColor.translateAlternateColorCodes('&', message);
    IChatBaseComponent chatComponent = IChatBaseComponent.ChatSerializer.a("{\"text\": \"" + message + "\"}");
    PacketPlayOutChat bar = new PacketPlayOutChat(chatComponent, ChatMessageType.GAME_INFO);
    ((CraftPlayer)sendTo).getHandle().playerConnection.sendPacket(bar);
}

Title + Subtitle

Ist ein Text, welcher (für eine gewisse Zeit) groß in der Mitte des Bildschirms angezeigt wird.

Hierzu ist die Methode sendTitle​(String title, String subtitle, int fadeIn, int stay, int fadeOut) nützlich.

Anklickbarer Text

"Hologramme"

Feuerwerke

Ping

Um die Latenz eines Spielers zum Server (in ms) zu bekommen, hat der CraftPlayer schon eine eingebaute Variable.

((CraftPlayer)player).getHandle().ping;

Eigene Crafting Rezepte

Zuerst braucht man eine ItemStack-Instanz des Items, welches beim craften rauskommen soll.
Diese wird dann in den ShapedRecipe Konstruktor als einzigsten Parameter eingespeißt.

ItemStack bottle = new ItemStack(Material.EXP_BOTTLE);
ShapedRecipe expBottle = new ShapedRecipe(bottle);

Nun müssen wir noch das "Format" setzten, wie es in die Crafting-Table eingesetzt werden muss. Die Funktion nimmt 3 Strings ("Zeilen") an (mit je 3 Zeichen). Jedes Zeichen steht nachher für ein Material.

// Einsetz-Format bestimmen
expBottle.shape("*%*","%B%","*%*");

// Entsprechendes Material für die Zeichen im Pattern setzen
expBottle.setIngredient('*', Material.INK_SACK, 2);
expBottle.setIngredient('%', Material.SUGAR);
expBottle.setIngredient('B', Material.GLASS_BOTTLE);

Damit der Server nun auch dieses Crafting-Rezept hat, muss man es nur-noch hinzufügen.

getServer().addRecipe(expBottle);

Villager Shop

"Eigene" Entities

Bücher erstellen

Amboss-GUI

Scoreboards

Fake-Blöcke

Um den Spieler ein "Block-Veränderung" vorzutäuschen (Clientseitig via. Packets), gibt es die Funktion Player#sendBlockChange​(Location loc, BlockData block) oder auch Player#sendBlockChange​(Location loc, Material material, byte data) (DEPRECATED).
Sobald der Spieler auf diesen Block klickt, wird der eigentliche Block wieder dargestellt.

Fake Schild-Text

Das gleiche gibt es auch speziell für Schilde, welche den Text Clientseitig ändert. Siehe sendSignChange​(Location loc, String[ lines)].