Tutorial - Plugin-Entwicklung

Aus Nitradopedia
Wechseln zu: Navigation, Suche

Dies ist ein Tutorial zur Entwicklung von Bukkit-Plugins. Bukkit-Plugins sind Java-Projekte, die ins Spielgeschehen eingreifen können und somit mitunter den Charakter des Spieles stark verändern können. Dieses Tutorial bietet einen Einblick in die wichtigsten Features der Entwicklung, indem es die Befehle '/heile','/heile <spieler>','/heile alle','/teamchat <text>','/join <team>' beschreibt und am Ende ein konfigurables Starterkit für neue Spieler einrichtet. Zudem erstellen wir ein Warp-System, um Werte dauerhaft speichern zu können.

Alles, was tatsächlich in den Sourcecode von Minecraft eingreift (also zB Bilder für neue Items, neue oder geänderte Ansichten (vom Inventar zB)) lässt sich nur durch Spout oder einen ClientMod für Minecraft realisieren. So etwas wird hier nicht behandelt, hier geht es ausschließlich um Bukkit-Plugins, die nur schon existierende Werte verändern, aber nichts Neues erschaffen können.

Benötigte Software

Aktuelle Eclipse-Version herunterladen und installieren

Link zu eclipse.org

Aktuelles JDK (Java Development Kit) und JRE (Java Runtime Environment) installieren

Link zu oracle.com

Link zu java.com

Aktuelle CraftBukkit-rec.build herunterladen

Link zu bukkit.org

Das ist auch die JAR-Datei, die man auch braucht, um einen Server einzurichten.

Notepad++ herunterladen und installieren

Link zu notepad-plus-plus.org

Das ist nicht unbedingt nötig, aber ich empfehle es allen. YML-Dateien sind ganz schön zickige Biester und Notepad++ zeigt dem Benutzer die Fehler direkt an, wenn man auf "alle Zeichen anzeigen" klickt.

Localhost

Um unser Plugin auch testen zu können, ohne den öffentlichen Spielserver durch ständige reloads zu belasten, sollten wir uns einen Server auf dem eigenen Rechner einrichten. Dazu erstellt man einfach das Verzeichnis, in dem der Server laufen soll, kopiert die CraftBukkit hinein und schreibt sich "mal eben" ne .bat datei. Dazu öffnet man nen beliebigen Editor (Notepad++ zB) und fügt folgendes ein:

@ECHO OFF
IF /I "%PROCESSOR_ARCHITECTURE:~-2%"=="64" "%ProgramFiles%\Java\jre7\bin\java.exe" -Xms512M -Xmx512M -jar "%~dp0craftbukkit.jar"
IF /I "%PROCESSOR_ARCHITECTURE:~-2%"=="86" java -Xms512M -Xmx512M -jar "%~dp0craftbukkit.jar"


PAUSE

Die beiden "craftbukkit.jar" in der Datei müssen natürlich genauso heissen, wie die Craftbukkit.jar in dem Ordner. Auch auf die Java-Version muss geachtet werden, manche haben noch Java6, dann müsste der Dateipfad in der .bat angepasst werden. Wie genau man die Datei nennt, ist egal, Hauptsache, man speichert sie nicht als .txt, sondern als .bat. Wenn man diese .bat-Datei dann doppelklickt, startet der Server: Die Konsole geht auf und man sieht die Serverlog. Die komplette Verzeichnis- und Dateistruktur, die Welten und alles andere, was benötigt ist, wird generiert. Wenn man "stop" eingibt, schließt der Server wieder. Sobald unser Plugin fertig - oder zumindest lauffähig - ist, können wir es in den Plugins-Ordner verschieben, den Server starten und dann auf unseren Server gehen (als IP einfach "localhost" angeben), um zu testen, ob es funktioniert.

Viele Plugins brauchen einen zweiten Spieler, um sie ordentlich testen zu können. In dieser Situation kann der offline-Modus des Servers ausgenutzt werden. Wenn man in den server.properties den "Online-Mode" auf false stellt, kannst du dich mit deinem Spieleraccount einloggen, kurz die Internetverbindung kappen, ein zweites Minecraft aufmachen und dich dort - ohne die Spielerregistrierung am Anfang einfach auf "play offline" - als "Player" anmelden. So kann man dann alles ausprobieren, wozu ein zweiter Spieler benötigt wird, ohne einen weiteren Spieleraccount kaufen zu müssen.

Grundstock

Neues Projekt erstellen

Ein neues Projekt erstellt man bei Eclipse, indem man auf "File --> New --> JavaProject" klickt. Die Java-Version, die man dort einstellen kann (Use execution-environment JRE), sollte die selbe sein, auf der der Server läuft. (Am besten alles mit der aktuellsten machen!) Den Namen des Projektes sucht man danach aus, wie das Plugin später heissen soll. In unserem Fall nennen wirs einfach Tutorial. Den Rest müssen wir nicht einstellen, der bleibt beim Standart.

CraftBukkit-Befehlsbibliothek (=API) einfügen

Um die API hinzuzufügen, gibt es 2 Möglichkeiten:

1. Direkt beim Erstellen des neuen Projektes: Wenn man bei Punkt 3.1 anstatt auf "Finish" auf "Next" klickt, gibt es oben einen Reiter namens "Libraries". Dort kann man über "Add external JAR" die CraftBukkit.jar einfügen.

2. Wenn man doch schon auf Finish geklickt hat, isset auch nicht tragisch. Dann rechtsklickt man auf den "Ordner" namens Tutorial, der grade neu entstanden ist, geht in die Properties und dann links auf JavaBuildPath. Dort kann man dann wieder über "Add external JAR" die CraftBukkit.jar einfügen.

Die Dokumentation <-- sehr wichtig, um allein klarzukommen! Da ist das aufgelistet, was man unter Strg+Leertaste in Eclipse findet. Um die Informationen auch in der Box in Eclipse zu sehen, muss man rechts auf die CraftBukkit.jar klicken (Im Projektbaum, Referenced Libraries), in die Properties gehen und den link zu den Docs (http://jd.bukkit.org/apidocs/) bei Javadoc location path eintragen.

Package erstellen

Damit alles seine Ordnung hat, erstellen wir ein Package. Packages sind die Ordnerstruktur in dem Projekt. Jeder Punkt kündigt einen neuen Unterordner an. Wenn man für die Listener oder CommandExecutors eigene Packages erstellt (oder in logischen Häppchen, wie zum Beispiel bei einem Projekt wie Essentials ein Package für die Warps und die Teleporte, ein anderes für die Weltmanipulation mit Wetter und Zeiteinstellungen und ein drittes für die Spielermanipulation, usw...), muss man die darin enthaltenen Klassen in der Main erstmal importieren (genaueres dazu weiter unten). Bei großen Projekten hilft es aber sehr, eine gewisse Ordnung beizubehalten. Dazu rechtsklicken wir auf src (unter Tutorial) --> new --> Package.

Beispiel für Namensgebung des Packages:

me.stuppsman.tutorial

Ich habe in einem Tutorial gehört, me.<authorname>.<pluginname> sei Standart. Wichtig dabei ist jedoch nur, dass die Anfangsbuchstaben nach allen Punkten klein sind - so hat man sich darauf geeinigt. Nur Klassen dürfen mit Großbuchstaben anfangen.

main-Klasse erstellen

Jedes Java-Projekt braucht eine main-Klasse; das ist die Hauptklasse des Projektes. Die Abarbeitung des Quellcodes fängt in der Main an. Dazu rechtsklicken wir diesmal auf das Package und erstellen dort eine neue Klasse (new-->class, klar, ne? ) mit dem Namen des Projekts (also wieder "Tutorial"). Weitere Einstellungen sind hier wieder nicht nötig.

Damit aus dem JavaProjekt ein Plugin für BukkitServer werden kann, muss hinter dem Namen der main "extends JavaPlugin" stehen - damit werden die grundlegenden Funktionen eines Plugins schonmal eingefügt. Damit sind keine Ingame-"Funktionen" gemeint, sondern die Standart-Vorgehensweise von Bukkit mit Plugins. (Dass es als Plugin erkannt wird, dass es geladen wird, usw)

Und schon beschwert sich Eclipse zum ersten Mal: JavaPlugin ist rot-unterschlängelt - der Compiler kennt diesen Begriff zwar noch nicht, er steckt aber irgendwo in der API der CraftBukkit. Bei Eclipse ist dieses Problem sehr, sehr einfach zu lösen: Beim Mouseover über dem "Fehler" werden Vorschläge gemacht, wie mit dem Fehler zu verfahren ist. Es wird angeboten, org.bukkit.plugin.java.JavaPlugin zu importieren. Genau das müssen wir tun. Diese Importe werden uns immer wieder über den Weg laufen. Teilweise muss man aufpassen, dass man die richtigen Importe auswählt, bei einigen Dingen werden mehrere Importe angeboten, da muss man dann das passende heraussuchen.

package me.stuppsman.tutorial;

import org.bukkit.plugin.java.JavaPlugin;

public class Tutorial extends JavaPlugin {

}

onEnable() und onDisable()

Zunächst möchten wir (der Ästhetik willen), dass sich das Plugin auch an- und wieder abmeldet beim Serverstart/-shutdown. Mittlerweile sind diese Methoden nicht mehr zwingend notwendig, bei ordentlichen Plugins gibts aber fast immer Dinge, die in Verbindung mit dem Serverstart/stopp stehen. (Zum Beispiel Lade-/Speichervorgänge, das registrieren von Events.. kommt alles noch)

Dazu schreiben wir im Class-Body (=innerhalb der geschweiften Klammern) zwei neue Methoden namens onEnable() und onDisable(). Diese Methoden müssen public sein, da man auch von ausserhalb der main-Klasse darauf zugreifen können muss. Einen Rückgabewert haben diese Methoden nicht, sind also void. Da sie aber über JavaPlugin schon vorgefertigt sind, müssen wir per @Override die vorgefertigten Methoden "ansprechen". (Mit @Override gibts ne Fehlermeldung, wenn die Argumente nicht mit den fürs JavaPlugin benötigten übereinstimmen.. ohne @Override gehts auch, aber es kann sein, dass die Methode dann als "Alternativ-Methode" gesehen wird, die auf andere Argumente reagiert.. viel zu kompliziert für den Anfang - macht es mit @Override, dann wisst ihr auch, ob ihrs richtig macht :) )

Über System.out.println(); schreiben wir beim Starten und Stoppen eine Nachricht in die Serverlog / Konsole.

Das sieht dann so aus:


@Override
public void onEnable() {
	System.out.println("[Tutorial] Plugin erfolgreich geladen!");
}
@Override
public void onDisable() {
	System.out.println("[Tutorial] Plugin erfolgreich deaktiviert!");
}


Theoretisch haben wir zu diesem Zeitpunkt fast schon ein lauffähiges BukkitPlugin geschrieben. Was jetzt nur noch fehlt, ist die plugin.yml und ein Sinn (Funktionen, Befehle) hinter dem Plugin. Zunächst die plugin.yml:

plugin.yml

In der plugin.yml werden sämtliche Einträge, die das Plugin braucht, festgelegt. Dazu erstellen wir per Rechtsklick auf "src" (nicht auf das Package!) --> new --> file eine Datei namens plugin.yml. In dieser werden folgende Einträge zwingend benötigt:

name: Tutorial //Pluginname
version: 0.1 //Selbsteinschätzung, wie weit man schon ist
description: Tutorial zum Erstellen von BukkitPlugins
website: http://www.chillicraft.de///wenn ihr keine Homepage habt, nehmt Bukkit.org oder sonstwas
author: Stuppsman
main: me.stuppsman.tutorial.Tutorial 

Die plugin.yml darf keine Tabulatoren enthalten! Das führt zu Fehlern und damit dazu, dass das gesamte Plugin nicht geladen werden kann. Wenn ihr Umlaute benutzen möchtet, müsst ihr die Datei vorher auf UTF-8 konvertieren. Später müssen wir hier zudem noch die Commands registrieren, aber dazu kommen wir, sobald der erste Command fertig ist.

onCommand()

Um auf Spielercommands zu reagieren, muss man die Methode onCommand(...) (zu den Argumenten komm ich noch) im main-class-Body einfügen. onCommand() wird jedesmal ausgeführt, wenn ein Spieler im Ingame-Chat eine Nachricht schreibt, die mit "/" beginnt. Alles, was direkt hinter dem / kommt, ist der Befehl(=cmd), Wörter oder Zahlen, die durch ein Leerzeichen vom Befehl getrennt sind, nennen sich Argumente(=args[]). Die Argumente werden als String-Array übergeben, das heisst, wenn die Argumente Spieler, Zahlen.. usw. darstellen sollen (also alles ausser reinen Text), müssen die Argumente zunächst in den richtigen Typ "gecastet" werden. Häufig muss man dabei auf Fehler von Spielern aufpassen, da unbeachtete Fehler (zB wenn man den Befehl /heile <spieler> hat, aber /heile 2 eingibt, weiss das Plugin nicht, was es damit machen soll, da 2 kein Spieler ist) Exceptions (=Ausnahmen) werfen und zu Lags oder sogar zum Servercrash führen könnten.

Wichtig ist es, dass man niemals davon ausgehen darf, dass der Endbenutzer alles richtig macht - im Gegenteil, man muss auf jeden möglichen Fehler vorbereitet sein.

Die Methode onCommand() wird so implementiert:

