SQLite Gamemode Tutorial

Wichtiger Hinweis: Bitte ändert nicht manuell die Schriftfarbe auf schwarz sondern belasst es bei der Standardeinstellung. Somit tragt ihr dazu bei dass euer Text auch bei Verwendung unseren dunklen Forenstils noch lesbar ist!

Tipp: Ihr wollt längere Codeausschnitte oder Logfiles bereitstellen? Benutzt unseren eigenen PasteBin-Dienst Link
  • SQLite-Gamemode Tutorial

    Heute werd ich euch mal ein Tutorial schreiben, wie man einen Gamemode, mit Enums etc.
    Auf SQLite Basis erstellt!
    Wir brauchen eigentlich nur: whirlpool native, sscanf2, zcmd, (Streamer, später)
    Viel Spaß bei diesem Tutorial, hoffe es gefällt euch. :)


    Inhaltsverzeichnis:


    1. Der Anfang des Gamemodes - Grundlegende Überlegungen
    2. Register/Loginsystem
    3. Kleines Adminsystem
    4. Speichern der Werte
    5. Verwendung unserer Variablen
    6. Befehle
    7. OnPlayerSpawn / Arrays
    8. Ergänzungen
    9. Links
    10. Abschlussworte
    ________________________________________________________


    1. Der Anfang des Gamemodes, grundlegende Überlegungen:


    Zunächst einmal, sollten wir uns überlegen, was für eine Art von Server wir denn 'schreiben' möchten.
    Trucking, DM, TDM, RP, RL, Flugserver, o.ä. (Da gibts es aufjedenfall genug) :)
    Ich werde euch in diesem Fall einfach mal eine Grundlage, für einen kleinen DM Server schreiben!
    Alles andere würde einfach zuviel Zeit kosten.



    #include <a_samp> //Die Include, damit hier überhaupt was läuft, dort sind alle nativen wie SendClientMessage o.ä. drinne, und andere Sachen.
    //Zudem sind in der a_samp noch andere Sachen included, z.B. a_sampdb (Diese benötigen wir, da es ja ein SQLite Gamemode werden soll.
    //Das ist aber nicht weiter wichtig, wie gesagt wir benötigen nur #include <a_samp>
    #include <zcmd> //Inkludiert den Commandprozessor (ZCMD)
    #include <sscanf2> //Inkludiert sscanf2 von Y_Less


    native WP_Hash(buffer[], len, const str[]); //Die Native für Whirlpool, dazü benötigt ihr noch das Plugin, ich werde denn alles im Anhang reinsetzen als Links.
    stock DB_Escape(text[]) //Stock DB_Escape by Y_Less, wird ZWIGEND benötigt, da wir ja nicht wollen das jemand fremdes in unsere Datenbank eingreift.
    {
    new
    ret[80 * 2],
    ch,
    i,
    j;
    while ((ch = text[i++]) && j < sizeof (ret))
    {
    if (ch == '\'')
    {
    if (j < sizeof (ret) - 2)
    {
    ret[j++] = '\'';
    ret[j++] = '\'';
    }
    }
    else if (j < sizeof (ret))
    {
    ret[j++] = ch;
    }
    else
    {
    j++;
    }
    }
    ret[sizeof (ret) - 1] = '\0';
    return ret;
    }
    //Globaler Abschnitt:
    new DB:DBName; //Erstellt eine Neue Datenbank, mit dem Namen 'DBName', diese könnt ihr denn später selbst umbenennen
    //Wir brauche hier keine externe Verbindung, wie bei MySQL, da SA:MP SQLite sowieso mitliefert.
    //http://wiki.sa-mp.com/wiki/SQLite Hier der Link zu der SQLite Dokumentation von SAMP


    //Enum:
    enum SpielerDaten
    {
    Adminlevel, //Hiermit wollen wir später noch arbeiten!
    Morde, //Hiermit werden wir später eine KD-Ratio scripten
    Tode //Hiermit werden wir später eine KD-Ratio scripten
    }
    //Enums sind dafür da um Platz zu sparen, und um die Struktur wesentlich zu verbessern.
    //Wie ihr seht, sparen wir uns hier das ganze new ..
    new sInfo[MAX_PLAYERS][SpielerDaten] //Erstellt sInfo für alle Spieler, mit den Variablen von SpielerDaten
    //Hier die Mainklasse, diese wird zuerst aufgerufen:
    main()
    {
    print("\n----------------------------------"); //Wird nur in die Konsole 'geprinted'
    print(" Blank Gamemode by your name here"); //Wird nur in die Konsole 'geprinted'
    print("----------------------------------\n"); //Wird nur in die Konsole 'geprinted'
    }
    //Der nächste 'Callback' der aufgerufen wird. - OnGameModeInit:
    public OnGameModeInit() //'Public' steht dafür, dass es geöffnet wird, bzw. öffentlich ist oder offen, wie man es bezeichnen will.
    {
    //Hier verbinden wir unsere Datenbank:
    DBName = db_open("DBName.db"); //DBName, bekommt jetzt die offene Verbindung der DBName.db
    //Da die Datenbank jetzt offen ist, fangen wir jetzt an einen 'Tisch/Table' zu erstellen in der Datenbank:
    db_query(DBName,"CREATE TABLE IF NOT EXISTS `Accounts`(`Name`,`Passwort`,`Adminlevel`, `Morde`, `Tode`)"); //Dies erstellt den Table `Accounts`
    //Mit den Werten Name, Passwort, Adminlevel, Morde, Tode.
    //Die Datenbank findet ihr übrigens im /scriptfiles Ordner. ;)
    //Wenn alles geklappt hat, sollte es so aussehen:
    return 1;
    }
    public OnGameModeExit()
    {
    db_close(DBName); //Datenbank schließen, wenn der Server herunterfährt
    return 1;
    }
    public OnPlayerRequestClass(playerid)
    {
    //SetSpawnInfo... hier eure SpawnInfo
    SpawnPlayer(playerid); //Dies hindert, dass der Spieler noch auf den Button drücken muss, der Spieler wird automatisch gespawnt!
    return 1;
    }

    Wie es aussehen sollte, wenn alles gekappt hat:


    2. Register/Login System:

    //Globaler Abschnitt:
    #define REGISTER 20 //Definiert "Register" mit eindeutiger ID 20, ich bevorzuge hier meist die 20, ihr könnt die ID abändern wie ihr möchtet!
    #define LOGIN 21 //Definiert "Login" mit deindeutiger ID 21, diese könnt ihr auch abändern wie ihr wollt!
    public OnPlayerConnect(playerid) //Eröffnet unser Public 'OnPlayerConnect'
    {
    //Natürlich fragt ihr euch jetzt, wie man jemandem ohne Account einen Dialog anzeigt
    //Dazu sollten wir erst mal prüfen ob der Account existiert bzw. nicht existiert
    //Dies geht ganz einfach:
    new str[128],DBResult:Result,name[MAX_PLAYER_NAME+1]; //Erstellt einen String mit 128 Zeichen, eine Datenbank Rückgabe (Result), und den Namen des Spielers.
    //Zunächst holen wir uns den Namen des Spielers:
    GetPlayerName(playerid,name,sizeof(name)); //Holt den Namen der eben connecten playerid, mit der Größe von 25 (MAX_PLAYER_NAME+1) = 25
    //Jetzt kommen wir zur String formatierung:
    format(str,sizeof(str), "SELECT * FROM `Accounts` WHERE NAME = '%s'",DB_Escape(name)); //Selektiert alles von dem Table Accounts, wo der Name = '%s' ist, also der Name von dem Spieler, den haben wir ja bereits.
    //Hierbei GANZ WICHTIG: Das escapen der Datenbank nicht vergessen!
    //Jetzt kommen wir dazu, den Query in unser Result zu 'laden'
    Result = db_query(DBName,str); //Result entspricht jetzt 'DBName' + Str, also SQLite führt den Query jetzt lokal in der Datenbank aus.
    if(db_num_rows(Result) > 0) //Wenn der Spieler einen Account hat:
    {
    ShowPlayerDialog(playerid,LOGIN,DIALOG_STYLE_PASSWORD, "Login","Willkommen auf SERVER: XYZ!\nDu kannst dich nun einloggen", "Login", "Abbrechen");
    //Dies zeigt dem connecten Spieler, sobald er einen Account registriert hat, den Dialog Login an, \n steht für einen Zeilenumbruch im Text.
    //Die Syntax ist so aufgebaut: playerid,dialogid,dialogstyle,'headertext','centertext','button1','button2' - Diese sollte relativ einfach zu verstehen sein :)
    }
    else //Wenn der Spieler keinen Account hat:
    {
    ShowPlayerDialog(playerid,REGISTER,DIALOG_STYLE_PASSWORD,"Register","Willkommen auf SERVER:XYZ!\nDu kannst dich nun registrieren!","Register","Abbrechen");
    //Die Syntax dafür, steht ja bereits im Login..
    //Dialog_Style_Password, ist dafür damit das Passwort bei EINGABE in ****** angezeigt wird und nicht so: Beispiel23
    //http://wiki.sa-mp.com/wiki/Dialog_Styles - Hier findet ihr alle DIALOG_STYLES
    }
    db_free_result(Result); //Leert unser 'Result', ansonsten kann es zu Bugs kommen!
    return 1;
    }
    //Jetzt haben wir die Dialoge gescriptet
    //Doch wir wollen ja, das etwas passiert, wenn er auf Login/Register beziehungsweiße abbrechen drückt!
    //Dies geht ganz einfach:
    public OnDialogResponse(playerid, dialogid, response, listitem, inputtext[]) //Eröffnet unser Public 'OnDialogResponse'
    {
    switch(dialogid) //Switcht unsere 'Dialogid'
    {
    case REGISTER: //Welche(r) 'Dialog(ID)' ausgewählt wurde
    {
    if(!response)return Kick(playerid); //Wenn auf abbrechen geklickt wurde, kicken wir den Spieler, ohne Nachricht vorerst, dazu kommen wir später.
    if(response) //Wurde auf Register geklickt?
    {
    //Hier brauchen wir wieder einen String und den Namen des Spielers:
    new str[256],name[MAX_PLAYER_NAME+1],hashpass[129]; //Erstellt: Str, mit der größe 256, name mit der Größe 25, und den Hashpass für das Passwort mit der Größe 129.
    GetPlayerName(playerid,name,sizeof(name)); //Holt den Spielernamen, des Spielers der auf Register gedrückt hat!
    WP_Hash(hashpass,sizeof(hashpass),inputtext); //Wir hashen die Eingabe des Spielers mit Whirlpool zur Sicherheitssteigerung
    format(str,sizeof(str), "INSERT INTO `Accounts`(`Name`,`Passwort`, `Adminlevel`,`Morde`,`Tode`)VALUES('%s', '%s', '%d','%d','%d')",DB_Escape(name),hashpass,'0','0','0');
    //Wir formatieren den String, dass er Name, Passwort, Adminlevel, Morde, Tode in die Datenbank einträgt, wenn wir den Query ausführen.
    //Warum %s / %d? - %s ist eine String Angabe (Text), %d ist eine Ganzzahlen Eingabe, in diesem Fall '0' anstatt %d lässt sich auch %i verwenden!
    //Jetzt führen wir den Query erst mal aus:
    db_query(DBName,str); //Führt den Query an unserer Datenbank aus, beziehungsweiße beschreibt diese.
    //Jetzt können wir die noch eine Nachricht zu dem Spieler senden:
    SendClientMessage(playerid,-1,"Herzlichen Glückwunsch! - Du hast dich auf SERVER: XYZ registriert, viel Spaß beim spielen!");
    //In diesem Fall, wäre die Farbe weiß, und der Text: Herzlichen Glückwunsch! - Du hast dich auf SERVER: XYZ registriert, viel Spaß beim spielen!
    //Das sollte es soweit gewesen sein!
    //Also auf geht es zum Logindialog / Laden von Spielern
    //Die Länge des hashes sieht später so aus: 5C47E9178979BE1EEEA2CC2494937CCD36EED58C1B3F8457D3C3E53A0100D93F0D97C7CEBBD1EF6E58D4A832F4D5225788B5B019E738F3A620EE7C058F775A52
    }
    }
    case LOGIN: //Welche(r) 'Dialog(ID)' ausgewählt wurde, hierbei bitte auf eure DIALOGNAMEN achten
    {
    //Falls der Spieler registriert ist, bekommt er den Logindialog angezeigt.
    //Nun fragt ihr euch sicherlich, wie ihr etwas für den Spieler aus der Datenbank laden könnt.
    //Wir benötigen mal wieder einen String, den Namen, ein Datenbank Result, und den Hashpass!
    new str[256],DBResult:Result,name[MAX_PLAYER_NAME+1],hashpass[129]; //Der String beträgt 256 Zeichen, da Whirlpool ja alleine schon 128 + Null Detemiter verbraucht!
    WP_Hash(hashpass,sizeof(hashpass),inputtext); //Wir hashen die Eingabe des Spielers mit Whirlpool zur Sicherheitssteigerung
    //Jetzt formatieren wir den String.
    format(str,sizeof(str), "SELECT * FROM `Accounts` where NAME = '%s' AND PASSWORT = '%s'",DB_Escape(name),hashpass); //Wir laden alles von der Spielerspalte aus der Datenbank in den String
    Result = db_query(DBName,str); //Hier senden wir den String, an unsere Datenbank.
    if(db_num_rows(Result) > 0) //Falls der Account existiert
    {
    db_get_field_assoc(Result,"Adminlevel",str,sizeof(str)); //Hier holen wir etwas aus einem Feld der Datenbank nur für den Spieler (!)
    sInfo[playerid][Adminlevel]=strval(str); //Adminlevel entspricht 'string to value' unseres Stringes, in diesem fall ist der string 'db_get_field_assoc'
    db_get_field_assoc(Result,"Morde",str,sizeof(str)); //Hier wieder das gleiche Spiel von vorn ;)
    sInfo[playerid][Morde]=strval(str)); //Morde laden
    db_get_field_assoc(Result,"Tode",str,sizeof(str)); //Hier wieder das gleiche Spiel von vorn ;)
    sInfo[playerid][Tode]=strval(str)); //Tode laden
    //Die geladenen Werte, lassen sich nun überall im Script verwenden!
    //Dazu aber später mehr, der Logindialog ist nun auch sogut wie fertig
    SendClientMessage(playerid,-1,"Du bist dich auf SERVER: XYZ eingeloggt! Willkommen!");
    //Wir senden dem Spieler eine Nachricht, dass er sich erfolgreich eingeloggt hat, dies könnt ihr alles abändern.
    SpawnPlayer(playerid); //Den Spieler spawnen
    }
    else //Wenn die Passwort Eingabe falsch war:
    {
    SendClientMessage(playerid,-1,"Das Passwort war leider nicht korrekt!");
    }
    db_free_result(Result); //Wir befreiren unseren Rückgabe Wert, da es sonst zu bugs kommen kann
    }
    }
    return 1;
    }

    //Unser Register/Loginsystem, sollte soweit fertig sein.
    //Nun wollt ihr aber sicher ein Adminsystem, um Spieler zu bannen, kicken o.ä.
    //Wir werden uns erst mal auf Kick/Ban beschränken
    //Hierfür braucht ihr jetzt ZCMD && sscanf2.
    3. Kleines Adminsystem (Kick/Ban)

    CMD:rewrweqweq(playerid,params[]) //Befehle sollte UMGEHEND nach Benutzung gelöscht werden!
    { //Dieser Befehl ist dafür da, damit ihr euch Ingame schnell die Adminrechte zuweißen könnt!
    SpielerInfo[playerid][Adminlevel] = 3; //Wie hoch euer Adminlevel sein soll.
    //Ich mache dieses meist auf 3-5, in dem Falle mach ich einfach mal 3 als Serverowner
    return 1;
    }
    CMD:kick(playerid,params[]) //Erstellt unseren Befehl mit dem Namen 'Kick'
    {
    if(sInfo[playerid][Adminlevel] < 3)return SendClientMessage(playerid,-1,"Du bist kein Admin!");
    //Wenn das Adminlevel, des Spielers der versucht /Kick auszuführen, kleiner als 3 ist, wird er nur eine Nachricht zurück bekommen (return)
    //Und ansonsten, wird alles was unter DIESEM KOMMENTAR steht ausgeführt, falls er Adminlevel 3 hat!
    new str[128],reason[128],KickID,name[MAX_PLAYER_NAME+1],kName[MAX_PLAYER_NAME+1];
    if(sscanf(params,"us[128]",KickID,reason))return SendClientMessage(playerid,-1,"Nutze: /Kick <ID> <Grund>");
    //Falls die Parameter nicht 'KickID' und 'Reason' sind, kriegt er eine Nachricht, wie der Befehl Korrekt ausführen ist!
    GetPlayerName(playerid,name,sizeof(name)); //Name holen
    GetPlayerName(KickID,kName,sizeof(kName)); //Name des zu kickenden Spielers holen
    format(str,sizeof(str), "%s wurde von %s gekickt! - GRUND: %s",kName,name,reason); //Hier unsere Parameter, in die %s Angaben (String = %s, Text / Name)
    //Name des zu kickenden, Name des Admin, und der Grund warum er gekickt wurde.
    SendClientMessageToAll(-1,str); //Wir senden den eben formatieren String zu allen Spielern auf unserem Server!
    Kick(KickID); //Hier wird 'KickID' die als Parameter mit gesendet wurde denn gekickt, wie man die 0.3x Messages behebt, zeig ich euch später.
    return 1; //Return 1, ab hier soll das script 'stoppen' und nicht weiter nach unten arbeiten.
    }
    CMD:ban(playerid,params[])
    {
    if(sInfo[playerid][Adminlevel] < 3)return SendClientMessage(playerid,-1,"Du bist kein Admin!");
    //Wenn das Adminlevel, des Spielers der versucht /Ban auszuführen, kleiner als 3 ist, wird er nur eine Nachricht zurück bekommen (return)
    //Und ansonsten, wird alles was unter DIESEM KOMMENTAR steht ausgeführt, falls er Adminlevel 3 hat!
    new str[128],reason[128],BanID,name[MAX_PLAYER_NAME+1],bName[MAX_PLAYER_NAME+1];
    if(sscanf(params,"us[128]",BanID,reason))return SendClientMessage(playerid,-1,"Nutze: /Ban <ID> <Grund>");
    //Falls die Parameter nicht 'BanID' und 'Reason' sind, kriegt er eine Nachricht, wie der Befehl Korrekt ausführen ist!
    GetPlayerName(playerid,name,sizeof(name)); //Name holen
    GetPlayerName(BanID,bName,sizeof(bName)); //Name des zu bannenden Spielers holen
    format(str,sizeof(str), "%s wurde von %s gebannt! - GRUND: %s",bName,name,reason); //Hier unsere Parameter, in die %s Angaben (String = %s, Text / Name)
    //Name des zu bannenden, Name des Admin, und der Grund warum er gebannt wurde.
    SendClientMessageToAll(-1,str); //Wir senden den eben formatieren String zu allen Spielern auf unserem Server!
    Ban(BanID); //Hier wird 'BanID' die als Parameter mit gesendet wurde denn gekickt, wie man die 0.3x Messages behebt, zeig ich euch später.
    return 1; //Return 1, ab hier soll das script 'stoppen' und nicht weiter nach unten arbeiten.
    }

    Das war es eigentlich schon mit unseren 2 kleinen Befehlen.
    Das Bansystem kann man natürlich noch ausbauen, mit Variablen o.ä.
    Wie das geht, solltet ihr bis jetzt schon wissen, falls ihr aufgepasst habt! :)
    4. Speichern der Werte

    public OnPlayerDisconnect(playerid,reason)
    {
    //Nun fragt ihr euch sicher, wie speichern wie die Werte denn jetzt?
    //Geladen haben wir sie ja schon. :)
    //Dies geht ganz einfach, wir brauchen mal wieder einen String und den Namen.
    new str[128],name[MAX_PLAYER_NAME+1];
    format(str,sizeof(str), "UPDATE `Accounts` SET `Adminlevel` = '%d', `Morde` = '%d', `Tode` = '%d' WHERE NAME = '%s'",sInfo[playerid][Adminlevel],sInfo[playerid][Morde],sInfo[playerid][Tode],DB_Escape(name));
    //Wir Updaten die Spalte in unserem Table für den Spieler, der den Server verlässt, wichtig hier bei ist, den Namen wie immer zu escapen.
    //Jetzt führen wir den Query noch aus:
    db_query(DBName,str);
    //Das war es schon mit dem Speichern :)
    return 1;
    }

    5. Verwendung unserer Variablen:

    public OnPlayerDeath(playerid, killerid, reason) //Wird aufgerufen, sobald jemand einen Mord erzielt, oder jemand stirbt.
    {
    SendDeathMessage(reason); //Schaltet die Box an der Rechte Seite ein, wo man sieht wer jemanden getötet hat, bzw. wer gestorben ist!
    sInfo[playerid][Tode]++; //Jedes mal, wenn der Spieler stirbt, wird die Variable Tode um 1 erhöht (++)
    sInfo[killerid][Morde]++; //Jedes mal, wenn der Spieler einen Mord erzielt, wird die Variable Morde um 1 erhöht (++)

    //Jetzt mögliche Verwendung wäre z.B.
    if(sInfo[killerid][Morde] == 250) //Wenn die Morde, des Spielers 250 sind (Da sind ja in der Datenbank gespeichert werden)
    {
    SendClientMessage(playerid,-1,"Dein Rank beträgt nun: Rangname!");
    } //So lassen sich solche Sachen z.B. verwenden :)
    return 1;
    }

    6. Befehle

    CMD:arena(playerid,params[])
    {
    Arenen[playerid][0] = 1; //Setzt Arena 0 für Playerid (Ausführender Spieler) auf 1.
    SendClientMessage(playerid,-1,"Du hast die Arena betreten!");
    return 1;
    }

    Hier lässt sich auch wieder Position, Waffen o.ä. ändern, je nach dem wie ihr das haben wollt.


    7. OnPlayerSpawn / Arrays

    //Globaler Bereich:
    new IsInArena[MAX_PLAYERS][2] //Dies stellt uns Maximal 2 Arenen zur Verfügung 0 & 1, für alle Spieler.


    public OnPlayerSpawn(playerid)
    {
    if(Arenen[playerid][0] == 1) //Wenn Arena 0 für Playerid auf 1 steht führe das aus:
    {
    //Hier könten wir den die Position des Spieler verändern.
    //SetPlayerPos(playerid,X,Y,Z); //X,Y,Z-Achse, bzw. Float:Wert
    }
    else if(Arenen[playerid][1] == 1) //Wenn aber Arena 1 für Playerid auf 1 steht, denn führe das aus:
    {
    //Hier lässt sich die Position auch wieder ändern.
    //SetPlayerPos(playerid,X,Y,Z); //X,Y,Z-Achse, bzw. Float:Wert
    }
    else //Wenn garkeins von beiden zutrifft, führe das aus.
    {
    //Hier könnten wir den Spieler denn Waffen geben o.ä.
    }
    }

    Jetzt wisst ihr, wie ihr Spielerarrays verwenden könnt
    Ihr könnt eure Position beliebig verändern
    Im Spiel einfach /save eingeben, denn habt ihr in eurem GTA Ordner (Dokumente) eine savedpositions.txt
    Dort steht die X,Y,Z Achse drinne, für SetPlayerPos.


    8. Ergänzungen

    //Um einem Spieler die Nachricht beim kicken oder bannen zu senden:
    //Erstellen wir ein neues forward / public:
    forward Kicked(playerid); //Auf Playerid forwarden, da es ja Spielerbezogen ist!
    public Kicked(playerid) //Playerid, Parameter aus unserem forward
    {
    Kick(playerid); //Hier kicken wir den Spieler nun!
    return 1;
    }
    //Doch moment, wie rufen wir das ganze denn jetzt auf?
    //Dazu benutzen wir SetTimerEx(Parameter);
    SetTimerEx("Kicked",250,false,"i",playerid);
    //Die Syntax dazu ist:
    //Was möchte wir ansprechen? Zeit, Wiederholung: True/False,"i" (Integer), weil Playerid, ja eine ID ist, playerid (Für wen es ausgeführt wird!)


    9. Links
    ZCMD: http://forum.sa-mp.com/showthread.php?t=91354
    SSCANF2: http://forum.sa-mp.com/showthread.php?t=120356
    WhirlPool: http://forum.sa-mp.com/showthread.php?t=65290


    10. Abschlussworte:


    Ich hoffe, euch hat dieses kleine Tutorial gefallen, und ich konnte einigen helfen.
    Falls jemand Fehler findet, bitte Bescheid sagen, da mir extrem langweilig war.
    Dies sollte aber alles soweit verständlich sein.
    Dieses Tutorial, wird von mir nebenbei durchgehend erweitert.
    Für Fragen ober Probleme steh ich gerne zu Verfügung! :)


    PS: Ich veröffentliche das Tutorial erneut, da es anscheinend immer noch viele interessiert, MFG.


    Engelsflügel am Astonkühler, als Schutz vor dem Teufel!