Beiträge von Lead0b110010100

    18.03.21


    Nach meinem dreiträgigen Krankenhausaufenthalt kann ich sagen: Gott, ich hasse es. Aber eine gute Sache hatte es: Ich hatte mehr als genug Zeit mir weitere Meldungen anzusehen. Ich dachte, ich zeige mal ein paar davon.


    Zunächst hab ich mir PVS - Studio auf absolut legalem Wege gekauft um es nun für Metin2 nutzen zu können (hust hust). Nachdem ichs also gecrackt.. eh eh gekauft hatte, konnte ich mich mal etwas austoben. Dabei habe ich mich bis jetzt vor allem auf die Server Source & nur die Punkte mit der Priorität 'High' konzentriert.


    Top 5: Unnötige Prüfungen

    Code
    1. int32_t iNewRange = 150; //(int32_t)(ch->GetMobAttackRange() * 0.2);
    2. if (iNewRange < 150)
    3. iNewRange = 150;


    Im Sourecode wurden folgende Zeilen als 'unecessary' betitelt. Macht ja auch Sinn, der Wert von iNewRang wird niemals kleiner als 150 werden.

    Über den Kommentar auf der rechten Seite wird ersichtlich, dass der Entwickler hier vergessen hat, die Prüfung rauszunehmen und vorher noch Berechnungen für Monster über deren Attack Range gemacht hatte.


    Top 4: Nutzen von Pointern, die womöglich NULL sind (Führen unabdinglich zu Corecrashes)

    Code
    1. LPDUNGEON pDungeon = CDungeonManager::instance().Find(info->dungeon_id);
    2. pDungeon->jump_to_event_ = NULL;


    Muss ich dazu noch was sagen? Falls kein Dungeon mit dieser ID gefunden wird, aus welchem Grund auch immer, derefernziere ich mit dem Pfeiloperator -> einen nullptr (NULL ist in clang 11 typedefed zu nullptr). Ich mache also womöglich:


    Code
    1. nullptr->jump_to_event_ = NULL;


    zum Fixxen einfach die Zeile ein paar Zeilen runterschieben:


    Code
    1. if (pDungeon)
    2. {
    3. pDungeon->jump_to_event_ = NULL;
    4. pDungeon->JumpToEliminateLocation();
    5. }


    Davon gab es extrem viele, wobei man drauf achten muss was man genau verändert. Das Tool ist auch nicht fehlerlos, es erkennt manchmal z.B nicht das Pointer vor dem Aufruf von Funktionen bereits geprüft werden. So wie z.B hier, wo das Tool false positives ausgepuckt hat:


    Code
    1. void CHARACTER::SendDamagePacket(LPCHARACTER pAttacker, int32_t Damage, uint8_t DamageFlag)
    2. {
    3. if (pAttacker->IsSelfDestruct())
    4. return;


    Das Tool hat gemeckert, dass pAttacker nullptr sein könnte. Kann es aber nicht, weil pAttacker beim Aufruf von CHARACTER::SendDamagePacket vorher geprüft wird auf Gülltigkeit. Was lernen wir daraus? Auch Tools sind nicht perfekt, immer schön eigene Gedanken machen. Nicht alles was automatisch erkannt wird, ist auch automatisch ein Fehlerfall in eurem Projekt.


    Top 3: Memory Leaks


    Der Klassiker, ich glaub das die YMIR Entwickler überhaupt nichts von Memory Leaks oder dieser Problematik wussten, anders kann ich mir diese Fehler einfach nicht erklären. In irgendeiner Funktion wird returned, ohne vorher das erstellte Objekt wieder zu löschen. Hier nur eins von 380.000 Beispielen in der Source:


    Code
    1. if (!ITEM_MANAGER::instance().GetTable(dwItemVnum))
    2. {
    3. sys_err("ReadMonsterDropItemGroup : item not exists in range %d~%d : %d : node %s", dwVnumRange[0], dwVnumRange[1], dwItemVnum, stName.c_str());
    4. M2_DELETE(pkGroup); // Das hier fehlte
    5. return false;
    6. }


    Top 2: Unnötiges neu erstellen von Strings


    Erklärt sich eigentlich auch selbst:


    Code
    1. int32_t currentState = pPC->GetFlag(stStateFlag.c_str());


    Ich habe einen std::string stStateFlag, diesen gebe ich als const char Pointer (const char*) über den Befehl c_str() an die Funktion GetFlag weiter. Dieser erstellt (je nachdem was die Min String Limit ist) diesen String und speichert ihn im Heap (ja für kleine Strings muss das nicht sein, unterscheidet sich je nach OS). Was ist effektiv passiert? Ich habe std::string zu const char* zu neuem std::string gemacht, das ist unnötig. Folgend wäre die Zeile korrekt:


    Code
    1. int32_t currentState = pPC->GetFlag(stStateFlag);


    Wenn die Funktion jetzt noch eine konstante Referenz auf den String entgegennimmt 'const std::string&', bin ich glücklich. Sonst wird der String auch wieder kopiert (also das selbe Prozedere), was unnötig ist.


    Top 1: str.find() wird als boolean verwendet


    Code
    1. if (rRegion.strMapName.find(szMapName))


    Was ist falsch an diesem Ausdruck? Schauen wir uns dazu wieder die Funktion an auf cppreference.com:


    Bitte melden Sie sich an, um diesen Anhang zu sehen.


    Wieder ganz viele Möglichkeiten, wie man etwas suchen kann. Interessant ist vor allem, dass es einen Unterschied macht ob man einen Character sucht also:


    Code
    1. std::string str = "test";
    2. str.find('c');


    oder einen const char* (der zu std::string bzw. basic_string& implizit konvertiert wird):


    Code
    1. std::string str = "test";
    2. str.find("c");


    Laut quick-bench.com ist der Unterschied aufjedenfall gegeben. Hier mal ein Vergleich bei Clang11 (LLVM++11) ohne Optimierung:


    Bitte melden Sie sich an, um diesen Anhang zu sehen.

    Gut aber das nur nebenbei, also wir merken uns: Wenn wir nur einen Char suchen, wollen wir auch nur den Char suchen. Es macht einen Unterschied ob man '' oder "" nutzt. Check? Check.

    Interessant für uns ist der Absatz Return Value:

    Code
    1. Return value
    2. Position of the first character of the found substring or npos if no such substring is found.


    Das heißt, wenn ich einen String "hallo" habe und "c" in diesem String suche, erhalte ich std::string::npos zurück und folgend steht dieser in der STL:


    Code
    1. static constexpr auto npos{static_cast<size_type>(-1)};


    Ergo ich bekomme -1 (als Zahl zurück), der je nach size_type einen anderen Datentypen bekommt bzw. statisch in diesen gecasted wird. Diese Variable ist zur Compilezeit bekannt und static (static constexpr). Alles klar, was passiert also, wenn 'c' nicht gefunden wird, wie sieht unsere Abfrage von oben dann aus?


    Code
    1. if (-1)


    -1 ist aber im Boolschen kein Fehlerwert und wird nicht als solcher wahrgenommen, ergo das Ergebnis hier ist:


    Code
    1. if (true)


    Dieser Ausdruck gibt also immer true zurück, egal ob c gefunden wurde oder nicht. Wie wäre es nun richtig? Naja, das sagt uns die Dokumentation ja, man soll auf std::string::npos prüfen, es wird schließlich std::string::size_type zurückgegeben und nicht bool. Also viel geredet, wie sieht die richtige Prüfung nun aus?


    Code
    1. if (rRegion.strMapName.find(szMapName) != std::string::npos)


    Geht doch. Ich bin jetzt ungefähr bei der Hälfte meiner Erkenntnisse aus dem Krankenhaus. Zyniker würden sagen, ich sollte öfter krank werden. Naja.

    Weiter hab ich meine Source noch auf C++20 geupgraded, war eig. ganz einfach wenn man schon auf c++17 ist. Hier und da noch ein bisschen Korrektur und voilá.


    Ich hab weiter noch Funktionen entfernt, die ich nicht nutze. So wie beim .DE Petsystem die Funktion HasOption sowie die Funktionen Mount() und Unmount() für Pets. Weiter hab ich alle dev_log's und das System als Solches entfernt. Und zuletzt noch die Variable '_dummy' in der guild.h, die nirgends gebraucht aber überall mitgegeben wird.


    Grob gefasst wars das eigentlich schon, klar hab ich noch mehr gemacht - Aber daran kann ich mich nicht mehr errinern. Ich denke, da nun die Punkte durch sind die als 'Optimization' und 'High' geranked waren, könnt ich mich nun an die 'Middle' Punkte ranmachen. Ich weiß auch schon welche zwei Themen ich das nächste Mal thematisiere, aber für heute sind das genug Kopfschmerzen für euch.


    Bis morgen!

    14.03.21


    Neuer Tag, neue Meldung:


    Code
    1. Id: missingOverride
    2. The function 'OnFlush' overrides a function in a base class but is not marked with a 'override' specifier.


    Das Tool hat gemeldet, dass gewisse Funktionen nicht als override spezifiziert wurden obwohl sie eine Funktion der Oberklasse überschreiben.


    Aus

    Code
    1. virtual void OnFlush();


    wurde also kurzerhand

    Code
    1. void OnFlush() override;


    Dazu habe ich gelernt, wo das virtual keyowrd und wo das override hingehört. Im Gegensatz zum virtual keyword ist das override eher eine Hilfestellung für den Entwickler und ist überhaupt nicht problematisch, wenn es fehlt. Es prüft nur beim Kompilieren, ob diese Funktion mit Rückgabeparamter, Funktionsname und Attributen wie const oder mutable tatsächlich in der Oberklasse exakt so existiert. Folgendes wäre z.B ein Fehler, der ohne override einfach kompilieren würde:


    Code
    1. struct Base
    2. {
    3. virtual int Test() const = 0; // Pure virtual function, muss in Derived Klasse definiert werden zum Nutzen, das ist nur eine Deklaration
    4. };
    5. struct Derived : public Base
    6. {
    7. long Test() const;
    8. }


    Das wäre rein syntaktisch kein Fehler. Stände direkt nach der Klammer 'override' wie oben beschrieben, würde der Compiler meckern das man überhaupt nichts überschreibt.

    Das virtual keyword wiederrum macht tatsächlich einen Unterschied, unzwar vor allem beim Destruktor von Klassen. Denn wenn die Basisklasse 'Base' zerstört wird, wird ohne 'virtual' der Destruktor der Derived - Klasse nicht aufgerufen. Da diese Basisklasse ja überhaupt nicht weiß, dass es überhaupt einen Desktruktor gibt, den eine erbende Klasse implementieren muss. Anders sieht es aus, wenn man den Destruktor der Derived Klasse aufruft, diese leitet das Zerstören weiter an die Basisklasse, weil sie ja eine eindeutige Basisklasse kennt und von ihr erbt.


    Wichtig war auch, dass das 'override' keyword vor allen anderen 'function specifiers' kommt. Also auch vor const. Folgendes ist ein Compilefehler:


    Code
    1. void OnFlush() const override;


    Richtig wäre:


    Code
    1. void OnFlush() override const;

    13.03.21

    Als nächstes sollte ich dieses erlangte Wissen auf alle Stellen im Source Code anwenden

    Ja gut, das war wohl nichts. cppcheck hat keine andere Funktion gefunden, die mit std::any_of hätte verkürzt werden können.

    Dafür aber std::find_if, was ich mir vorher ja schon angesehen habe. Hab noch eine Methode gefunden, std::find_if verkürzt zu schreiben.


    Anscheinend kann man std::find_if nutzen, falls man nicht nur ein bool erwartet das einem sagt "ob irgendein Element zur Funktion p true ergibt" (std::any_of) sondern "wenn irgend ein Element für p true ergibt, gebe den Iterator an der Stelle des Elementes zurück" (std::find_if).


    Da diese beiden Dinge abgehakt waren, habe ich mir mal andere 'Fehler'-Meldungen angesehen:

    Code
    1. &(*pkTable)

    cppcheck hat angemerkt, dass es keinen Sinn macht pkTable zu dereferenzieren um ihn wieder zu referenzieren, da pkTable ein Pointer ist.


    Ich habe noch andere Stellen in meinem Source Code gefunden, die das machen aber nicht von cppcheck gemeldet wurden. Vllt. kauf ich mir tatsächlich PVS Studio und lasse die Source später damit prüfen.. Für jetzt halte ich mich mal Strikt an die Vorgaben von cppcheck.


    Code
    1. if ((row = mysql_fetch_row(pMsg2->Get()->pSQLResult)) && row != NULL){


    Hier hat er gemeckert, dass die Prüfung auf row != NULL immer true ist. weil bereits der erste Ausdruck:

    Code
    1. (row = mysql_fetch_row(pMsg2->Get()->pSQLResult))

    entweder wahr oder falsch ist. Wenn er falsch ist, kommt man nie zur Prüfung

    Code
    1. row != NULL


    Deshalb ist das immer true. War auch interessant sich mal etwas mit der boolschen Logik außeinanderzusetzen. Klar kennt man das aus dem Studium, aber so nochmal vor Augen geführt zu bekommen war schon spannend.


    Ich denke ich suche mir morgen einen neuen interessanten Fehler und analysiere den.

    12.03.21


    So, Mittagspause. Schnell mal weiter machen, wo waren wir stehengeblieben? std::any_of. Na gut, dann los.

    Lieblingsseite für C++ Docs geöffnet: Bitte melden Sie sich an, um diesen Link zu sehen. und folgende drei Templates gefunden:


    Bitte melden Sie sich an, um dieses Bild zu sehen.


    Scheint als gäbs folgende Möglichkeiten zur Nutzung:

    (begin, end, function) -> returns bool

    (begin, end, function) -> returns constexpr bool (Schonmal cool, dass es das seit C++20 gibt. Muss man halt drauf achten, dass die Argumente die man rein gibt ins Template zur Compilezeit errechnet werden können und bekannt sind)

    (execution_policy, begin, end, function) -> returns bool


    Nach 2 Minuten googeln hab ich herrausgefunden, dass die execution_policy einfach nur eine Art ist der Funktion mitzuteilen wie sie durch den Container iterieren soll. z.B von hinten, von vorne, oder eine ganz eigene Policy wie das man gewisse Mengen betrachtet. Das ist für unsere aktuelle kleine Aufgabe nicht relevant, schätze ich. Aber gut das im Hinterkopf zu haben für später.


    Die Implementation ist auch interessant, es wird nur eine bekannte Funktion (std::find_if) aufgerufen und mit einem != Operator auf last geprüft:


    Code
    1. template< class InputIt, class UnaryPredicate >
    2. constexpr bool any_of(InputIt first, InputIt last, UnaryPredicate p)
    3. {
    4. return std::find_if(first, last, p) != last;
    5. }


    Ergo: Wenn std::find_if mit der Funktion p einen Iterator zurückgibt, der nicht bis zur letzten Stelle 'last' gekommen ist, ist der Ausdruck true. Denn dann hat die Funktion für eines der Elemente zwischen first und last funktioniert. Haben wir ein Array aus den Membern 1,2 und 3 und prüfen ob einer der Member == 3 ist, dann wäre der Iterator nicht an der letzten Stelle. Die quasi eins weiter im Array wäre, bei Stelle 4 (gedanklich Nr. 4) und Index 3.


    Jetzt noch Beispielcode besichtigen und dann eigenen Code aufhübschen:


    Code
    1. * v ist ein Vektor
    2.     * DivisibleBy(..) ist eine oben definierte Funktion bzw. ein struct das bei Aufruf prüft ob die Zahl durch 7 teilbar ist.
    3.     if (std::any_of(v.cbegin(), v.cend(), DivisibleBy(7))) {
    4. std::cout << "At least one number is divisible by 7\n";
    5. }


    Und num zum eigenen Codebeispiel:


    Code
    1. bool IsConfigured() const
    2. {
    3.     for (const auto &it : attributes)
    4.     {
    5.         if (it.bType && it.sValue)
    6.             return true;
    7.     }
    8.     return false;
    9. }


    Kann also wohl geschrieben werden als:


    Code
    1. bool IsConfigured() const
    2. {
    3.     return std::any_of(std::begin(attributes), std::end(attributes), [](const auto& attr) { return attr.bType && attr.sValue; });
    4. }


    Nach dem Standartfehler "Bezeichner wurde nicht gefunden" hab ich bei cppreference noch geschaut, in welchem Header diese Methode drin ist. Sie ist wohl in 'algorithm' drin. Also:


    Code
    1. #include <algorithm>

    am Anfang der Datei eingefügt...


    Geht doch!


    Code
    1. ========== Erstellen: 0 erfolgreich, 0 fehlerhaft, 10 aktuell, 0 übersprungen ==========


    Als nächstes sollte ich dieses erlangte Wissen auf alle Stellen im Source Code anwenden, hoffentlich hat das GUI von cppcheck eine Filteroption für bestimmte 'Fehler'-Typen.

    Hallo,


    ich wollte hier mal ein Tagebuch zu meinem Exkurs in die statische Codeanalyse führen. Am Ende hilft ja vielleicht sogar meine Erkenntnis dem ein oder Anderen.

    Auf meinem Weg werde ich kleine Ausschnitte aus meinem Code / dem Standard YMIR Code präsentieren und zeigen wie ich sie geändert habe.


    Bevor wir allerdings dort ankommen, hier eine kurze Erklärung warum so ein öffentliches Tagebuch:

    Ich bin faul und so ein öffentlicher Thread motiviert mich vielleicht etwas (so zumindestens der Plan, mal sehen obs aufgeht).


    Wenn ihr Kritik & Wünsche für den Verlauf habt, gebt Bescheid.


    Mein Primärziel: Den YMIR Code weitestgehend aufräumen und dabei bestmöglichst etwas Wissen gewinnen durch die praktische Umsetzung. Wissen ist Macht, das merke ich jeden Tag auf Arbeit - Die Programmiersprache ist am Ende nur noch ein Tool. Und dennoch gillt es dieses Tool zu meistern und dabei helfen solche externen Hilfsprogramme ungemein. Aber das werdet ihr merken, falls ihr mich auf meinem Weg begleitet.


    Na dann..


    -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


    11.03.21


    Direkt vorab: Viel hab ich heute nicht gemacht. Bin ja auch faul.

    Trotzdem hab ich mir einige Extension für Visual Studio Code angesehen und geschaut, welche Tools diese intern wiedderum nutzen um an die gängigsten und modernsten Tools zu kommen.

    Aus meiner Vergangenheit wusste ich ja, dass cppcheck eine schöne cli Anwendung ist, um ein solches Vorhaben umzusetzen. Und trotzdem wollte ich andere Tools ausprobieren und... was soll ich sagen.


    Sie (die Guten) kosten alle was. SonarQube, Pc-Lint, PVS-Studio (mein Lieblingstool).

    Es blieb nur sowas wie SonarLint übrig, was zwar cool ist aber aus Erfahrung mit anderen Programmiersprachen mehr auf den Stylefaktor eingeht und die grobe Kompelexitätseinschätzung von Funktionen. Was völlig dumm ist, aber ja. ReSharper hab ich noch gefunden, aber nicht verwendet.

    Es gab noch Lizard, welches auch rasch installiert war - Aber mehr Komplexität durch bestimmte Tokenisierung prüft als meinen Code effektiv zu verbessern. Ich konnte dem Tool 0,0 abgewinnen, was ein Dreck.


    Da sind wir also nun. cppcheck soll es sein. Meine erste Intention war klar, ich will alle meine Dateien prüfen. Was macht man?


    Code
    1. cppcheck -h


    Man versucht die Dokumentation zu lesen. Nach ein paar Zeilen der erste Versuch:


    Code
    1. cppcheck .


    mh. Ich sehe nichts. Warum seh ich nichts? Er prüft, aber dann labert er was von maximal 15 ifdefs blabla. Gut nochmal 'cppcheck -h'. Aha. Ich muss ihn zwingen, alle ifdefs durchzuprüfen. Ja gut, dann weiter.


    Code
    1. cppcheck -f .


    Immer noch keine Meldungen, mach ich was falsch? Etwas Google und StackOverflow später..


    Code
    1. cppcheck -f --enable=all


    Na, geht doch! Aber das muss doch einfacher gehen.. Ich bin faul as fuck, als ob ich jedes Mal ne CLI nutzen muss um dann mühseelig die Meldungen mit Google und StackOverflow auszutauschen...

    Es gibt eine Anwendung - Na Gott sei dank!


    Bitte melden Sie sich an, um diesen Anhang zu sehen.


    Da der Button zum Auswählen eines Verzeichnisses besser versteckt ist, als performanter Code bei YMIR... hat es mich sage und schreibe 10 Minuten gekostet den Button zu finden.

    Achso und wählt den Ordner mit der Solution, sonst meckert cppcheck das er keine Konfiguration erkennen würde. Mimimi. Muss weiblich sein dieses Tool. #SexismusLebt


    Bitte melden Sie sich an, um diesen Anhang zu sehen.Bitte melden Sie sich an, um diesen Anhang zu sehen.


    Eine Sekunde geprüft und der erste Interessante fehler. Bei solchen For Loops, die am Ende nur return true zurückgeben gibt es wohl eine Methode std::any_of aus der STL.

    Die schau ich mir morgen an, meine Freundin spammt mich schon zu. Mein Code muss echt Scheiße sein, cppcheck läuft nämlich seitdem ich hier schreibe. Also entweder hab ich geschlampt oder cppcheck ist scheiße langsam.

    Easy beide Partien bedienen. Erst nen Hackshield Coden und dann den passenden „Cheat“

    Durch unsere lange Erfahrung in der "Cheat scene" sind wir sehr zuversichtlich, trotzdem wird demnächst ein Testserver aufgesetzt um jeden Zweifel zu beseitigen.

    Macht Lalaker doch nicht anders. Obs verwerflich ist, sei mal dahingestellt.

    Wer Cheats schreiben kann, kann sich auch bestmöglichst gegen diese schützen. So funktioniert halt literally die Cybersicherheit.

    Wenn ich das schon sehe, weiß ich warums publik ist. Das einzig gute an dem System ist das GUI, das von Aeldra (glaub ich?) geklaut ist.


    Code
    1. for (int i = 0; i<ch->missions_bp.size(); ++i)
    2. {
    3. ch->ChatPacket(CHAT_TYPE_COMMAND, "missions_bp %d %d %d %d", i, ch->missions_bp[i].type, ch->missions_bp[i].vnum, ch->missions_bp[i].count);
    4. ch->ChatPacket(CHAT_TYPE_COMMAND, "info_missions_bp %d %d %d %s", i, ch->v_counts[i].count, ch->v_counts[i].status, ch->rewards_bp[i].name);
    5. ch->ChatPacket(CHAT_TYPE_COMMAND, "rewards_missions_bp %d %d %d %d %d %d %d", i, ch->rewards_bp[i].vnum1, ch->rewards_bp[i].vnum2, ch->rewards_bp[i].vnum3, ch->rewards_bp[i].count1, ch->rewards_bp[i].count2, ch->rewards_bp[i].count3);
    6. }
    7. ch->ChatPacket(CHAT_TYPE_COMMAND, "size_missions_bp %d ", ch->missions_bp.size());
    8. ch->ChatPacket(CHAT_TYPE_COMMAND, "final_reward %d %d %d %d %d %d", ch->final_rewards[0].f_vnum1, ch->final_rewards[0].f_vnum2, ch->final_rewards[0].f_vnum3, ch->final_rewards[0].f_count1, ch->final_rewards[0].f_count2, ch->final_rewards[0].f_count3);
    9. ch->ChatPacket(CHAT_TYPE_COMMAND, "show_battlepass");

    Ich habe mein Forum von meinem Homepagedesign adaptieren lassen und bin mit dem Resultat mehr als zufrieden.

    Daemon ist auf alle meine Wünsche eingeggangen und hat sie bestmöglichst umgesetzt. Er hat sogar Fehler in Plugins gelöst, die er theoretisch nicht verantworten müsste.

    Die Kommunikation verlief reibungslos - Von mir also eine klare Empfehlung!

    Zu eurer Information!

    Die jüngsten Ereignisse haben mich zum Nachdenken gebracht: "Wie kann ich solche Fehler meinerseits in Zukunft verhindern?"


    Bitte melden Sie sich an, um dieses Bild zu sehen.


    In Zukunft führe ich Buch über jede Dienstleistung die von mir erfüllt wird. Ich schreibe es penibel nieder, damit im Zweifelsfalle sich darauf bezogen werden kann.

    Google Tabellen erlaubt keine Änderung, die nicht in den Logs zu sehen ist. Die Logs sind auch nicht deaktivierbar, demnach ich es ein Leichtes diese Vorgänge nachzuvollziehen.

    Da ich das Ganze hier mittlerweile im Recht großen Stil betreibe und mit den verschiedensten Kunden, reichen die Github Analyse-Tools mir nicht mehr.


    Wenn du ein Kunde von mir bist bzw. in Zukunft wirst, kannst du auch Zugriff auf diese Liste für dich (und deine Elemente) bekommen um den Supportzeitraum im Blick zu haben.

    Ich hoffe, dass ich somit meine Kunden glücklicher machen und gleichzeitig mein Geschäft etwas strukturierter führen kann.


    Falls du ein Kunde bist, der mir jüngst geschrieben hat und keine Antwort erhalten hat: Ich bedanke mich wirklich für die ganzen Nachfragen und Angebote, ich gebe mein Bestes euch allen eine faire Zeit meinerseits zuzuweisen, aktuell häufen sich die Anfragen allerdings wodurch ich teilweise Chats verliere. Bitte schreibe mir einfach erneut, ich gebe mein Bestes - Danke!


    ~ Lead

    Hab das vor einer weile so geschrieben, dass Gamemaster dies standartmäßig können. Für User würde es das Ambiente mancher Maps kaputtmachen finde ich. Da gibt es bessere Lösungen für mehr Sicht ;)


    Aber Danke fürs teilen, können manche vllt. gebrauchen!

    Solltest du jemals ein Iphone kaufen, kauf dir die AirPods Pro. Glaub mir, das ist purer Sex - Sogar noch besser.

    Du wirst kommen, vertrau mir.

    Ich durfte Lead als fachlich begabten, wie auch persönlich netten, kennenlernen.
    Er hat gute Arbeit geleistet und sich sehr professionell benommen. Selbst bei einem Fehler meinerseits, hat er die Hälfte der Arbeit wiederholt.

    Ich habe mich an dem letzten Tag des Supportzeitraums bei Ihm gemeldet und er hat ohne Diskussion alle Nachbesserungen/Wünsche erledigt.
    Preise und Fristen wurden eingehalten.


    Ich wünsche dir weiterhin viel Erfolg.

    Endlich mal wieder so eine richtig schöne Review die wirklich mal auf die Zusammenarbeit eingeht. So hab ich das auch empfunden, vielen Dank für die schönen Worte :)

    NICHTS aber wirklich NICHTS schlägt die AirPods Pro. Einmal gekauft, trotz Apple-Produktes wirklich glücklich.

    Die Konkurrenz ist in diesem Sektor auch nicht gerade billiger, hab einige ausprobiert davor wie die Galaxy Buds & die Libratone. Alles Schrott.


    Es gibt nur eine Einschränkung: Ich würd sie mir nur holen, wenn du auch ein Iphone hast. Da ist die Integration einfach perfekt, ansonsten vllt. was Anderes.