@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {	 
	return false;
}
  • @Override kündigt dem Compiler an, dass diese Methode eine Methode einer Superklasse überschreibt oder ein Interface bedient. Kann man theoretisch zwar weglassen, aber es eliminiert Fehler, weil der Compiler sofort meckert, wenn Übereinstimmungsfehler vorkommen.
  • public genauso wie oben - man soll halt nicht nur privat in der Main etwas damit anfangen können.
  • Aber wieso "boolean"? Der boolean (=Wahrheitswert) gibt an, ob der Command, der ausgeführt wurde, auch erfolgreich ausgeführt wurde. War der Vorgang erfolgreich, passiert nichts, als das, was während des Befehls passiert ist. War der Vorgang jedoch nicht erfolgreich, wird dem ausführenden Spieler eine Nachricht mit der richtigen Vorgehensweise geschickt. (dazu kommen wir gleich)
  • CommandSender sender: Repräsentiert denjenigen, der den Command gesendet hat. Das muss nicht zwangsläufig ein Spieler sein, es könnte auch die Serverkonsole sein. (Wenn man versuchen würde, den Befehl /heile von der Console ausführen zu lassen, ist der Server verwirrt.. --> Exception!)
  • Command cmd: Die Commands, auf die er anspringt, sind in der plugin.yml festgelegt (wie gesagt, dazu kommen wir gleich noch). Alle anderen Commands werden ignoriert.
  • String label: Hab ich noch nie gebraucht Keine Ahnung, wozu das gut sein soll
  • String[] args: Hier werden die oben erwähnten Argumente gespeichert.
  • return false: Falls die Methode soweit kommt, wird false zurückgegeben (Wir brechen den Command sofort ab, wenn er erfolgreich ausgeführt wurde. Wenn nicht, wird nicht abgebrochen und das Programm läuft bis zum Ende. Da das Ende für uns aber bedeutet, dass wir in keinen Codeblock gekommen sind, der die Methode abbricht, ist der Command falsch benutzt worden. (Erklärt sich gleich, sobald wir den ersten Command drin haben!))


/heile


Wir schreiben jetzt den ersten Command namens /heile, der - erstmal - nur uns selber heilen soll:

if (cmd.getName().equalsIgnoreCase("heile")) {
}

cmd.getName() gibt den Namen des Commands aus (als String), den wir per equalsIgnoreCase mit "heile" vergleichen. equalsIgnoreCase ist in diesem Fall sehr praktisch, da dann die Groß-und Kleinschreibung nicht beachtet wird. Es funktionieren dann also sowohl /heile, als auch /HEILE, als auch /hEiLE.

Dazu müssen wir erstmal sicher gehen, ob der CommandSender auch ein Spieler ist. Wenn ers nicht ist, soll ne Fehlermeldung ausgegeben werden, wenn doch, casten wir direkt den CommandSender zu einem Player:

if (!(sender instanceof Player)) {
	System.out.println("Dieser Befehl ist nur für Spieler!");
	return true;
}
Player player = (Player) sender;

Das Ausrufezeichen bedeutet "nicht" bei Java. Das heisst, der {}-Block wird nur ausgeführt, wenn sender nicht zum Typ Player gehört. Aber wieso "return true;"? Der Befehl wurde ordentlich ausgeführt! Auch wenn er "nichts" tut, ausser ne Fehlermeldung auszugeben, war die Anwendung des Befehls nicht falsch. Dem Spieler, der die Console bedient, hilft es nichts, wenn ihm die korrekte Schreibweise nochmal angezeigt wird - auch dann wird er die Console nicht heilen können. ;) Durch den return wird der restliche Programmablauf unterbrochen, das heisst alles, was nach dem return kommt, wird nicht mehr verarbeitet - also auch nicht mehr der Cast vom CommandSender zum Player.

Player player = (Player) sender; hat mich anfangs extrem verwirrt. Ist aber, wenn man es einmal raus hat, ganz einfach:

  • "Player" ist die Typbezeichnung der Variable, die grade deklariert wird (wie in "int i=1")
  • "player" ist der Name der Variable - frei wählbar!
  • "(Player)" ist der Cast in einen Player. "Cast" heisst Typveränderung. (hier von CommandSender zu Player)
  • "sender" ist der CommandSender, der beim onCommand(CommandSender sender, ... ) übergeben wird.

Das heisst, wir erstellen hier eine Variable namens player vom Typ Player, indem wir den CommandSender "sender" zu einem Player casten. Ein Cast ist die Typveränderung von Klassen, die miteinander verwandt sind, also voneinander erben. (Siehe Punkt abstrakte Klassen) Diesen Cast können wir hier bedenkenlos machen, da wir vorher geprüft haben, ob er möglich ist.


Wie genau man die einzelnen Bedingungen verschachtelt, schauen wir uns gleich noch an, da kann die Reihenfolge sehr wichtig sein.

Zunächst müssen wir aber noch feststellen, ob hinter dem Befehl Argumente angegeben wurden. Da wir momentan keine Argumente brauchen, da wir ja selbst geheilt werden sollen, prüfen wir, um zu starten, ob die Länge des String-Arrays == 0 ist. ("==" ist ein Vergleich, "=" eine Zuweisung!) Wenn sie es nicht ist, geben wir false zurück, um dem Spieler die korrekte Syntax nochmal zu schicken.

if (args.length>0) {
	return false; 
}

Wenn wir also wissen, dass der cmd "/heile" war, dass der sender ein Spieler ist und dass keine Argumente angegeben wurden, dürfen wir aktiv werden.

if (args.length == 0) {
	player.setHealth(20.0);
	player.sendMessage(ChatColor.GREEN + "Du wurdest geheilt.");
	return true;
}

Damit haben wir fast alles, was wir brauchen, wir müssen es nur noch in die richtige Reihenfolge bringen: Dazu müssen wir überlegen, was zunächst am wichtigsten ist. Natürlich müssen wir zuerst überprüfen, ob der Befehl /heile ist. Aber wenn man mehrere Befehle hat, die von der Console nicht benutzt werden dürfen, kann es auch geschickter sein, das vorher zu prüfen - da ist logisches Denken gefragt. Denn wenn wir gleich den Befehl /heile <spieler> einbauen, kann der ja auch von der Console aus ausgeführt werden. Lediglich im Falle von args.length==0 muss geprüft werden, ob der sender ein Player ist.

In diesem Fall sollte es jedenfalls so aussehen:

 
if (cmd.getName().equalsIgnoreCase("heile")) {
	if (args.length == 0) {
		if (!(sender instanceof Player)) {
			System.out.println("Dieser Befehl ist nur für Spieler!");
			return true;
		}
		Player player = (Player) sender;
		player.setHealth(20.0);
		player.sendMessage(ChatColor.GREEN + "Du wurdest geheilt.");
		return true;
	}
	if (args.length>0) {
		return false; 
	}
}

Das ist schon der komplette CodeBlock für einen sicheren Befehl namens /heile. Der muss natürlich in die onCommand() vor den letzten return false;. Bestimmt ist einigen von euch aufgefallen, dass beide if-Abfragen sich gegenseitig eigentlich ausschließen und demnach unnützen Code darstellen. Die Abfrage if (args.length>0) kann man natürlich weglassen.

Damit der Server aber weiss, dass genau dieses Plugin auf den Befehl reagiert, müssen wir den Befehl jetzt in der plugin.yml "anmelden". Um einen Befehl ordentlich anzumelden, braucht man 3-4 Einstellungen:

  • description: Heilt den Spieler.

(Hier ist ne große Fehlerquelle !! Viele übersehn in den descriptions irgendwo ein ä, ö oder ü und wundern sich dann, dass nix funzt!)

  • usage: /<command>

(Hier wird die "korrekte Schreibweise", die ich schon so häufig erwähnt hab, festgelegt. Das heisst, wenn der Server den Befehl erkannt hat, der Rückgabewert aber trotzdem false ist, wird das, was hier steht, ins Chatfenster des Spielers geschrieben - "command" wird dann durch den Command-Namen ersetzt)

  • permission: tutorial.heile

(üblicherweise <pluginname>.<befehlsname>, kann man aber auch in Gruppen zusammenfassen wie <pluginname>.member oder <pluginname>.admin)

  • permission-message: 'Dazu fehlt dir die Berechtigung!' (siehe 1.! Fehlerquelle !!)
name: Tutorial
version: 0.2 
description: Tutorial zum Erstellen von BukkitPlugins
website: http://www.chillicraft.de/
author: Stuppsman
main: me.stuppsman.tutorial.Tutorial 

commands:
<2Leerzeichen>heile:
<4Leerzeichen>description: bla
<4Leerzeichen>usage: /<command>
<4Leerzeichen>permission: tutorial.heile
<4Leerzeichen>permission-message: 'Dazu fehlt dir die Berechtigung!'

Wenn man nicht unbedingt weiss als Chat-Farbe für die permissions-message haben möchte, lässt man die Zeile dazu in der plugin.yml weg und packt dafür in die onEnable() folgende Zeile hinein:

this.getCommand("heile").setPermissionMessage(ChatColor.RED + "Dazu fehlt dir die Berechtigung!");

Die Dateien müssen natürlich alle gespeichert werden. Danach wird das Projekt exportiert (Rechtsklick auf den obersten Ordner 'Tutorial' --> Export --> (Java)-JAR --> Projekt auswählen, Häkchen wegmachen (classpath und project brauchen wir für Plugins nicht!) Namen angeben und gib ihm!) Falls das nicht funktioniert, weil die plugin.yml angeblich nicht ganz auf der Höhe ist, müsste man einmal kurz "refreshen" (unterm export). Diese JAR dann nur noch in den Plugins-Ordner des Servers kopieren, restarten und hoffen, dass die Serverlog sich nicht beschwert ;)

/heile <spieler>


Wenn das jetzt einwandfrei funktioniert, haben wir alles richtig gemacht. Wenn nicht, solltest du dir die Fehlersuche angucken.

So, jetzt reicht es uns aber natürlich nicht, nur uns selber heilen zu können.. man möchte ja auch angeben mit dem, was man geschaffen hat ;) Also bauen wir jetzt den Befehl etwas um, um ihn auch so benutzen zu können: /heile <spieler>

Was genau müssen wir dafür machen? Der Grundbefehl existiert ja schon, das heisst, wir müssen also an der plugin.yml nichts verändern. Wir müssen nur überprüfen, ob es nicht doch ein Argument gibt. Das heisst, zunächst müssen wir

if (args.length>0) {
	return false; 
}

in

if (args.length>1) {
	return false; 
}

ändern, da ab jetzt auch (args.length==1) eine Bedeutung für uns hat.

Wenn das Argument dann auch noch ein Spieler ist, können wir ihn auch heilen. Die Abfrage, ob ein Argument angegeben wurde, hatten wir ja schon (zmdst ganz ähnlich):

if (args.length == 1) {
}

Um herauszufinden, ob das angegebene Argument ein Spieler ist, erstellen wir uns per

 Player ziel = this.getServer().getPlayer(args[0]);

ein Player-Objekt namens ziel. (Das erste Element ist NICHT args[1], man fängt bei [0] an zu zählen!) Wenn es den Spieler jedoch nicht gibt (oder er nicht online ist), wird der Player ziel mit "null" initialisiert (also zwar initialisiert, aber ohne Wert). Sollte man auf "null" zugreifen, also - wie hier - einen so initialisierten Spieler heilen wollen, würde das eine NullPointerException werfen - und Exceptions heisst es ja zu vermeiden. Wenn "ziel" also "null" ist, müssen wir wieder aus dem Code raus und ne Fehlermeldung ausgeben:


Player ziel = this.getServer().getPlayer(args[0]);
if (ziel == null) {
	sender.sendMessage(ChatColor.RED + "Heilung fehlgeschlagen! Spieler nicht gefunden!");
	return false;
}


Wenn "ziel" nicht "null" ist, läuft der Code weiter, das heisst, wir haben einen Player, den wir heilen können.

ziel.setHealth(20.0);
ziel.sendMessage(ChatColor.GREEN + "Du wurdest von "+sender.getName()+" geheilt!");
return true;


Unsere komplette Tutorial.java sollte jetzt so aussehen

package me.stuppsman.tutorial;

import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;

public class Tutorial extends JavaPlugin {

	@Override
	public void onEnable() {
		System.out.println("[Tutorial] Plugin erfolgreich geladen!");
	}
	@Override
	public void onDisable() {
		System.out.println("[Tutorial] Plugin erfolgreich deaktiviert!");
	}
	@Override
	public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {	 
		if (cmd.getName().equalsIgnoreCase("heile")) {
			if (args.length>1) {
				return false; 
			}
			if (args.length == 0) {
				if (!(sender instanceof Player)) {
					System.out.println("Dieser Befehl ist nur für Spieler!");
					return true;
				}
				Player player = (Player) sender;
				player.setHealth(20.0);
				player.sendMessage(ChatColor.GREEN + "Du wurdest geheilt.");
				return true;
			}
			if (args.length==1) {
				Player ziel = this.getServer().getPlayer(args[0]);
				if (ziel == null) {
					sender.sendMessage(ChatColor.RED + "Heilung fehlgeschlagen! Spieler nicht gefunden!");
					return false;
				}
				ziel.setHealth(20.0);
				ziel.sendMessage(ChatColor.GREEN + "Du wurdest von "+sender.getName()+" geheilt!");
				return true;
			}
			
		}

		return false;
	}
	
}

/heile alle


Es würd ja keinen Spass machen, wenn wir da keinen draufsetzen könnten.. Jeden Spieler einzeln zu heilen nervt, vor allem, wenn man einen großen Server hat. Wäre doch praktisch, wenn man alle gleichzeitig heilen könnte. Dazu muss man nur wissen, wie man an eine Liste der Spieler kommt, die online sind und diese verarbeitet.

this.getServer().getOnlinePlayers()

Gibt uns einen Array voll von Playern zurück, genau was wir brauchen.(=Player[]) Mit einer einfachen for-Schleife können wir dann alle Spieler nacheinander abarbeiten.

for (Player player : this.getServer().getOnlinePlayers()) {
	player.setHealth(20.0);
}

Diesen Befehl kann man quasi "vorlesen": Für (jeden) Player (Im folgenden Codeblock nennen wir ihn)player(der Liste): this.getServer().getOnlinePlayers() (machst du){player.setHealth(20.0);}.

Sobald das durch ist können wir wieder ne Erfolgsnachricht an den sender schicken.

sender.sendMessage(ChatColor.GREEN + "Alle Spieler wurden geheilt!");

Wir könnens aber auch - weil an sich soll ja auch jeder wissen, dass du der Hengst warst, der alle geheilt hat - nicht nur dem Anwender, sondern auch einfach allen schicken. Eine Möglichkeit wäre, das mit in die for-schleife zu packen:

