Bukkit / Spigot - Packets abfangen (eingehend + ausgehend)

  • Hey!


    So ich bin hier noch relativ neu, aber ich hab mir gedacht, dass ich hier auch mal ein Tutorial verfasse.


    ACHTUNG: Dieses Tutorial ist NICHT für Anfänger gedacht.


    Achtung: Dies dieses Tutorial enhält komplizierte Satzstrukturen mit vielen Nebensätzen und Objekten, um es so kurz wie möglich zu halten. Also: Am besten 2x lesen :)


    Ebenfalls beleuchte ich hier lediglich das Prinzip! Weiteres soll von euch gemacht werden. Ich werde am Ende auch eine kleine Aufgabe geben.


    Vorraussetzungen (darauf werde ich nicht weiter eingehen):

    • Grundlegende Kenntnisse mit der Bukkit-/Spigot API
    • Gute Java Kenntnisse (Syntax, Interfaces, ...)
    • Reflection Kentnisse
    • Kenntnisse zu Packets (Bukkit/Spigot) - Nur für Teil 2
    • Objektorientierung - Nur für Teil 2


    Teile:
    1. Eingehende Packets abfangen (PacketPlayInUseEntity)
    2. Ausgehende Packets abfangen (PacketPlayOutExplosion)


    Teil 1: Eingehende Packets abfangen


    Das Prinzip:
    Erarbeiten wir uns zunächst den Weg, das ein eingehendes Packet zurücklegt an dem Beispiel des UseEntity Packets(Rechtsklick/Linksklick auf Entity).

    • Spieler schlägt ein Entity
    • Spieler sendet das Packet an den Server (Internet, klar? :D)
    • Server: Oh toll! Ein neues Packet. Geben wir das mal an unsere IO Software (bei Bukkit/Spigot Netty) weiter!
    • Netty findet das Packet auf und durchläuft folgende "Stadien" (Pipeline-Prinzip):

      • timeout
      • legacy_query
      • splitter
      • decoder -> Wird interessant für uns
      • prepender
      • encoder
      • packet_handler


    • Bukkit/Spigot bearbeitet das Packet


    Jetzt müssen wir uns irgendwo einhaken. Das mache ich meißtens nach dem "decoder".
    D.h Wir warten bis ein Packet es bis zum "decoder" in der Pipeline schafft und bearbeiten es dann. Dazu fügen wir nach dem "decoder" einen sogenannten "ChannelHandler" ein.


    So genug langweiliges Prinzip. Auf zum Praktischen.
    Die Pipeline wird in Netty durch das Interface "ChannelPipeline" representiert. Eine Instanz davon bekommen wir durch die im NetworkManager befindliche Channel-Instanz mit der Methode "pipeline()". Der NetworkManager befindet sich in der PlayerConnection der EntityPlayer Instanz des Spielers, die sich durch die Methode "getHandle()" in der Klasse CraftPlayer bekommen lässt.


    Die Channel-Instanz ist dabei in einem privaten Feld gespeichert. Daher müssen wir Reflection verwenden, um diese zu bekommen.
    Dieses Field werden wir statisch speichern, um es dann immer wieder aufrufen zu können.


    Dann werden wir im Statischen Initialisierungsblock, welcher bei erstem Aufruf der Klasse aufgerufen wird, die Variable initialisieren. Dazu iterieren wir durch alle Felder der NetworkManager-Klasse und überprüfen, ob sie von der Channel-Klasse bestimmbar ist.


    Also:


    Erstellen wir uns nun das Interface "PacketReceivingHandler", welches eine Methode verlangen wird, die ein Packet und eine Player-Instanz verlangt, die dann aufgerufen wird, wenn das Packet angekommen ist.
    Ebenfalls muss ein "boolean" gereturnt werden, welcher anzeigt, ob das Packet weitergesendet werden soll:

    Code
    1. public static interface PacketReceivingHandler {
    2. public boolean handle(Player p, PacketPlayInUseEntity packet);
    3. }


    Nun haben wir unser Channel-Field. Erstellen wir nun den Methodenbody, der eine Player-Instanz und eine Instanz des PacketReceivingHandler Interfaces.
    Dann noch dazu eine Methode um den Netty-Channel zu bekommen.


    Nun können wir über die Methode ChannelPipeline#addAfter(String baseName, String name, ChannelHandler handler) unseren "Listener" einfügen. Dazu müssen wir eine ChannelHandler-Objekt übergeben. Als ChannelHandler verwenden wir einen MessageToMessageDecoder, der ChannelHandler implementiert. Dieser benötigt ein Generic, für das wir Packet (aus net.minecraft.server.[VERSION]) einsetzten.
    Da es eine Abstrakte Klasse ist müssen/können wir eine innere anonyme Klasse erstellen (klar, eine normale tuts auch).

    Code
    1. ChannelHandler handle = new MessageToMessageDecoder<Packet>() {
    2. @Override
    3. protected void decode(ChannelHandlerContext chc, Packet packet, List<Object> out) throws Exception {
    4. //...
    5. }
    6. };
    7. pipe.addAfter("decoder", //Wie oben erwähnt nach dem decoder
    8. "listener", //Channel Name -> DARF NICHT DOPPELT VORKOMMEN!
    9. handle); //Unser zuvor erstelltes ChannelHandler Objekt
    10. return handle; //Zum schließen des Listeners


    Nun wird immer wenn ein Packet empfangen wird die "decode" Methode aufgerufen. Um das Packet auf seinen Weg weiterzuschicken fügen wir es zu der out-Liste hinzu. Wenn nicht dann nicht.
    Wenn die Methode jetzt ankommt überprüfen wir, ob das packet eine Instanz von PacketPlayInUseEntity ist. Wenn ja rufen wir die "handle" Methode im "PacketReceivingHandler" auf und leiten das Packet je nach dem, was es zurückgibt weiter oder nicht.

    Code
    1. if(packet instanceof PacketPlayInUseEntity) {
    2. if(!handler.handle(p, (PacketPlayInUseEntity) packet)) { //Wenn es false zurückgibt (wenn es weitergeleitet werden soll)
    3. out.add(packet); //weiterleiten
    4. }
    5. return; //Fertig
    6. }
    7. out.add(packet); //Wenn es keine Instanz ist trotzdem weiterleiten, sonst würde nichts mehr auf dem Server laufen.


    So. Wie jetzt der ein oder andere gemerkt hat returnen wir den ChannelHandler bei der listen Methode. Warum? Ganz einfach: Zum schließen:


    So. Das wäre es jeztz dafür. Zur Anwendung einfach z.B folgendes:


    /-\
    | |
    | |
    Bitte den Code nicht verwenden, ist nur für 1 Spieler Server gedacht :D Lieber in eine HashMap<String, ChannelHandler> speichern.
    [/code]


    Jezt nochmal die ganze Methode:


    -> Das PacketPlayInUseEntity ist nur ein Beispiel. Es kann jedes anderes verwendet werden. Alle Packets sind hier aufgelistet.


    Teil 2: Ausgehende Packets abfangen - Some Credits to Janhektor
    Mal wieder erstmal das Prinzip:
    Wie man wissen sollte für diesen Teil sendet man die Packets über die PlayerConnection#sendPacket Methode. Diese verwendet ebenfalls Spigot. Man kann es ahnen:
    Ich will diese Methode überschreiben. Aber wie macht man das am besten? PlayerConnection ist eine Klasse, die (wie man wissen sollte) eine andere Klasse erben kann mit all ihren Methoden.
    Und genau das machen wir uns zu nutzen:
    Vorgehensweise:

    • Neue Klasse, die von PlayerConnection erbt
    • Super-Constructor aufrufen
    • Eigenen einfach zu benutzenden Konstrukor erstellen
    • "sendPacket" Methode überschreiben
    • Listener einfügen


    1. -> Neue Klasse, die von PlayerConnection erbt
    Man erstelle eine neue Klasse (respekt, wenn du nicht weißt, wie das geht, aber trotzdem bis hier gelesen hast :D) und lasse sie von PlayerConnection erben. (extends PlayerConnection)
    Jetzt wird man relativ schnell merken, das Eclipse/andere IDE meckert.


    2. -> Super-Constructor aufrufen

    Code
    1. public CustomPlayerConnection(MinecraftServer minecraftserver,
    2. NetworkManager networkmanager, EntityPlayer entityplayer) {
    3. super(minecraftserver, networkmanager, entityplayer);
    4. }


    -> Ich denke mal, dazu muss ich nichts erklären.


    3. Eigenen einfach zu benutzenden Konstrukor erstellen
    Zunächst erstellen wir einen Konstrukor, der einen EntityPlayer fordert. Diser beinhaltet die Fields

    • server -> MinecraftServer Instanz
    • playerConnection.networkManager -> NetworkManager Instanz


    Also instanziieren wir unseren zuerst erstellten Konstrukor mit diesen Feldern und dem EntityPlayer selbst.

    Code
    1. public CustomPlayerConnection(EntityPlayer p) {
    2. this(p.server, p.playerConnection.networkManager, p);
    3. }


    Nun... jetzt ist das ja immer noch nicht so schön.
    Also erstellen wir uns eine Methode, die die EntityPlayer Instanz der Player-Instanz zurückgibt und erstellen einen weiteren Konstrukor mit dem Argument "Player", mit dem wir dann den zuvor erstellten Konstrukor instanziieren.

    Code
    1. public CustomPlayerConnection(Player player) {
    2. this(getNMSPlayer(player));
    3. }
    4. public static EntityPlayer getNMSPlayer(Player p) {
    5. return ((CraftPlayer)p).getHandle();
    6. }


    Das war es jetzt mit den Konstrukoren.


    4. -> "sendPacket" Methode überschreiben
    Folgender Code:

    Code
    1. @Override
    2. public void sendPacket(Packet packet) {
    3. super.sendPacket(packet); //Methode in PlayerConnection aufrufen
    4. }


    - macht erstmal garnichts.


    5. -> Listener einfügen
    So hier passiert nun die Magie.
    Wir erstellen ein neues Interface (mal wieder) mit dem Namen "PacketSendHandler" (oder auch anders)

    Code
    1. public static interface PacketSendHandler {
    2. public boolean handle(Player p, PacketPlayOutExplosion packet);
    3. }


    Wie man jetzt unschwer erkennen kann fange ich hier beispielweise das PacketPlayOutExplosion ab.
    Nun erstelle ich ein neues Feld in der Klasse, die von PlayerConnection erbt (in meinem Fall "CustomPlayerConnection") mit dem Typ PacketSendHandler und auch noch einen Setter dafür.

    Code
    1. private PacketSendHandler handler;
    2. public void setHandler(PacketSendHandler handler) {
    3. this.handler = handler;
    4. }


    So nun fügen wir in der "sendPacket" Methode eine if-Abfrage ein, die überprüft, ob der Handler schon gesetzt wurde.
    Wenn ja schauen wir, ob das Packet eine Instanz von "PacketPlayOutExplosion" ist. Wenn ja geben wir das an den Handler weiter, der dann wieder entscheidet, ob es weitergeleitet werden soll. (true wenn nicht)
    Wenn es nicht weitergeleitet werden soll returnt man einfach, um die Methode in der Super-Klasse nicht aufzurufen.

    Code
    1. if(handler != null) {
    2. if(packet instanceof PacketPlayOutExplosion) { //Wenn es eine Instanz ist
    3. if(handler.handle((Player) this.player.getBukkitEntity(), //getBukkitEntity returnt einen CraftPlayer, der einfach zum Player gecastet werden kann.
    4. (PacketPlayOutExplosion) packet)) { //Lass den Handler regeln
    5. return; //Wenn nicht weitergeleitet werden soll return
    6. }
    7. }
    8. }


    Nun bringt uns das erstmal überhaupt nichts. Wir müssen die PlayerConnection des Spielers erst beim Join mit unserer CustomPlayerConnection initialisieren.
    Also:

    Code
    1. CustomPlayerConnection pc = new CustomPlayerConnection(e.getPlayer());
    2. CustomPlayerConnection.getNMSPlayer(e.getPlayer()).playerConnection = pc;


    Dann setzen wir noch den Handler, der in meinem Fall dem Spieler eine Nachicht sendet.

    Code
    1. pc.setHandler(new PacketSendHandler() {
    2. @Override
    3. public boolean handle(Player p, PacketPlayOutExplosion packet) {
    4. p.sendMessage("§cBOOM!");
    5. return false;
    6. }
    7. });


    Das ganze sieht dann z.B. so aus:


    Event registrieren nicht vergessen ;)


    Aufgabe: Schreibe einen Packetunabhängigen PacketListener (am besten gibt man irgendwo das Packet an). Sende es per P/H-astebin hier drunter oder an mein Skype: niklas-5999 - Ist ein Experiment


    Soo, das wars!
    Bei Fehlern bitte melden, Kritik oder Verbesserungsvorschläge sehr erwünscht.

    MfG


    00110101 00110001 00110101 00111000 00110100 00110110 00110011 00110001 00110101 00111001 00110101 00110111 00110100 00110110 00110011 00110000 00110110 00110001 00110101 00110111 00110100 01000100 00110011 01000100 8o

    5 Mal editiert, zuletzt von Aquaatic ()

  • Ich habe das Tutorial jetzt nur überflogen - macht aber schon einen guten Eindruck!
    In der Tat ein schönes Thema.


    Hätte noch eine Ergänzung: Ausgehende Packets (Server -> Client) abfangen.
    Ich habe da auch direkt einen Ansatz, den ich seit einiger Zeit erfolgreich einsetze:

    • Die Klasse PlayerConnection erweitern
    • sendPacket()-Methode überschreiben (hier erfolgt der Zugriff!)
    • Dem Spieler beim Betreten des Servers die CustomPlayerConnection verpassen (einfach Feld neu initialisieren)

    Kannst ja mal überlegen, ob das noch irgendwie da rein passt ;)

  • Hallo,


    schön das du für die Anfänger so ein Tutorial machst, sieht auch auf den ersten Blick ganz gut aus, allerdings habe ich da so ein paar Sachen:



    Ich habe auch einen Packet Handler, dieser ist in jedem meiner Projekte drin, allerdings ist dieser ca. 30 Klassen groß. Du kannst nicht wirklich alles in eine Klasse programmieren.
    Du solltest mit Interfaces für z.B die Packets arbeiten, dass erleichtert dir die Arbeit!
    Du bist nicht auf ServerConnection etc. eingegangen, du hast das Thema nur von einer Seite betrachetet, (Client -&gt; Server)
    Du solltest Class.forName net.minecraft.util.io.netty.channel.Channel benutzen! :)


  • Danke für das Feedback! Das Ziel dieses Beitrags war es ja nicht die perfekte Lösung den Leuten zum abschreiben darzulegen, sondern um das Prinzip zu erläutern. Zudem hatte ich auch relativ wenig Zeit, aber ich werde es vermutlich noch erweitern.
    @Janhektor gute Idee, werde ich mit einbringen. :)

    MfG


    00110101 00110001 00110101 00111000 00110100 00110110 00110011 00110001 00110101 00111001 00110101 00110111 00110100 00110110 00110011 00110000 00110110 00110001 00110101 00110111 00110100 01000100 00110011 01000100 8o

  • Hallo.
    Das gehört zwar nicht ganz dazu, aber wie kann man es schaffen, dass man Packets manuell senden kann. Ich will das mit dem PacketPlayOutPlayerInfo machen. Das bekommt man von allen Spielern zugesendet, wenn man den Server beitritt. Nun will ich es aber so machen, dass ich wie z.B. bei der TagAPI einen Spieler "refreshen" kann und dann seine PlayerInfoDaten in PlayerInfo-Packets an alle Spieler noch mal gesendet werden. Dann kann ich diese wieder modifizieren und weitergeben.

    Es existiert ein Interesse an der generellen Rezession der Applikation relativ primitiver Methoden komplementär zur Favorisierung adäquater komplexer Algorithmen.


    Mit freundlichen Grüßen
    Twister21

  • Ich hab es jetzt zwar nicht ganz verstanden - aber du kannst die Packets ja abfangen, irgendwo abspeichern und wieder aufrufen / ggf. noch davor clonen.

    MfG


    00110101 00110001 00110101 00111000 00110100 00110110 00110011 00110001 00110101 00111001 00110101 00110111 00110100 00110110 00110011 00110000 00110110 00110001 00110101 00110111 00110100 01000100 00110011 01000100 8o

  • Ich will es ja so machen, dass ich die Daten noch mal verändern kann. Das Problem ist, dass das Packet was der Spieler gesendet bekommt die EnumAction "Add" hat. Das bedeutet, dass wenn ich dem Spieler das Packet noch mal sende zwei mal in der TabList sein Name steht. Ich will aber seinen alten Namen nicht entfernen sondern ihn bearbeiten. Geht das?


    EDIT: Habe gerade was von Update als EnumAction gesehen. :D

    Es existiert ein Interesse an der generellen Rezession der Applikation relativ primitiver Methoden komplementär zur Favorisierung adäquater komplexer Algorithmen.


    Mit freundlichen Grüßen
    Twister21

  • Hallo Leute...


    Ich habe mir damit eine kleiner Methode geschrieben und dabei ist mir aufgefallen, dass wenn ich das packet abfange und es durch ein anderes, abgewandeltes ersetze... dann ist das ganze sich selbst aufrufend (rekursiv), sodass der Server abschmiert... wie fixe ich das am besten?

  • Kommt drauf an, was du machen willst. Wenn du ein eigenes Packet erstellen möchtest wird es schwieriger - ist aber möglich. Möchtest du eines verändern, änderst du einfach den Wert.

    MfG


    00110101 00110001 00110101 00111000 00110100 00110110 00110011 00110001 00110101 00111001 00110101 00110111 00110100 00110110 00110011 00110000 00110110 00110001 00110101 00110111 00110100 01000100 00110011 01000100 8o

  • Oh, ich glaube ich muss das Tutorial mal etwas verändern. Es gibt eine Möglichkeit einen Encoder in die Pipeline einzufügen und so das Packet zu verändern

    MfG


    00110101 00110001 00110101 00111000 00110100 00110110 00110011 00110001 00110101 00111001 00110101 00110111 00110100 00110110 00110011 00110000 00110110 00110001 00110101 00110111 00110100 01000100 00110011 01000100 8o