for (Player player : this.getServer().getOnlinePlayers()) {
	player.setHealth(20.0);
	player.sendMessage(ChatColor.GREEN + "Du wurdest von "+sender.getName()+" geheilt!");
}

Aber nicht immer ist grad ne for-Schleife zur Hand, wenn man die Funktion braucht, allen Spielern eine Nachricht zu schicken. Natürlich gibt es eine einfachere Möglichkeit:

this.getServer().broadcastMessage(ChatColor.GREEN + sender.getName() + " hat alle Spieler geheilt!");

Man sollte diese Zeile nur definitiv nicht in die for-Schleife packen, sonst kommt die Nachricht genau so oft, wieviele Spieler grad online sind, weil jeder einen broadcast an alle abschickt. Dahinter nur noch ein return true;, damit der Server weiss, dass alles gut gegangen ist und der Befehl ist lauffähig, wenn auch noch nicht ganz sicher. Es gibt immernoch die Möglichkeit, dass niemand online ist, wenn jemand den Befehl in der Konsole eingibt. Wenn das so wäre, gäbe es vielleicht eine NullPointerException, weil der Array leer ist - ich weiss nicht, wie for auf einen leeren Array reagiert. Aber allein um die Möglichkeit auszuschließen, und um dem Anwender in diesem Fall eine blöde Nachricht zuschicken zu können, bauen wir noch folgenden Code ein:

if (this.getServer().getOnlinePlayers().length==0) {
	System.out.println("Langeweile?");
	return true;
}

Der ganze Befehl ist also:

if (args[0].equalsIgnoreCase("alle")) {
	if (this.getServer().getOnlinePlayers().length==0) {
		System.out.println("Langeweile?");
		return true;
	}
	for (Player player : this.getServer().getOnlinePlayers()) {
		player.setHealth(20.0);
	}
	this.getServer().broadcastMessage(ChatColor.GREEN + sender.getName() + " hat alle Spieler geheilt!");
	return true;
}

Nur wo muss der jetzt hin? Natürlich kurz nach den Check, ob es nur ein Argument ist, und noch vor dem Versuch einen Spieler aus "alle" zu machen.

Wenn es tatsächlich einen Spieler geben sollte, der alle heisst, sollte man ihn bannen, sonst werden alle geheilt, wenn nur der Spieler alle geheilt werden sollte. ^^ Selbst schuld, wenn man sich alle nennt ;)

(Wenn wir die Config einbauen, können wir den Command konfigurabel machen, falls tatsächlich der unglaubliche Fall eintritt.. Natürlich müssen wir niemanden aus Bequemlichkeit bannen!)

CommandExecutor

Jetzt möchten wir natürlich nicht nur einen, sondern ganz viele Commands einbringen. Man könnte zwar alles in die eine Main packen:

if (cmd.getName().equalsIgnoreCase("heile") {
...
}
if (cmd.getName().equalsIgnoreCase("pvp") {
...
}
usw.

Aber das wird grade bei umfangreichen Projekten extrem unübersichtlich. Man scrollt sich dumm und dämlich, wenn man mal etwas sucht oder ändern möchte. Deswegen wurde die Möglichkeit des CommandExecutors eingebaut. So kann man sich für jeden Befehl eine eigene Klasse schreiben und hat sofort alles zur Hand, was man sucht. Die CommandExecutor-Klasse packen wir in das selbe Package und sie wird genauso erstellt wie die main-Klasse (rechtsklick aufs Package...). Wenn ihr für CEs oder Listener eigene Packages erstellen möchtet, könnt ihr das ohne weiteres tun, müsst dann aber diese Packages dort importieren, wo ihr sie benutzt. Wir nennen unsere erste jetzt CE_heile - wenn wir dieses Schema beibehalten haben wir die CEs in der Leiste links alle untereinander. Diese Klasse muss CommandExecutor implementieren:

package me.stuppsman.tutorial;

import org.bukkit.command.CommandExecutor;

public class CE_heile implements CommandExecutor {

}

An dieser Stelle erinnert Eclipse dich daran, dass eine Klasse, die CommandExecutor implementiert, auf jeden Fall eine Methode namens onCommand(..) braucht. Das kann man ignorieren, wir übernehmen die komplette Methode aus der main-Klasse. Später aber ist das ganz praktisch, dann kann man es sich sparen, die paar Zeilen selbst schreiben zu müssen. Am besten man probiert es kurz aus, was passiert, wenn man auf "Add unimplemented Methods" klickt und macht es mit Strg+z wieder rückgängig, um zu sehen, was dann passiert. Wir schneiden jetzt also die komplette onCommand() {code für den heile-Befehl} aus und fügen sie in den CommandExecutor ein. Damit ist die main schön übersichtlich, weil sie nur noch aus den onEn- und onDisable-Methoden besteht. (Beim Kopieren muss man die letzte '}' stehen lassen.) Jetzt haben wir im CommandExecutor aber ein Problem. Plötzlich funktioniert "this" nicht mehr. Wieso ist das so? "this" verweist immer auf die Klasse, in der man sich grade befindet. Jetzt sind wir nicht mehr in der Tutorial-Klasse, die über JavaPlugin eine Methode namens getServer() vererbt bekommt. Glücklicherweise kann Bukkit das auch statisch. Wir ersetzen also per Strg+F eben alle "this" durch "Bukkit". Aber das funktioniert nur in diesem speziellen Fall, wir werden Bukkit auch noch ersetzen.

Wie oben bereits angedeutet, der eigentliche Programmablauf passiert in der Main. Woher also soll die Main wissen, wo sie nach dem Code für den Command suchen soll? Dafür müssen wir in der onEnable den Executor für den jeweiligen Command setten. Das passiert durch:

this.getCommand("heile").setExecutor(new CE_heile(this)); 

Hier wird "this" an den CE übergeben. Damit der damit aber überhaupt umgehen kann, müssen wir einen Konstruktor erstellen. Ein Konstruktor beschreibt die Vorgehensweise, wenn eine neue Instanz der Klasse aufgerufen wird. Darin werden die wichtigsten Variablen initialisiert. Dazu schreiben wir in den CommandExecutor: (man kann sich durch ein Mouseover die Arbeit teilweise abnehmen lassen)

private Tutorial plugin;
public CE_heile (Tutorial t) {
	plugin = t;
}

Hier reservieren wir oben bei "private Tutorial plugin;" Speicherplatz für eine Instanz von der Klasse Tutorial, die wir plugin nennen. Wenn wir jetzt den Command anmelden, erstellen wir mit dem "new" über den Konstruktor quasi eine modifizierbare Kopie des CommandExecutors, die für die Zeit, die der Server läuft mit der Instanz der Tutorial-Klasse arbeitet - damit initialisieren wir "plugin" (=packen Daten (=Tutorial t) in den reservierten Speicherplatz). Wir brauchen ja die Informationen/Befehle aus der Tutorial-Klasse.

Jetzt verweist "plugin" auf unser altes "this". Also können wir jetzt Bukkit, wenn wir wollen, durch plugin ersetzen.

Das müsste jetzt wieder lauffähig und sicher sein. Das praktische ist, man kann die nächsten Befehle einbauen, wie man lustig ist. Man kann beliebig viele Commands in die Main und in jeden CommandExecutor packen, solange man für den Command den richtigen Executor einstellt.

CE_heile.java:

package me.stuppsman.tutorial;

import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;

public class CE_heile implements CommandExecutor {
	
	private Tutorial plugin;
	public CE_heile(Tutorial tutorial) {
		plugin = tutorial;
	}

	@Override
	public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {	 
		if (cmd.getName().equalsIgnoreCase("heile")) {
			...
		}
		return false;
	}
}

Tutorial.java:

package me.stuppsman.tutorial;

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

public class Tutorial extends JavaPlugin {
	@Override
	public void onEnable() {
		System.out.println("[Tutorial] Plugin erfolgreich geladen!");
		this.getCommand("heile").setPermissionMessage(ChatColor.RED + "Dazu fehlt dir die Berechtigung!");
		this.getCommand("heile").setExecutor(new CE_heile(this));
	}
	@Override
	public void onDisable() {
		System.out.println("[Tutorial] Plugin erfolgreich deaktiviert!");
	}
	
	
}

HashMaps

Da es hier im Forum zum Thema gemacht wurde und es sich hervorragend dazu eignet, die Funktion von HashMaps zu erklären, schreiben wir eben nen Gruppenchat-Befehl. Die Nachricht soll dann nur an Mitglieder eines Teams verschickt werden. Da wir noch keine Teams haben, schreiben wir auch noch einen /join-Befehl dazu.. wir hams ja.

Dazu müssen wir erstmal eine HashMap initialisieren. HashMaps sind Listen, die sich Pärchen merken können. Man deklariert 2 Typen, den ersten für den "Key", der zweite ist dann der "Value"(=Inhalt) von dem Key. Wir in unserem Fall müssen uns für jeden Spielernamen merken, in welchem Team er sich befindet. Das ist entweder ein Player und ein String, oder einfach zwei Strings. Wenn es nur 2 Teams sein sollen, können wir sogar <Player, boolean> benutzen, das wär noch etwas weniger Speicherverbrauch. Aber ich hatte mir das jetzt so vorgestellt, dass man das Team frei benennen kann, so dass es theoretisch auch unzählige Teams geben könnte.

HashMap<String, String> teamHM = new HashMap<String, String>(); 

Durch das "= new HashMap<String, String>;" haben wir direkt eine leere HashMap erzeugt, in der (die?) wir Daten ablegen können. Wenn man die HashMap nur deklariert, aber keine neue erstellt, gibt das ne NullPointerException bei jedem Mal, wenn wir auf die HashMap zugreifen. (Nä? Wenn ihr rausgeschickt werdet, um ein Auto zu waschen, aber nur ne Garage findet, wisst ihr auch nicht, was ihr machen sollt.. )

Die HashMap erstellen wir in der main-Klasse.

Wir brauchen auf jeden Fall den nächsten CommandExecutor. Wie das geht, wissen wir ja mittlerweile. Erstmal für den Join-Befehl. Der Befehl soll genau diese Funktion haben: /join <team>

In HashMaps Daten hineinpacken funktioniert per:

plugin.teamHM.put(spielername, team) 

Da wir nicht in der Main sind, nehmen wir hier wieder plugin, statt this. (an den Konstruktor gedacht?) (Viele Leute schreiben sich auch für die Main nen Konstruktor, dann funktioniert plugin überall. Da ich mir so aber nie vor Augen geführt hatte, was das sollte und wofür das gut war, habe ich das hier im Tutorial weggelassen!) Wir müssen nur eben festlegen, was "spielername" und "team" sind.

Unser Code sollte jetzt so aussehen:

@Override
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
	if (cmd.getName().equalsIgnoreCase("join")) {
		plugin.teamHM.put(sender.getName(), args[0]);
		plugin.getServer().broadcastMessage(sender.getDisplayName() + ChatColor.AQUA + " ist jetzt in Team " + ChatColor.GREEN + args[0] + "!");
		return true;
	}
	return false;
}

Nicht vergessen, dass wir CE_join(this) noch als Executor setzen müssen. Wenn wir gleich dazu auch schon den Executor für den nächsten Befehl setzen, macht Eclipse den Grundstock für einen CE schon per Mouseover. CE_teamchat - alles, was nach /teamchat (oder, weils so lang ist, /tc - aber den alias (=Alternativschreibweise) bringen wir später rein, wenn wir uns um die plugin.yml kümmern) kommt, soll nur an Teammitglieder gesendet werden.

Theoretisch ist das auch nur eine einfache for-Schleife. Nur die Bedingungen sind komplizierter als oben.. Erstens muss der Spieler online sein - sendMessages an Offline-Spieler müssten NullPointerExceptions werfen. Nach jedem Neustart sind die Gruppen ohnehin wieder resetted, so bringt das eh nichts. Wenn wir das nicht möchten, müssen wir die HashMap speichern - aber das kommt erst später. Momentan funzt das also nur bei Spielern die online sind. Die for-Schleife kennen wir ja schon.

Wir müssen prüfen, ob der Empfänger und der Sender schon ne Gruppe gesettet haben. Wenn das nicht der Fall ist, suchen wir nach ner Gruppe, die noch nicht implementiert wurde --> NullPointerException. Das machen wir, indem wir schauen, ob die HashMap den key schon hat.


for (Player player : plugin.getServer().getOnlinePlayers()) {
	String empfaenger_key = player.getName();
	if (plugin.teamHM.containsKey(empfaenger_key)) {...}}

Man muss diesen Zwischenschritt ("String ...") nicht unbedingt machen, aber es dient doch stark der Übersichtlichkeit und Verständlichkeit. player steht immernoch für jeden Player, der Online ist, wir sind ja noch in der for-Schleife. (Nicht wie im heile Befehl, wo wir den CommandSender in player umgewandelt hatten - nicht durcheinander bringen, zwei völlig verschiedene Geschichten!)

Also.. Wir haben jetzt einen Codeblock geschaffen, der uns nur auf Spieler reagieren lässt, die ihre Gruppe gesettet haben. Vorher müssen wir aber immernoch prüfen, ob der Anwender selbst schon ne Gruppe hat - wenn nicht, sollten wir ihm das sagen ;)

Wenn jetzt noch das sender_team und das empfaenger_team übereinstimmen, dürfen wir die Nachricht senden.

String sender_key = sender.getName();
if (!plugin.teamHM.containsKey(sender_key)) {
	sender.sendMessage(ChatColor.RED + "Erst ein Team wählen!" + ChatColor.GRAY + "(/join <team>)");
	return true;
}
String sender_team = plugin.teamHM.get(sender_key);
String nachricht = "";

for (Player player : plugin.getServer().getOnlinePlayers()) {
	String empfaenger_key = player.getName();
	if (plugin.teamHM.containsKey(empfaenger_key)) {
		String empfaenger_team = plugin.teamHM.get(empfaenger_key);
		if (sender_team.equalsIgnoreCase(empfaenger_team)) {
			player.sendMessage(ChatColor.BLUE + "[" + ChatColor.GREEN + sender_team + ChatColor.BLUE + "] " + ChatColor.AQUA + nachricht);
		}
	}
}

Gelangweilte Admins sind jetzt wieder das Problem.. was passiert, wenn kein Spieler online ist? Wie in /heile alle brauchen wir den Check hier drin. Und wenn gar keine Argumente mitgeschickt werden (die einzige Möglichkeit bei dem Befehl irgendetwas falsch zu machen), sollten wir dafür sorgen, dass der Anwender die richtige Schreibweise erklärt bekommt.

if (plugin.getServer().getOnlinePlayers().length == 0) {
	sender.sendMessage(ChatColor.RED + "Selbstgespräche?");
	return true;
}
if (args.length==0) {
	sender.sendMessage(ChatColor.RED + "Sprich dich aus..");
	return false;
}

Die Befehle jetzt nochmal komplett:

package me.stuppsman.tutorial;

import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;

public class CE_join implements CommandExecutor {

	private Tutorial plugin;
	public CE_join(Tutorial tutorial) {
		plugin = tutorial;
	}
	@Override
	public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
		
		if (cmd.getName().equalsIgnoreCase("join")) {
			plugin.teamHM.put(sender.getName(), args[0]);
			plugin.getServer().broadcastMessage(ChatColor.DARK_AQUA + sender.getName() + " ist jetzt in Team " + args[0] + "!");
			return true;
		}
		
		return false;
	}
	
}

package me.stuppsman.tutorial;

import org.bukkit.ChatColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;

public class CE_teamchat implements CommandExecutor {

	private Tutorial plugin;
	public CE_teamchat(Tutorial tutorial) {
		plugin = tutorial;
	}

	@Override
	public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
		
		if (plugin.getServer().getOnlinePlayers().length == 0) {
			sender.sendMessage(ChatColor.RED + "Selbstgespräche mit der Serverlog?!");
			return true;
		}
		if (args.length==0) {
			sender.sendMessage(ChatColor.RED + "Sprich dich aus..");
			return false;
		}
		String sender_key = sender.getName();
		if (!plugin.teamHM.containsKey(sender_key)) {
			sender.sendMessage(ChatColor.RED + "Erst ein Team wählen!" + ChatColor.GRAY + "(/join <team>)");
			return true;
		}
		String sender_team = plugin.teamHM.get(sender_key);
		String nachricht = "";
		
		for (Player player : plugin.getServer().getOnlinePlayers()) {
			String empfaenger_key = player.getName();
			if (plugin.teamHM.containsKey(empfaenger_key)) {
				String empfaenger_team = plugin.teamHM.get(empfaenger_key);
				if (sender_team.equalsIgnoreCase(empfaenger_team)) {
					player.sendMessage(ChatColor.BLUE + "[" + ChatColor.AQUA + sender_team + ChatColor.BLUE + "] " + ChatColor.YELLOW + sender.getName() + " " + ChatColor.AQUA + nachricht);
				}
			}
		}
		
		return true;
	}

}

Jetzt müssen wir aus den Argumenten nur noch ne Nachricht machen und der Befehl ist fertig. Das passiert so: Wir nehmen ne Buffer-Variable, vom Typ StringBuilder, die nach und nach aufgefüllt wird mit einem Argument und einem Leerzeichen:

StringBuilder nachricht = new StringBuilder();
for(int i = 0; i < args.length; i++) nachricht.append(args[i]).append(" ");

Dieses mal sieht die for-Schleife anders aus. Aber wieder kann man sie quasi vorlesen: Für (die Startbedingung) int i = 0 (machst du solange wie) i<args.length (während) i++ (i immer größer wird) (genau das:) nachricht.append(args[i]).append(" "); (append = hinten anhängen)

int i = 0; ist gleichzeitig eine Deklaration und eine Implementation. (=ohne int davor fehlt was ^^) Da sendMessage("..") mit dem Typ StringBuilder klarkommt, brauchen wir nichts weiter zu tun, als unser String nachricht = ""; durch die beiden Zeilen zu ersetzen und müssen nur noch die plugin.yml anpassen. Da kommen wir dann zurück auf die Aliases. Man kann in der plugin.yml Alternativcommands einstellen. Dann reagiert das plugin sowohl auf /teamchat, als auch auf /tc.. Dafür müssen wir nur den entsprechenden Eintrag bei dem Command machen:

name: Tutorial
version: 0.7 
description: Tutorial zum Erstellen von BukkitPlugins
website: http://www.chillicraft.de/
author: Stuppsman
main: me.stuppsman.tutorial.Tutorial 

commands:
  heile:
    description: bla
    usage: /<command>
    permission: tutorial.heile
  teamchat:
    description: bla
    usage: /<command> <nachricht>
    permission: tutorial.tc
    aliases: tc
  join:
    description: bla
    usage: /<command> <team>
    permission: tutorial.join

Ich kanns gar nicht oft genug wiederholen: keine Tabulatoren!

Damit müsst ihr euch eigentlich nur noch schöne Farben aussuchen, und das kleine Gruppenchat-Plugin sollte funktionieren :) Die Konsole hab ich absichtlich nicht ausgeschlossen. So könnte man sich per /tc mit nem Admin über die Konsole unterhalten.. sehr praktisch, wie ich finde.

Listener

Wäre doch cool, wenn man den Teamchat auch togglen (an- und ausschalten) könnte. Wenn man auf nem großen Server ist und in einer kleinen Gruppe an einem Projekt arbeitet, schreibt man 90% des Chatinhaltes dann an die Gruppe. Immer /tc davor zu schreiben, ist auf Dauer doch nervig und wird bestimmt häufig vergessen. Also schreiben wir uns '/tc an' und '/tc aus' - nur "an" oder "aus" an die Gruppe zu schreiben, ist dann zwar unmöglich, aber mal ehrlich.. Es gibt schlimmeres.

Einen neuen CommandExecutor brauchen wir nicht, der Befehl /tc existiert ja schon, er braucht nur ne weitere Funktion. Also bauen wir in den Code die Abfrage hinein, ob die Argumentlänge 1 ist und das Argument "an" oder "aus" ist. Wenn das so ist, merken wir uns, dass der Spieler den Teamchat angeschaltet hat - mehr muss in dem Command ja nicht passieren. Das "merken" funktioniert natürlich wieder über ne HashMap, die wir - natürlich - wieder vorher in der Main deklarieren und initialisieren müssen. Diesmal reicht ein String und ein Boolean.

if (args.length == 1 && args[0].equalsIgnoreCase("an")) {
	plugin.TeamChatHM.put(sender.getName(), true);
	sender.sendMessage(ChatColor.DARK_AQUA + "TeamChat angeschaltet!");
	return true;
}

Das selbe schreiben wir natürlich auch für "aus" mit "false" und "ausgeschaltet". Ansonsten komplett identisch.

Cool. Jetz haben wir uns also gemerkt, dass jemand den TeamChat benutzen möchte. Aber was machen wir damit? Wie reagiert man auf etwas, was sich nicht durch ein "/" im Chat ankündigt? Dafür braucht man Listener. Listener warten im Hintergrund, bis etwas bestimmtes passiert (das "Event") und führen dann ihren Codeblock aus. Das kann man für alles mögliche benutzen - vom PlayerSchaden übers InventarÖffnen bis zum Login kann man echt auf fast jeden nur erdenklichen Fall reagieren. Das heisst, man kann ins Spielgeschehen eingreifen - solange man das richtige Event ankündigt. In unserem Fall müssen wir reagieren, wenn jemand etwas in den Chat schreibt, das Event dazu heisst - welch Zufall - PlayerChatEvent.

Bevor wir aber was mit dem Event anfangen können, brauchen wir den Listener. Dazu erstellen wir uns wieder ne extra Klasse, diesmal implementiert die Klasse "Listener". Das gibt uns Zugriff auf den EventHandler, der der main-Klasse vermittelt, auf welche Events in diesem Listener reagiert wird. Ich nenne die Klasse jetzt L_Chat - aber wie ihr die nennt, ist wie immer euch überlassen.

package me.stuppsman.tutorial;

import org.bukkit.event.Listener;

public class L_Chat implements Listener {

}

Da wir die HashMap aus der Main-Klasse brauchen, schreiben wir uns wieder nen Konstruktor und den "Parkplatz" für die HashMap - theoretisch könnten wir auch wieder die komplette main übergeben, aber das ist momentan nicht nötig, uns reicht die HashMap vollkommen.

	private HashMap<String, Boolean> hm;

	public L_Chat(HashMap<String, Boolean> hm) {
		this.hm = hm;
	}

Um das Event anzukündigen, schreiben wir uns jetzt eine Methode. Früher wars wichtig, wie diese Methode heisst, mittlerweile ist der Name schnurzpiep, wichtig ist nur das Event und ein @EventHandler darüber.

@EventHandler
public void onPlayerChat(PlayerChatEvent event) {
}

Was soll denn jetzt passieren, wenn jemand etwas in den Chat schreibt? Zunächst mal müssen wir verhindern, dass die Nachricht ganz normal verarbeitet wird. Sonst würde sie ja wie alles andere an jeden geschickt werden und das wollen wir ja nicht. Dafür müssen wir das Event canceln mit event.setCancelled(true);. Das soll aber nur passieren, wenn die HashMap erstens den Key enthält, und wenn, der Eintrag für den Key "true" ist - der TeamChat also angeschaltet ist. Wenn es so ist, soll genau das passieren, was wir im Command /teamchat festgelegt haben. Das passiert durch player.performCommand(String) - der "String" ist alles, was nach dem Slash im Chatfenster geschrieben werden sollte, um den gewünschten Effekt zu erzielen.

Heisst, unser kompletter Listener ist also:

package me.stuppsman.tutorial;

import java.util.HashMap;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerChatEvent;

public class L_Chat implements Listener {
	private HashMap<String, Boolean> hm;
	public L_Chat(HashMap<String, Boolean> hm) {
		this.hm = hm;
	}
	@EventHandler
	public void onChat(PlayerChatEvent event) {
		Player player = event.getPlayer();
		String name = player.getName();
		if (hm.containsKey(name)) {
			if (hm.get(name)) {
				player.performCommand("tc " + event.getMessage());
				event.setCancelled(true);
			}
		}
	}
}

Damit die Main weiss, wo sie denn nach Events suchen soll, müssen wir das Event noch eben registrieren. Das passiert in der onEnable über den PluginManager des Servers.

getServer().getPluginManager().registerEvents(new L_Chat(TeamChatHM), this); 

Und schon haben wir den ersten Listener eingebaut. Bei Listenern ist es genau wie mit CommandExecutoren, man könnte 1000 Events in einen Listener packen, wie man auch 1000 Commands in einen CommandExecutor packen kann. Ob man sich jetzt für jedes Event einen eigenen Listener schreibt oder das in Gruppen macht, ist wie immer ganz euch überlassen. (Theoretisch könnte man die main auch als Listener benutzen, aber das halte ich für keine gute Idee und deshalb geh ich hier auch nicht drauf ein)

ArrayLists

Es gibt Situationen, in denen braucht man keine HashMap - ein Eintrag zu dem Key ist nicht nötig, uns reicht der Key eigentlich vollkommen.

Die HashMap ist an dieser Stelle eigentlich völliger Quatsch. Natürlich funktioniert das, aber eigentlich müssen wir uns doch nur merken, welche Spieler den TeamChat angetoggelt haben - die komplette "false"-Geschichte in der HashMap brauchen wir nicht, uns reicht doch die "true".

Also brauchen wir nur eine Liste von Spielernamen, die sich merkt, wer den TeamChat angeschaltet hat; beim Ausschalten streichen wir den Namen wieder von der Liste und gut is - unser "selbstgebauter Boolean" ^^ Wir könnten jetzt also unsere HashMap durch eine ArrayList ersetzen - muss nicht sein, ihr könnt euch das hier jetzt einfach nur durchlesen. Das bisken, was an Speicherbedarf gespart wird, ist die Mühe kaum wert - aber ich möchte trotzdem drauf eingehen, weils definitiv die effektivere Variante ist und weil es einfach noch häufig vorkommen wird.

Wir erstellen uns in der Main eine leere Liste. Einmal public, damit Command_Executor und Listener drankommen, zudem static, damit beide die selbe benutzen. (Jetzt das mit dem Lolli verstanden? ;) )

Dadurch, dass wir die komplette main an den Command_Executor übergeben (siehe Konstruktor) und die Liste (bisher noch als HashMap) direkt an den Listener übergeben, könnte man auch ohne "public" drauf zugreifen - am Beispiel des CommandExecutors über "plugin.teamChatListe" - weil wir die main als "plugin" definiert haben. Wenn wir die Liste als "static" deklarieren, kommt man auch über "Tutorial.teamChatListe" dran - ganz ohne Konstruktor usw.

public static ArrayList teamChatListe = new ArrayList(); 

in dem Codeteil, der auf '/tc an' reagiert (if (args.length==1 && args[0].equalsIgnoreCase("an"))) {...} im CommandExecutor CE_teamchat) müssten wir also den Teil, in dem wir etwas in ne HashMap packen (plugin.teamChatHM.put(sender.getName(), true); ), ändern in einen Teil, in dem wir etwas in ne ArrayList packen:

Tutorial.teamChatListe.add(sender.getName());

Und in dem Listener dann 'if (hm.containsKey(sender.getName()))' durch 'if (Tutorial.teamChatListe.contains(sender.getName())) {...}' ersetzen. Wir können uns sogar eine Abfrage sparen, denn uns reicht "contains", ein "get(bla)" brauchen wir nicht - den boolean brauchen wir ja gar nicht mehr.. diese if-Schleife kann also weg. Dafür müssen wir jetzt aber beim Entfernen der Liste sicher gehn, dass es den Eintrag denn überhaupt gibt. (Wieder über "contains")

Das Entfernen von der Liste (ersetzt das "plugin.teamChatHM.put(sender.getName(), false);") geschieht durch

if (plugin.teamChatListe.contains(sender.getName()) {
	plugin.teamChatListe.remove(sender.getName())
} 

Mit etwas logischem Denken sollte man es jetzt hinbekommen, die HashMap komplett durch die ArrayList zu ersetzen.


Wenn wir aus der ArrayList einen String machen möchten, erstellen wir uns wieder einen StringBuilder, der aber dieses Mal nicht nur Leerzeichen zwischen die Elemente packt, sondern immer, bis auf das letzte Mal, ein Komma und ein Leerzeichen. Ich benutze dafür jetzt eine Methode von XemsDoom (leicht verändert).

public String arrayListToString(ArrayList<String> playernames){
  
      #Neues StringBuilder Objekt - Sollte man verwenden für String Manipulationen generell
      StringBuilder builder = new StringBuilder();
      
      #Der Zähler und die Grösse des Array, damit wir nicht
      #noch am Ende ein Komma machen.
      int counter = 0;
      int size = playernames.size();

      #Durch die ArrayList durch iterieren
      for(String name : playernames){
          
          #Den Counter hochzählen
          counter++;

          #Nur Komma hinzufügen wenn wir nicht beim letzten Element sind
          if(counter != size)
            builder.append(name + ", ");
          else
            builder.append(name); 

      }          

    #builder zu String
   return builder.toString();

}

Scheduler

Um Verzögerungen einzubauen oder einen Vorgang in regelmäßigen Abständen wiederholen zu lassen, gibt es Scheduler. Scheduler sind tasks, die im Hintergrund darauf warten, im richtigen Moment aktiv zu werden.

Wiederholungen

Hier werden wir einen Serverbroadcast programmieren, der alle zwei Minuten eine Nachricht ausgeben soll. Dazu rufen wir die Methode "scheduleSyncRepeatingTask" auf, (zu finden über getServer().getScheduler()) die die Argumente vom Typ JavaPlugin, Runnable, Long und Long erwartet. An erste Stelle kommt also unsere Instanz des Plugins, das wir da grad schreiben (in der Main also "this", in jeder anderen Klasse das, was wir als plugin im Konstruktor annehmen), die beiden Longs sind vorne der Delay (also die Verzögerung, bevor der Scheduler das erste mal die run-Methode aufruft) und hinten der Abstand zwischen den Wiederholungen. Beide Zeitangaben werden in Serverticks erwartet, das bedeutet im Normalfall 20 pro Sekunde, also 1200 pro Minute. Bei Laggs kann der Wert etwas abweichen, aber das sollte bei unserer Aufgabenstellung recht egal sein.

Die Runnable ist ein Interface, bei der eine Methode (void run()) noch definiert werden muss. In dieser Methode beschreiben wir, was jedes mal wieder passieren soll.

int taskID = getServer().getScheduler().scheduleSyncRepeatingTask(this, new Runnable(){
		public void run(){
			getServer().broadcastMessage("Ich benutze keine Tabulatoren in yml-Dateien!");
		}			
	},1200L, 2400L);
 

So wird also nach einer Minute nach Schedulerstart angefangen, alle zwei Minuten im Chat daran zu erinnern, wieso die Hälfte aller Plugins beim ersten Mal crashen ;) Diesen Codeteil kann man jetzt in einen Befehl packen, oder auch direkt in die onEnable(), einfach dahin, wo ihrs hinhaben wollt.

Als Rückgabe hat diese Methode einen Integerwert, der dem Task eine ID zuweist. Mit Hilfe dieser ID kann man den Task dann stoppen, wenn seine Dienste nicht mehr benötigt werden.


getServer().getScheduler().cancelTask(taskID);

Verzögerungen

Viele PVP-Server möchten gewährleisten, dass sich Spieler nicht feige aus Kämpfen herausteleportieren können. Dazu schreiben wir einen Teleport-Befehl, der mit einer kleinen Verzögerung funktioniert. Bewegt sich der Spieler in dieser Zeit (oder wird getroffen), wird der Teleport-Vorgang abgebrochen. Dazu müssen wir einmal bei der Eingabe des Befehls und einmal sagen wir zwei Sekunden später aktiv werden. Zusätzlich müssen wir im PlayerMoveEvent natürlich prüfen, ob der Spieler sich bewegt hat.

Die Programmierlogik dahinter ist ganz einfach, wir erstellen eine ArrayList für die Spieler, die "auf den Teleport warten". Sind sie am Ende der Wartezeit immernoch in der Liste, werden sie auch teleportiert (und dann natürlich herausgelöscht). Werden sie vorher aus der Liste gelöscht, bricht der Teleport ab.


Der delayedTask funktioniert an sich genauso wie der repeatingTask, nur dass der zweite Longwert natürlich wegfällt. Der Teil im normalen Command-Rahmen:

plugin.teleportArrayList.add(name);
plugin.getServer().getScheduler().scheduleSyncDelayedTask(this, new Runnable() {

	@Override
	public void run() {
		if (plugin.teleportArrayList.contains(name)) {
			player.teleport(loc);
			plugin.teleportArrayList.remove(name);
		}
	}
		
}, 40L);

Wo man die Location hernimmt, sei jedem selbst überlassen.. Um den Spawnpunkt der Welt zu nehmen, zB:

Location loc = player.getWorld().getSpawnLocation();

Und im PlayerMoveEvent prüfen wir jetzt noch jede Bewegung. Wenn der Spieler, der sich bewegt in der Liste ist, wird er herausgelöscht. Der Code muss natürlich in einen Listener.


@EventHandler
public void onMove(PlayerMoveEvent e) {
	String name = e.getPlayer().getName();
	if (plugin.teleportArrayList.contains(name))
		plugin.teleportArrayList.remove(name);
}

config.yml

Es gibt viele Plugins, bei denen es Einstellungen gibt, die von Server zu Server unterschiedlich sein sollen. Wenn man zum Beispiel bei einem Berufe-Plugin den Erfahrungspunkte- oder Geldzuwachs einstellbar machen möchte, ein Zeitintervall eintragen können möchte oder eine Art Starterkit angeben möchte.. auch hier sind dem Entwickler keine Grenzen gesetzt. Über die config.yml lassen sich solche Sachen einstellen, ohne großartig im Quelltext des Plugins rumgraben zu müssen. Wir in unserem Fall schreiben uns jetzt ein Starterkit - entweder in ein eigenes Plugin oder basteln es einfach eben mit ins Tutorial-plugin. Die Kombination aus Spielerheilung, TeamChat und Starterkit macht nur Sinn, wenn man daran dann weiterbastelt, bis man Essentials oder CommandBook oder wie sie alle heissen, ersetzen kann (und selbst dann nicht ^^) - aber es soll ja ohnehin nicht wirklich laufen, es dient ja nur dazu, zu verstehen, wovon ich hier erzähle.

Ich bastel es aus Faulheit also eben mit ins Tutorial. Als Starterkit möchte ich einstellen können:

  • Art der Werkzeuge
  • Art der Rüstung
  • Anzahl und Art der Nahrung
  • Anzahl Fackeln
  • Anzahl Zäune
  • Anzahl Schilder


Dazu erstellen wir erstmal - parallel zur plugin.yml, also ins src - eine config.yml. Diese config.yml ist die Standart-Config, die in der Tutorial.jar mitgeliefert wird. Heisst, wenn man das Plugin anderen Servern zugänglich macht, ist das die Config, mit der das Plugin von vornherein arbeitet. Wie man davon jetzt eine Kopie macht, die im Ordner "plugins/Tutorial" abgelegt wird und vom Admin verändert werden kann, schauen wir uns gleich noch an.

In diese config.yml kommen jetzt auf jeden Fall schonmal alle Einträge, die man veränderbar haben möchte. Wenn man Unterpunkte einstellen möchte, muss man das wie in der plugin.yml über Leerzeichen, nicht über Tabulatoren machen. Wir brauchen jetzt erstmal einen Hauptpunkt namens "Starterkit", damit wir auch andere Sachen in der config einstellen können und es übersichtlich bleibt. Dass "Starterkit" einen oder mehrere Einträge ankündigt, zeigt man der .yml durch einen Doppelpunkt dahinter. Entweder schreibt man jetzt direkt den Eintrag dahinter, wenn "Starterkit" jetzt nur aus einem Int-Wert oder einem String oder so bestehen würde, oder aber man geht eine Zeile darunter, 4 Leerzeichen nach rechts und schreibt den Unterpunkt samt Unterpunkten oder Eintrag. (Jaa, ich hab hier Tabulatoren drin, aber nur, weil sonst die Formatierung flöten geht.. Also nix mit faul kopieren.. abschreiben!) Klingt wild, ist aber gar nicht so tragisch, wenn man nicht versuchen würde, es in Worte zu fassen ;) Am besten zeigt sich das an einem Beispiel: (Zeilen, die mit einer # anfangen, gelten als Kommentare und werden beim Auslesen übergangen)

Starterkit:
#Material angeben: Leder, Kette, Eisen, Gold, Diamant oder "keine" - default: Leder
	Ruestung: Leder
#Material angeben: Holz, Stein, Eisen, Gold, Diamant oder "keine" - default: Stein
	Werkzeug: Eisen
#ID Steaks - 364, Chicken - 366, Melon - 360, Apple - 260, Cookies - 357 - default: 32 Steaks
	Nahrung:
		ID: 364
		Anzahl: 32
#default: 16 Fackeln
	Fackeln:
		Anzahl: 16
#default: 64 Zaeune
	Zaeune:
		Anzahl: 64
#default: 1 Schild
	Schilder:
		Anzahl: 1 

Die Unterpunkte "Anzahl" bei Fackeln, Zäune und Schild könnte man sich natürlich sparen, man könnte auch direkt hinter Fackel, Zäune und Schilder die Zahl schreiben, ohne noch nen Unterpunkt drin zu haben. Ich persönlich habs aber lieber einheitlich, dann muss ich gar nicht drüber nachdenken, ob ich grade bei dem jetzt noch nen Unterpunkt drin hab oder nicht. Aber nötig isset nicht unbedingt.

Damit haben wir jetzt schonmal alles, was wir brauchen - wir müssen nur noch wissen, wie wir die Infos aus der config.yml ziehen und verarbeiten. Am besten ist es, wenn man die Config nicht zwischenspeichert, sondern immer über getConfig() aufruft.

Aber bevor wir überhaupt zu nem Punkt kommen, an dem wir auf die Config zugreifen, müssen wir uns ja erstmal noch nen Rahmen basteln, in dem das Starterkit Sinn macht. Man könnte es über einen Befehl zugänglich machen, dann bastelt man sich nen CommandExecutor dafür und schreibt den Befehl in die plugin.yml oder wir machens direkt mit dem ersten Betreten der Servers - da gibts zwar Plugins, die der default-Gruppe die Rechte für Items nimmt und die Items dann sofort wieder verschwinden, aber davon müssen wir ja jetzt nicht unbedingt ausgehen. Ich finds auf jeden Fall am praktischsten, wenn der Spieler es direkt am Anfang bekommt, dann ist er nicht davon abhängig, ob ein Admin oder Moderator da ist, der ihm das eben zaubert. Also brauchen wir nen neuen Listener - man könnte es zwar in den alten packen, aber wieso unnötig Unordnung schaffen? Wir erstellen also einen Listener namens L_FirstLogin, der implementiert wieder Listener, wie immer. Diesmal übergeben wir beim Registrieren des Events (this), also die main, an den Listener (getServer().getPluginManager().registerEvents(new L_FirstLogin(this), this); ) in der onEnable() der main, lassen uns von Eclipse eben nen Konstruktor zusammenschustern...

public class L_FirstLogin implements Listener{
	public Tutorial plugin;
	public L_FirstLogin(Tutorial tut) {
		plugin = tut;
	}
... 

...und schreiben jetzt unsere Methode, die das Event ankündigt. Das Event heisst - schon wieder son Zufall - PlayerJoinEvent. Das Event greift aber jedesmal, wenn jemand auf den Server kommt. Um zu testen, obs das aller erste mal ist, gibts die Abfrage hasPlayedBefore(). Wenn er also vorher noch nicht gespielt hat, soll er ein Starterkit bekommen, ansonsten nicht.

...
@EventHandler
public void onFirstJoin(PlayerJoinEvent event) {
	Player player = event.getPlayer();
	if (!player.hasPlayedBefore()) {
		setStarterkit();
	}
}

Die Methode setStarterkit() kennt die Klasse noch nicht, also bringen wir sie ihr bei. Das muss zwar nicht unbedingt so aussehen (man könnte alles, was wir in setStarterkit() packen, einfach mit in die if-{} packen - aber es ist viel übersichtlicher, wenn man sich sowas in Methoden aufteilt). Bei nem Mouseover über das Rotgeschlängelte erscheint "create Method" oder sowas ähnliches. Wenn man draufklickt, wird die Methode erstellt (irgendwo weiter unten). Das sieht dann so aus:

private void setStarterkit() {
		// TODO Auto-generated method stub
		
	} 

Das void deutet darauf hin, dass die Methode keinen Rückgabewert hat. Wenn die Methode eine Zahl errechnen soll, muss statt des "void" "int" oder "double" dahin, wenn ein String zurückgegeben werden soll, "String" usw.. (Man kann auch eigene Klassen als Rückgabewert gebrauchen, dazu später mehr) Einen Rückgabewert brauchen wir nicht, es soll nur etwas passieren, also ist es bei uns void.

Da es etwas komplizierter ist, die Werkzeuge und Rüstungen zu verteilen, schreiben wir auch dafür Methoden. Die Nahrung, Fackeln, Zäune und Schilder können wir aber schonmal verteilen. Also kommen wir endlich dazu, auf die config.yml zuzugreifen. Über plugin.getConfig().getXXX (<Pfad als String>) bekommt man, was man vorher eingestellt hat. Die Xse sollen Platzhalter für das sein, was man da raus ziehen möchte. (Vorsicht! Wenn man nen Int darausziehen möchte, aber Text dadrin steht, gibt es zum Beispiel eine NumberFormatException - die fangen wir hier zwar durch die Default-Einstellung ab, aber drüber nachdenken sollte man trotzdem beim Programmieren) Unterpunkte erreicht man hier per Punkt. Also wenn wir wissen wollen, wieviele Zäune verteilt werden sollen, machen wir das über:

plugin.getConfig().getInt("Starterkit.Zaeune.Anzahl") 

Wenn man einen default-Wert einstellen möchte (wir möchten ;) ), also einen Wert, der benutzt wird, wenn in der Config kein Wert dafür festgelegt ist / die Config nicht gefunden werden kann oder sonst irgendetwas schiefgelaufen ist, packt man ihn hinter den Pfad, durch ein Komma getrennt:

plugin.getConfig().getInt("Starterkit.Zaeune.Anzahl", 64) 

Also müssen wir nur noch das Inventar des Spielers manipulieren und zumindest die Zäune sind drin:

player.getInventory().addItem(...) 

ist die Methode zum Hinzufügen eines Items in ein Inventar. Dazu muss man eine neue Instanz der Klasse ItemStack erstellen und angeben, welche ID und wieviele davon in dem Stack sein sollen. Das können wir direkt im addItem() machen:

player.getInventory().addItem(new ItemStack(<ID>, <Anzahl>))  

Jetzt haben wir alles, um setStarterkit() zu Ende zu schreiben.

private void setStarterkit() {	
	setRuestung();
	setWerkzeug();
	player.getInventory().addItem(new ItemStack(plugin.getConfig().getInt("Starterkit.Nahrung.ID", 364), plugin.getConfig().getInt("Starterkit.Nahrung.Anzahl", 32)));
	player.getInventory().addItem(new ItemStack(50, (plugin.getConfig().getInt("Starterkit.Fackeln.Anzahl", 16))));
	player.getInventory().addItem(new ItemStack(85, (plugin.getConfig().getInt("Starterkit.Zaeune.Anzahl", 64))));
	player.getInventory().addItem(new ItemStack(323, (plugin.getConfig().getInt("Starterkit.Schilder.Anzahl", 1))));
}
 

Aber moment, player funktioniert gar nicht? Klar - wir haben ihn der Methode ja auch gar nicht übergeben.. Heisst, wir müssen in der Event-Methode, in die leeren Klammern von setStarterkit() den player reinschreiben:

setStarterkit(player);  

und dann natürlich in der Deklaration der Methode einen Player "annehmen":

private void setStarterkit(Player p) {..} 

Den player müssen wir jetzt natürlich auch den beiden Methoden setRuestung(player); und setWerkzeug(player); übergeben - wenn wir den Mouseover jetzt darüber machen, erstellt Eclipse direkt eine Methode, die einen Player annimmt.

Wir müssen also nur noch Werkzeuge und Rüstung verteilen und das Starterkit läuft. Aber wie machen wir jetzt aus dem String, den wir in der Config angeben, etwas nützliches?

Bei der Rüstung geht das echt super, die IDs liegen in der Liste direkt untereinander. Die ziehen wir dem Spieler auch direkt an - bei uns am Spawn schneits..

private void setRuestung(Player player) {
		
		player.getInventory().setHelmet(new ItemStack(ID, 1));
		player.getInventory().setChestplate(new ItemStack(ID+1, 1));
		player.getInventory().setLeggings(new ItemStack(ID+2, 1));
		player.getInventory().setBoots(new ItemStack(ID+3, 1));
		
	}

Nur was ist die ID? Eine grobe Liste der IDs findet ihr hier. Detaillierter stehts in der Wiki. Wir müssen aus den Begriffen in der Config jetzt die richtigen Integer machen, damit ItemStack damit etwas anfangen kann.

		if (plugin.getConfig().getString("Starterkit.Ruestung").equalsIgnoreCase("keine")) {return;}
		int ID = 298; //default-Einstellung: Lederrüstung
		if (plugin.getConfig().getString("Starterkit.Ruestung").equalsIgnoreCase("Kette")) {ID = 302;}
		if (plugin.getConfig().getString("Starterkit.Ruestung").equalsIgnoreCase("Eisen")) {ID = 306;}
		if (plugin.getConfig().getString("Starterkit.Ruestung").equalsIgnoreCase("Diamant")) {ID = 310;}
		if (plugin.getConfig().getString("Starterkit.Ruestung").equalsIgnoreCase("Gold")) {ID = 314;}
 

Das muss natürlich über den Vorgang des "Anziehens". Sollte "keine" angegeben sein, verlassen wir die Methode und machen nicht weiter - der Spieler bleibt nackt. Dann stellen wir den default-Wert ein, und verändern ihn, wenn der String in der Config "Kette", "Eisen", "Diamant" oder "Gold" ist.

Dann wird der Spieler angezogen und gut ist. Wenn irgendwas angegeben ist, was nicht verstanden wird, bleibt die ID bei 298 und dem Spieler wird eine Lederrüstung angezogen.

Bleibt also nur noch die setWerkzeug-Methode. Die funktioniert leider nicht so schön, da müssen wirs leider "einzeln" machen, was aber auch kein Problem darstellt:


private void setWerkzeug(Player player) {
		if (plugin.getConfig().getString("Starterkit.Werkzeug").equalsIgnoreCase("keine")) {return;}
		int schwert = 272; //default: Stein
		int schaufel = 273;
		int hacke = 274;
		int axt = 275;
		int sense = 291;
		if (plugin.getConfig().getString("Starterkit.Werkzeug").equalsIgnoreCase("Diamant")) {schwert = 276; schaufel = 277; hacke = 278; axt = 279; sense = 293;}
		if (plugin.getConfig().getString("Starterkit.Werkzeug").equalsIgnoreCase("Gold")) {schwert = 283; schaufel = 284; hacke = 285; axt = 286; sense = 294;}
		if (plugin.getConfig().getString("Starterkit.Werkzeug").equalsIgnoreCase("Eisen")) {schwert = 267; schaufel = 256; hacke = 257; axt = 258; sense = 292;}
		if (plugin.getConfig().getString("Starterkit.Werkzeug").equalsIgnoreCase("Holz")) {schwert = 268; schaufel = 269; hacke = 270; axt = 271; sense = 290;}
		
		player.getInventory().addItem(new ItemStack(schwert, 1));
		player.getInventory().addItem(new ItemStack(schaufel, 1));
		player.getInventory().addItem(new ItemStack(hacke, 1));
		player.getInventory().addItem(new ItemStack(axt, 1));
		player.getInventory().addItem(new ItemStack(sense, 1));
}
 

Um die config.yml in den dafür vorgesehenen Standartordner (/plugins/Tutorial/config.yml) zu speichern, damit der Serveradmin auch drankommt, ohne die .jar-Datei auseinandernehmen zu müssen, speichern wir, wenn es die Datei noch nicht gibt, die mitgelieferte (von uns erstellte, im Plugin enthaltene) config.yml mit der von JavaPlugin übergebenen Methode saveDefaultConfig(). Wenn sie existiert, laden wir den Inhalt daraus, und überspeichern sie nicht, da sonst alle vom Admin geänderten Einträge wieder zurückgesetzt werden würden. (Es gibt Plugins, da macht das durchaus Sinn, hier aber nicht.) Dafür benutze ich jetzt eine Methode, die XemsDoom für seine Plugins benutzt. Ich bediene mich an der Stelle hier daran, weil sie es einfach auf den Punkt bringt:

public void loadConfig(){
	config = getConfig();
	config.options().copyDefaults(true);
		
	if(new File("plugins/Tutorial/config.yml").exists()){			
		System.out.println("[Tutorial] config.yml geladen.");	
	}else{
		saveDefaultConfig();
		System.out.println("[Tutorial] config.yml erstellt und geladen.");
	}
}

Damit das funktioniert, muss man sich in der Main noch einen Platzhalter für die config reservieren. Das passiert durch

public FileConfiguration config; 

loadConfig() wird dann in der onEnable() aufgerufen und man kann mit config.getSonstwas("zielort.unterpunkt") alles aus der config.yml ziehen, was man benötigt.

andere Klassen und Plugins einbinden

Immer wieder taucht die Frage auf, wie man jetzt zB in einer CommandExecutor-Klasse an die Config kommt oder Plugins wie PermissionsEx oder WorldEdit einbindet oder ähnliches.

Alles folgt dem selben Prinzip: Damit man eine Variable oder Methode benutzen kann, braucht man eine Instanz (ein klar definiertes Objekt) der Klasse, in der sie (die Variable oder Methode) definiert worden ist. Wir haben zB im Konstruktor vom CommandExecutor vom Heile-Befehl eine Instanz unserer Tutorialklasse übergeben und sie (über den Konstruktor) plugin genannt. Bzw nicht irgendeine, bei der JavaPlugin-Klasse ist es wichtig, die aktive Instanz zu nehmen (this) und keine neue Instanz zu erstellen, da diese dann nicht bei Bukkit angemeldet ist.

Damit kommen wir über plugin. an alle Methoden und Variablen, die in unserer Tutorialklasse oder der Klasse von der sie erbt als public deklariert worden sind. Deshalb funktioniert in dem Fall dann plugin.getConfig(). Diese Methode ist von JavaPlugin geerbt.

Eine neue Instanz einer Klasse erstellt man über den new-Operator, wie wir ihn benutzt haben, um eine Instanz eines CommandExecutors zu erstellen. Im new-Operator wird (einer) der Konstruktor(en) aufgerufen, um die benötigten (Instanz-)variablen zu initialisieren. Wenn kein Konstruktor definiert worden ist, wird der Standartkonstruktor ohne Variablen aufgerufen. Der Compiler erstellt dann automatisch den leeren Konstruktor in der zu instanzierenden Klasse.

MeineKlasse instanzname = new MeineKlasse();

Fremde Plugins einzubinden funktioniert ganz ähnlich, man muss nur die .jar-Datei einbinden, wie man die Craftbukkit.jar hinzugefügt hat. Zu beachten ist dabei wieder, dass die aktive Instanz übernommen werden muss. Das funktioniert über den PluginManager des Servers. Möchten wir also unsere Tutorialklasse in ein anderes Plugin einbauen, muss die Initialisierung unserer Instanz so aussehen:

Tutorial tutorial = (Tutorial) Bukkit.getServer().getPluginManager().getPlugin("Tutorial");

Hierbei castet man zwar wieder von der Superklasse, von der geerbt wird, zur Subklasse, was eigentlich eine instanceof-Abfrage voraussetzt; in dem Fall gibt es jedoch natürlich keine Möglichkeit, dass der Cast schiefgeht.

Anders ist es bei statischen Klassenvariablen und -methoden. Diese sind als static deklariert worden und funktionieren ohne Instanziierung der Klasse, wie

PermissionsEx.getUser("spielername").wasAuchImmerIhrVorhabt();

Codenesting

Immer wieder ist zu sehen, wie Anfänger ihren Code bis zur Unkenntlichkeit nach rechts verschieben. Das ist noch ein recht harmloses Beispiel, aber das Spielchen kann man tatsächlich soweit führen, dass man nicht mehr nur hoch und runter, sondern auch nach links und rechts scrollen muss. al ist einfach ne ArrayList<String> in einer fiktiven CommandExecutorKlasse.


//!!NICHT NACHMACHEN, ABSCHRECKENDES BEISPIEL!!\\

if (sender instanceof Player) {
  Player p = (Player) sender;
  if (p.hasPermission("per.mis.sion")) {
    String name = p.getName();
    if (al.contains(name) {
      if (nochEinBoolean) {
        endlich der Code
      }
    }
  }
}

Wenn man hier für die einzelnen Abfragen jeweils noch eine Fehlermeldung einrichten möchte, wird es schnell recht unübersichtlich:


//!!NICHT NACHMACHEN, ABSCHRECKENDES BEISPIEL!!\\

if (sender instanceof Player) {
  Player p = (Player) sender;
  if (p.hasPermission("per.mis.sion")) {
    String name = p.getName();
    if (al.contains(name) {
      if (nochEinBoolean) {
        //endlich der Code
      }
    } else {
        p.sendMessage(ChatColor.RED + "Du musst dich erst anmelden!");
        return true;
    }
  }else {
      p.sendMessage(ChatColor.RED + "Du nich!");
      return true;
  }
}else {
    System.out.println("Der Befehl ist nur für Spieler!");
    return true;
}

Also ist es sinnvoll, alles, was möglich ist, zu negieren. Das hat gleich mehrere Vorteile. Zum Einen muss man nicht nach der richtigen Klammer suchen, um die passende Fehlermeldung auszugeben, sie steht direkt unter der Abfrage. Dadurch lässt sich eine Abfrage auch viel leichter und schneller kurzzeitig deaktivieren oder komplett entfernen - man muss den Code danach nicht wieder in die richtige Formatierung bringen, man muss die geschweifte Klammer "zu" nicht suchen. Stellt euch nur kurz vor, wie es wäre, die Permission-Abfrage im oberen Code herauszunehmen. Welche Klammer muss ich mit abreissen, welche muss bleiben? Sähe der Code wie folgt aus, wäre diese Frage viel leichter zu beantworten:

if (!(sender instanceof Player)) {
    System.out.println("Der Befehl ist nur für Spieler!");
    return true;
}
Player p = (Player) sender;
if (!p.hasPermission("per.mis.sion")) {
    p.sendMessage(ChatColor.RED + "Du nich!");
    return true;
}
String name = p.getName();
if (!al.contains(name)) {
    p.sendMessage(ChatColor.RED + "Du musst dich erst anmelden!");
    return true;
}
if (!nochEinBoolean) {
    return true;
}
//hier der Code


abstrakte Klassen

Wenn man viele Plugins programmieren möchte, lohnt es sich eventuell, ein Grundplugin zu schreiben, das bestimmte abstrakte Klassen bereithält, um euch Wiederholungsarbeit zu ersparen. Bei jedem Command, der nicht von der Konsole ausgeführt werden kann, muss zB der sender erst überprüft und dann zu einem Player-Objekt gecastet werden. Wenn ihr euch eine abstrakte Klasse namens PlayerCommand schreibt (ob in einem extra Plugin oder einfach mit in einem großen), könnt ihr statt der üblichen Vorgehensweise mit CommandExecutoren eigene Systeme entwickeln, wie mit euren Commands umgegangen wird. Ich zum Beispiel habe mir die folgende abstrakte Klasse erstellt:

public abstract class PlayerCommand implements CommandExecutor {

	public String name;
	@Override
	public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {

		if (!(sender instanceof Player)) {
			System.out.println("Dieser Befehl ist nur für Spieler.");
			return true;
		}
		Player player = (Player) sender;
		name = player.getName();
		return (command(player, cmd, args));
	}
	public abstract boolean command(Player player, Command cmd, String[] args);

	
}


So kann ich mir den normalen CommandExecutor sparen und damit die Abfragen und die Zuweisung von "name" weglassen - das passiert dann jedesmal automatisch bei jeder Command-Klasse, die PlayerCommand extendet. Solche Klassen brauchen dann eine Methode namens command, die genauso funktioniert wie die onCommand-Methode vom CommandExecutor, nur ohne, dass das CommandLabel extra nochmal mitgegeben wird und man die sechs Zeilen Code eintippen muss.

Die Möglichkeiten, euch dadurch Mehrarbeit zu ersparen, sind endlos, das sollte man immer vor Augen haben. ("Zauber"-Klasse für Befehle, bei denen eine Art "Mana" abgezogen wird, "MyPlugin"-Klasse, um automatisch Config-Dateien zu laden oder extra-Methoden benutzen zu können, für jeden Befehl automatisch die Permission-Message einrichten, und und und...)

enum

Auch der Datentyp "enumerated", kurz enum, ist ein Datentyp, der so manche Aufgaben bei der Plugin-Programmierung angenehmer macht. In einem enum verbindet man logisch zusammenhängende Konstanten miteinander zu einer Gruppe. Man benutzt sie in der Plugin-Programmierung andauernd, wie zB bei ChatColor oder Material. Die Konstanten schreibt man üblicherweise in Großbuchstaben. Das praktische ist, man kann Methoden für diese Konstantengruppen schreiben und in Verbindung mit den Konstruktoren so bestimmte Werte für sie festlegen und abfragen. Klingt wild, erklärt sich an einem Beispiel jedoch viel besser. Nehmen wir an, ihr wollt euch ein Berufesystem schreiben. Dann lohnt es sich, ein enum namens Beruf zu erstellen:

public enum Beruf {
	BAUMEISTER, 
	HÄNDLER, 
	KÄMPFER, 
	MINENARBEITER, 
	ZAUBERER;
}

So kann man zB statt einer HashMap<String, String> für den Spielernamen und Beruf "direkt" eine HashMap<String, Beruf> erstellen. Das eliminiert Schreibfehler und hilft stark bei der Übersichtlichkeit. Zusätzlich kann man das enum jetzt noch ausbauen, indem man Variablen definiert und Methoden schreibt. Wir nehmen hier das Beispiel eines Chat-Präfixes, den ihr euch dann in den Chatteil bauen könntet. Dazu schreiben wir uns einen Konstruktor, der einen String namens prefix initialisiert und eine Methode, die uns den auch zurückgibt.

public enum Beruf {
	BAUMEISTER("[B]"), 
	HÄNDLER("[H]"), 
	KÄMPFER("[K]"), 
	MINENARBEITER("[M]"), 
	ZAUBERER("[Z]");

private String prefix;

Beruf(String prefix) {
	this.prefix = prefix;
}
public String getPrefix() {
	return prefix;
}

Wie ihr sehen könnt, müsst ihr dann bei der Definition der Konstanten den Konstruktor "bedienen". Man kann aber verschiedene Konstruktoren schreiben und in manchen Dingen macht das auch Sinn.

Um dann den Präfix abzurufen, könnt ihr entweder, wenn ihr eine Berufsvariable habt (wie in der o.g. HashMap zB), beruf.getPrefix() benutzen und ihr bekommt immer den richtigen Präfix zurück. Auch den speziellen Präfix eines bestimmten Berufes kann man so immer abrufen: Beruf.BAUMEISTER.getPrefix().

Auch hierbei sind die Möglichkeiten unbegrenzt, euch die Arbeit übersichtlicher zu machen und generell zu vereinfachen.

Speicherfunktion

Aber man möchte ja nicht nur die config.yml speichern. Es gibt sehr häufig Dinge, die man sich über einen Serverneustart hinaus merken möchte, die man nicht konfigurieren möchte. Es gibt dazu im Internet einige Methoden zu finden, die allesamt irgendwie funktionieren, aber wirklich übersichtlich ist das fast nie. Hier möchte ich jetzt ein paar Methoden aufzeigen, die man benutzen könnte.

MySQL

MySQL ist ein Datenbanksystem, das auch nicht viel schwerer zu verstehen ist, aber dafür ungleich mehr Möglichkeiten bietet. Zum Beispiel lässt sich die Datenbank mit Homepages verbinden, so dass Spielfortschritte und Statistiken auch auf einer Homepage angezeigt werden können. Auf sämtliche Möglichkeiten von MySQL werde ich nicht eingehen und auch nicht erklären, wieso welcher Befehl funktioniert. Die gängigen Suchmaschinen bieten da ausreichend Informationen, um dieses recht leicht verständliche, aber sehr umfangreiche Datenbankverwaltungssystem benutzen zu können.

Der Aufbau ist im ersten Moment zwar etwas abschreckend, man gewöhnt sich jedoch recht schnell daran und sobald die Methoden einmal geschrieben sind, sind Speicher- und Sortiervorgänge auf einmal ganz unkompliziert.

Zunächst erstellen wir uns eine eigene Klasse für die Methoden, die wir verwenden. In welches Package ihr die packt, ist recht egal, nur müssen die Importe stimmen. Ich nenne diese Klasse MySQL und hab sie in einem extra-Serverplugin, das von allen meinen anderen Plugins angesprochen wird. Dazu aber später mehr - hier im Tutorial reichts dicke, wenn ihr die einfach mit ins Hauptpackage packt.

Kommentar
Normalerweise braucht man den JDBC-Connector für MySQL, dieser ist jedoch schon in der Craftbukkit enthalten, so dass wir direkt loslegen können.

Connection

In diese Klasse erstellen wir zunächst nur ein Objekt der Klasse Connection (Import: java.sql.Connection), zum initialisieren schreiben wir noch Methoden.

public class MySQL {
  public Connection con;
}

Diese Connection ist das Objekt, das jeglichen Zugriff auf die Datenbank ermöglicht. Damit das aber funktioniert, muss con natürlich erst noch initialisiert werden. Momentan ist ja nur der Platzhalter, aber kein Wert dafür definiert. Dafür schreiben wir jetzt eine Methode namens "connect". Damit wir uns verbinden können, muss der Treiber aber erstmal "von Hand" geladen werden. Das passiert durch die Methode

Class.forName("com.mysql.jdbc.Driver");

Da diese Methode aber eine ClassNotFoundException wirft, müssen wir die durch einen try-catch-Block abfangen:

try {
    Class.forName("com.mysql.jdbc.Driver");
} catch(ClassNotFoundException e) {
    System.err.println("Konnte den MySQL-Driver nicht finden!");
    return;
}
        

Soo, der Treiber ist geladen, also können wir uns vom DriverManager eine Connection zuweisen lassen:

try {
    con = DriverManager.getConnection("jdbc:mysql://" + host + ":" + port + "/" + database + "?user=" + user + "&password=" + password + "&autoReconnect=true");
    if(!(con.isClosed())) {
        System.out.println("Erfolgreich mit MySQL verbunden!");
    }
} catch(SQLException e) {
    System.err.println("Konnte nicht mit MySQL verbinden! Error: " + e.getMessage());
    return;
}

Die Werte wie host, port und alles müssen natürlich irgendwie ersetzt werden. Für eigene Plugins würde theoretisch das direkte Eintragen der Werte helfen, es bietet sich aber an, dafür ne Config zu erstellen und die Werte direkt beim Methodenaufruf mitzuübergeben. So machen wirs jetzt auch, so dass unser Methodenkopf 5 Strings annehmen muss. Die komplette connect-Methode sollte jetzt also so aussehen:

public void connect(String host, String port, String database, String user, String password) {
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch(ClassNotFoundException e) {
            System.err.println("Konnte den MySQL-Driver nicht finden!");
            return;
        }
        
        try {
            con = DriverManager.getConnection("jdbc:mysql://" + host + ":" + port + "/" + database + "?user=" + user + "&password=" + password + "&autoReconnect=true");
            if(!(con.isClosed())) {
                System.out.println("Erfolgreich mit MySQL verbunden!");
                return;
            }
        } catch(SQLException e) {
            System.err.println("Konnte nicht mit MySQL verbinden! Error: " + e.getMessage());
            return;
        }
    }

Damit wir diese Methode jetzt auch aufrufen können, müssen wir zuerst eine Instanz der neuen MySQL-Klasse erstellen, damit das Plugin damit auch arbeiten kann. Dafür reservieren wir in der Hauptklasse zunächst einen Platzhalter und erstellen uns in der onEnable() eine Instanz zur Initialisierung. Diese Datenbank deklarieren wir als Klassenvariable (static), damit wir auch ohne Instanziierung der Plugin-Klasse darankommen können.

public static MySQL datenbank;
private String host, port, database, user, password;

public void onEnable() {
    loadConfig();
    
    host = config.getString("host");
    port = config.getString("port");
    database = config.getString("database");
    user = config.getString("user");
    password = config.getString("password");
    
    datenbank = new MySQL();
    datenbank.connect(host, port, database, user, password);
}



Viele Plugins machen aber gar keinen Sinn, wenn keine Connection gefunden wurde. Wenn zB die gespeicherten Orte für ein Warp-Plugin nicht gefunden werden können, wird jeder Befehl des Plugins wieder einen neuen Fehler werfen. Also empfiehlt es sich, in der connect-Methode zurückzugeben, ob denn die Connection zustande gekommen ist, oder nicht, um das Plugin gegebenenfalls wieder deaktivieren zu können. Dafür müssen wir nur den Rückgabetyp der Methode in "boolean" verändern und bei allen return den jeweils passenden Wahrheitswert hinzufügen. (Nur unter "Erfolgreich..." return true hinzufügen, alle anderen return false.. klar, oder?)

Und anstelle des einfachen Methodenaufrufs fragen wir jetzt ab, ob die Connection erfolgreich war. Wenn sie es nicht war, geben wir möglichst viele Informationen über den Fehler und seine Behebung aus und deaktivieren das Plugin wieder.

if (!datenbank.connect(host, port, database, user, password)) {
	System.out.println("[" + getDescription().getName() + "] muss deaktiviert werden. " +
		"Fülle die config.yml aus und sorge für ausreichende Rechte des Datenbankbenutzers.");
	Bukkit.getPluginManager().disablePlugin(this);
	return;
}
		

Die Connection muss beim Herunterfahren auch wieder geschlossen werden. Wenn sie zu dem Zeitpunkt aber nicht aktiv ist, würde das wieder Fehler werfen. Deshalb müssen wir überprüfen, ob die Verbindung grade besteht oder nicht, bevor wir sie schließen. Der Methodenaufruf kommt natürlich in die onDisable() über datenbank.close();, die Methode selbst natürlich in die MySQL-Klasse.

public void close() {
    try {
        if(con != null && (!(con.isClosed()))) {
            con.close();
            if(con.isClosed()) {
                System.out.println("MySQL-Verbindung geschlossen.");
            }
        }
    } catch(SQLException e) {
        System.err.println("Fehler beim Schliessen der MySQL-Verbindung.");
    }
}
    

Tabellen

Sooo viel Vorbereitung und die Klasse kann immernoch nix nützliches. Aber immerhin haben wir jetzt eine Verbindung zur Datenbank aufgebaut, mit der wir arbeiten können, ohne dass uns die Fehler um die Ohren fliegen. Was wir als nächstes brauchen, ist die Datenbankstruktur selbst - zu Testzwecken (mit lokalem Test-Spielserver) bräuchten wir sogar eine lokale Datenbank (Suchmaschine benutzen, XAMPP installieren, Apache und MySQL starten). Auf jeden Fall brauchen wir in der Datenbank eine Tabellenstruktur. Wenn wir bei dem Beispiel des Warpsystems bleiben, erstellen wir eine Tabelle namens "locations" mit den Einträgen "weltname","x", "y", "z", "pitch", "yaw" und "warpname".

Kommentar
Je genauer man die Einträge "beschreibt", desto weniger Speicherplatz wird verbraucht und desto effektiver kann man mit MySQL arbeiten.

Wir schreiben uns also eine Methode namens createTables(), die einen Befehl an die Datenbank sendet, um die benötigte(n) Tabelle(n) zu erstellen, falls sie noch nicht existieren. Für jede weitere Tabelle müssen wir natürlich nur das nächste Update abschicken.

private boolean createTables() {
    	try {
    		con.createStatement().executeUpdate("CREATE TABLE IF NOT EXISTS `locations` " +
			"(id_warp INT NOT NULL AUTO_INCREMENT PRIMARY KEY, " +
			"weltname VARCHAR(20), " +
			"warpname VARCHAR(16), " +
			"x DOUBLE(11), " +
			"y DOUBLE(11), " +
			"z DOUBLE(11), " +
			"pitch FLOAT(11), " +
			"yaw FLOAT(11)" +
			")");

		return true;
   		
	} catch (SQLException e) {
		System.err.println("Fehler beim Erstellen der Tabelle!");
		e.printStackTrace();
		return false;
	}
		
    }

Auch hier hab ich den boolean als Rückgabewert gewählt, um herauszufinden, ob das Datenbanksystem nach dem Erstellen der Tabellen tatsächlich einsatzbereit ist. Es hat genauso wenig Sinn, das Plugin starten zu lassen, wenn es einen Fehler beim Erstellen der Tabellen gab, wie als wenn die Connection erst gar nicht zustande gekommen ist.

Die Tabellen werden ohnehin nur erstellt, wenn es sie noch nicht gibt, daher kann man ohne Bedenken jedes mal, wenn die Connection erstellt wird, die Methode aufrufen. Also anstelle "true" zurückzugeben in der connect-Methode, geben wir das Ergebnis der createTables()-Methode zurück:

try {
    con = DriverManager.getConnection("jdbc:mysql://" + 
        host + ":" + port + "/" + database + 
        "?user=" + user + "&password=" + password +
        "&autoReconnect=true");
    if(!(con.isClosed())) {
        System.out.println("Erfolgreich mit MySQL verbunden!");
        return createTables();
    }
} catch(SQLException e) {
    System.err.println("Konnte nicht mit MySQL verbinden! Error: " + e.getMessage());
    return false;
}

Updates

So, endlich haben wir alles, um uns die Methoden schreiben zu können, die uns auch wirklichen Nutzen bringen.

Wie ihr oben schon sehen konntet, erstellt man Tabellen über die Methode executeUpdate(). Updates sind generell alle Veränderungen an der Datenbank. Ob man Daten eintragen oder bestehende Daten ändern möchte, neue Tabellen erstellen oder bestehende Tabellen erweitern.. Dabei müssen immer Updates ausgeführt werden.

    public void doUpdate(String sql) {
    	try {
    		con.createStatement().executeUpdate(sql);
    	}catch (SQLException e) {
    		System.err.println("Konnte das Update ("+sql+") nicht ausführen!");
    	}
    }
    

Um jetzt Daten in die Datenbank abzulegen müssen wir also die Methode aufrufen und einen String übergeben, mit dem MySQL etwas anfangen kann. Der String hierfür ist

String sql = "INSERT INTO `locations`(`warpname`,`weltname`, `x`, `y`, `z`, `pitch`, `yaw`) VALUES " +
                "('"+warpname+"','"+weltname+"',"+x+","+y+","+z+","+pitch+","+yaw+")";

Natürlich muss man die ganzen Variablen vorher noch definieren, aber das solltet ihr mit Hilfe der letzten Kapitel allein hinbekommen.

Queries und ResultSets

Abfragen, die nur Daten aus der Datenbank herausholen, nennen sich Queries. Queries liefern ResultSets, die Ausschnitte der Tabelle zurückgeben, die man, je nach Abfrage, ganz nach seinen Wünschen sortiert abfragt. Dadurch spart man sich komplizierte Algorithmen zum erstellen von Top10-Ausgaben und ähnlichem.

    public ResultSet doQuery(String sql) {
        try {
            return con.createStatement().executeQuery(sql);
        } catch(SQLException e) {
            System.err.println("Konnte das Query ("+sql+") nicht ausführen!");
        }
        return null;
    }

ResultSets dürfen erst angesprochen werden, wenn die Methode .next() aufgerufen wird, ansonsten gibts ne Exception, weil der "Cursor" grade noch nicht auf die erste Zeile im ResultSet zeigt. Diese Methode liefert alle Zeilen des ResultSets nacheinander und positioniert den Cursor danach auf die erste Zeile. Spricht man sie also in einer while-Schleife an, wird Zeile für Zeile, also Datengruppe für Datengruppe ausgegeben. Wir holen uns jetzt also die Locations zurück aus der Datenbank per folgendem Query:

public HashMap<String, Location> getLocations() {
    	String sql = "SELECT * FROM `locations`";
    	HashMap<String, Location> hm = new HashMap<String, Location>();
        try {
            Statement stmt = con.createStatement();
            ResultSet rs = stmt.executeQuery(sql);
            while (rs.next()) {
            	String warpname = rs.getString("warpname");
            	String welt = rs.getString("weltname");
            	double x = rs.getDouble("x");
            	double y = rs.getDouble("y");
            	double z = rs.getDouble("z");
            	float pitch = rs.getFloat("pitch");
                float yaw = rs.getFloat("yaw");
                Location loc = new Location(Bukkit.getWorld(welt), x, y, z, pitch, yaw);
            	
                hm.put(warpname, loc);
            }
            
        } catch(SQLException e) {
            System.err.println("Konnte " + sql + " nicht ausführen!");
            e.printStackTrace();
        }
        return hm;
    }
    

Updates

Die Craftbukkit-API entwickelt sich ja - gott sei dank - weiter. Wenn ich mir überlege, wie kompliziert es Anfang des Jahres 2012 noch war, eine Config-Datei zu erstellen, ist das auch wirklich mehr als gut so. Das Problem ist, unsere Plugins müssen auf dem neusten Stand gehalten werden. Wenn sich die API ändern sollte, funktionieren auf einmal irgendwelche Sachen nicht mehr, werfen Exceptions, lassen das Plugin erst gar nicht laden - oder führen im schlimmsten Fall zum Servercrash. So wurden zum Beispiel alle Plugins, die Listener benutzt haben vor kurzem zu Fehlerkanonen, weil es früher heissen musste "extends PlayerListener" oder "extends BlockListener" - mittlerweile ist es nur "implements Listener" - für alle Fälle.

Das heisst, sobald eine neue CraftBukkit-Version auf dem Server läuft, müssen wir unsere CraftBukkit-API daran anpassen. Dazu gehen wir wie im Punkt 3.2 vor und ersetzen die CraftBukkit, die da angegeben ist. Wenn sich der Compiler dann nicht beschwert, hat man evtl Glück und muss nichts an dem Plugin verändern. Bei größeren Updates muss man sich dann aber wieder mit der Dokumentation auseinander setzen, um sein Plugin wieder lauffähig zu bekommen.

Fehlersuche

Wenn man programmiert, kommt man zwangsläufig an einen Punkt, an dem man Fehler macht (wenn nicht, solltet ihr euch untersuchen lassen, normal wär das nicht..). Eclipse beschwert sich zwar nicht unbedingt, aber trotzdem gibts Exceptions, wenn bestimmte Sachen passieren. Ich werd versuchen hier die häufigsten Fehler mal "nachzustellen" und aufzuzeigen, weshalb sie auftreten und wie man sie umgeht.

Die server.log-Datei des Servers gibt immer genau an, wo was falsch ist. (zumindest wenns Syntaxprobleme sind - wenn die Logik im Plugin nicht stimmt, kann auch die server.log nicht helfen)

Plugin lädt überhaupt nicht

Sonderzeichen/Umlaute in der plugin.yml

Ein Klassiker! Man hat in der Description des Plugins oder eines Commands ein Sonderzeichen oder Umlaut benutzt. Die Serverlog sagt uns in der Fehlermeldung genau, wo wir suchen müssen. Hier, Position

2012-06-06 06:55:53 [SEVERE] Could not load 'plugins\TeamChat.jar' in folder 'plugins' 
org.bukkit.plugin.InvalidDescriptionException: Invalid plugin.yml
at org.bukkit.plugin.java.JavaPluginLoader.getPluginDescription(JavaPluginLoader.java:204)
at org.bukkit.plugin.SimplePluginManager.loadPlugins(SimplePluginManager.java:132)
at org.bukkit.craftbukkit.CraftServer.loadPlugins(CraftServer.java:213)
at org.bukkit.craftbukkit.CraftServer.<init>(CraftServer.java:189)
at net.minecraft.server.ServerConfigurationManager.<init>(ServerConfigurationManager.java:53)
at net.minecraft.server.MinecraftServer.init(MinecraftServer.java:166)
at net.minecraft.server.MinecraftServer.run(MinecraftServer.java:432)
at net.minecraft.server.ThreadServerApplication.run(SourceFile:492)
Caused by: unacceptable character '?' (0xFFFD) special characters are not allowed
in "<reader>", position 847
at org.yaml.snakeyaml.reader.StreamReader.checkPrintable(StreamReader.java:98)
at org.yaml.snakeyaml.reader.StreamReader.update(StreamReader.java:191)
at org.yaml.snakeyaml.reader.StreamReader.<init>(StreamReader.java:63)
at org.yaml.snakeyaml.Yaml.load(Yaml.java:411)
at org.bukkit.plugin.PluginDescriptionFile.<init>(PluginDescriptionFile.java:42)
at org.bukkit.plugin.java.JavaPluginLoader.getPluginDescription(JavaPluginLoader.java:199)
... 7 more

extends PlayerListener

Man hat ein veraltetes Tutorial benutzt und benutzt für den Listener nicht "implements Listener":

2012-06-05 21:38:04 [SEVERE] Could not load 'plugins/MagicCarpet.jar' in folder 'plugins' 
org.bukkit.plugin.InvalidPluginException: java.lang.NoClassDefFoundError: org/bukkit/event/player/PlayerListener
at org.bukkit.plugin.java.JavaPluginLoader.loadPlugin(JavaPluginLoader.java:149)
at org.bukkit.plugin.SimplePluginManager.loadPlugin(SimplePluginManager.java:305)
at org.bukkit.plugin.SimplePluginManager.loadPlugins(SimplePluginManager.java:230)
at org.bukkit.craftbukkit.CraftServer.loadPlugins(CraftServer.java:213)
at org.bukkit.craftbukkit.CraftServer.<init>(CraftServer.java:189)
at net.minecraft.server.ServerConfigurationManager.<init>(ServerConfigurationManager.java:53)
at net.minecraft.server.MinecraftServer.init(MinecraftServer.java:166)
at net.minecraft.server.MinecraftServer.run(MinecraftServer.java:432)
at net.minecraft.server.ThreadServerApplication.run(SourceFile:492)
Caused by: java.lang.NoClassDefFoundError: org/bukkit/event/player/PlayerListener
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(Unknown Source)
at java.security.SecureClassLoader.defineClass(Unknown Source)
at java.net.URLClassLoader.defineClass(Unknown Source)
at java.net.URLClassLoader.access$100(Unknown Source)
at java.net.URLClassLoader$1.run(Unknown Source)
at java.net.URLClassLoader$1.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(Unknown Source)
at org.bukkit.plugin.java.PluginClassLoader.findClass(PluginClassLoader.java:41)
at org.bukkit.plugin.java.PluginClassLoader.findClass(PluginClassLoader.java:29)
at java.lang.ClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at com.Android.magiccarpet.MagicCarpet.<init>(MagicCarpet.java:47)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
at java.lang.reflect.Constructor.newInstance(Unknown Source)
at org.bukkit.plugin.java.JavaPluginLoader.loadPlugin(JavaPluginLoader.java:145)
... 8 more
Caused by: java.lang.ClassNotFoundException: org.bukkit.event.player.PlayerListener
at java.net.URLClassLoader$1.run(Unknown Source)
at java.net.URLClassLoader$1.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(Unknown Source)
at org.bukkit.plugin.java.PluginClassLoader.findClass(PluginClassLoader.java:41)
at org.bukkit.plugin.java.PluginClassLoader.findClass(PluginClassLoader.java:29)
at java.lang.ClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
... 27 more

Befehl funktioniert nicht (ohne Fehlermeldung in der server.log)

Fehlermeldung: /<Befehl> (im Chatfenster)

mögliche Fehler:

  • Die Logik im Befehl ist nicht ganz richtig: Irgendeine if-Bedingung führt dazu, dass der Code-Teil mit "return true;" übersprungen wird.
  • Unstimmigkeiten in der plugin.yml: Man hat sich beim Befehl selbst verschrieben. Der angemeldete Befehl stimmt nicht mit dem Befehl überein, auf den im Plugin reagiert wird.


Fehlermeldung: You don't have permission to do that. (oder die eingestellte Permission-Nachricht)

mögliche Fehler:

  • Die in der plugin.yml angegebene Permissionsnode stimmt nicht mit der in der permissions.yml angegebenen überein oder wurde komplett vergessen.

Befehl funktioniert (oder auch nicht), aber mit Fehlermeldung in der server.log

NullPointerException

Man greift auf etwas zu, was nicht initialisiert wurde - zum Beispiel einen Spieler, der keiner ist (im Befehl verschrieben), eine Variable, die bisher nur einen Platzhalter, aber keinen Inhalt bekommen hat oder ähnliches. Auch hier sagt die Serverlog genau, wo wir danach gucken müssen (hier: Zeile 756 in der Klasse AdminBefehle):

2012-05-31 15:17:04 [SEVERE] null
org.bukkit.command.CommandException: Unhandled exception executing command 'tpab' in plugin AdminBefehle v1.6
at org.bukkit.command.PluginCommand.execute(PluginCommand.java:42)
at org.bukkit.command.SimpleCommandMap.dispatch(SimpleCommandMap.java:166)
at org.bukkit.craftbukkit.CraftServer.dispatchCommand(CraftServer.java:479)
at net.minecraft.server.NetServerHandler.handleCommand(NetServerHandler.java:821)
at net.minecraft.server.NetServerHandler.chat(NetServerHandler.java:781)
at net.minecraft.server.NetServerHandler.a(NetServerHandler.java:764)
at org.getspout.spout.SpoutNetServerHandler.a(SpoutNetServerHandler.java:103)
at net.minecraft.server.Packet3Chat.handle(Packet3Chat.java:34)
at net.minecraft.server.NetworkManager.b(NetworkManager.java:229)
at net.minecraft.server.NetServerHandler.a(NetServerHandler.java:113)
at org.getspout.spout.SpoutNetServerHandler.a(SpoutNetServerHandler.java:169)
at net.minecraft.server.NetworkListenThread.a(NetworkListenThread.java:78)
at net.minecraft.server.MinecraftServer.w(MinecraftServer.java:567)
at net.minecraft.server.MinecraftServer.run(MinecraftServer.java:459)
at net.minecraft.server.ThreadServerApplication.run(SourceFile:492)
Caused by: java.lang.NullPointerException
at me.stuppsman.adminBefehle.AdminBefehle.onCommand(AdminBefehle.java:756)
at org.bukkit.command.PluginCommand.execute(PluginCommand.java:40)
... 14 more

Hier fehlen noch einige Standard-Fehler, wenn ihr grad welche zur Hand habt, schreibt sie bitte mit dazu.

Dieses Tutorial basiert auf der CraftBukkit-Version: 1.5.2
Dieses Tutorial wurde erstellt durch: Stuppsman Dieses Tutorial wurde erstellt am: 12.06.2012
Zuletzt bearbeitet durch: Stuppsman Letzte Aktualisierung am: 28.06.2012