Cataclysm Dark Days Ahead: Statische Analyse und Roguelike-Spiele

Bild 5

Sie müssen bereits aus dem Titel erraten haben, dass sich der heutige Artikel auf Fehler im Software-Quellcode konzentrieren wird. Aber nicht nur das. Wenn Sie nicht nur an C ++ interessiert sind und über Fehler im Code anderer Entwickler lesen möchten, sondern auch ungewöhnliche Videospiele ausgraben und sich fragen, was "Roguelikes" sind und wie Sie sie spielen, dann lesen Sie weiter!

Auf der Suche nach ungewöhnlichen Spielen bin ich auf Cataclysm Dark Days Ahead gestoßen, das sich unter anderem durch seine Grafiken auszeichnet, die auf ASCII-Zeichen in verschiedenen Farben basieren, die auf dem schwarzen Hintergrund angeordnet sind.

Eine Sache, die Sie an diesem und anderen ähnlichen Spielen überrascht, ist, wie viel Funktionalität in sie eingebaut ist. Insbesondere in Cataclysm können Sie beispielsweise nicht einmal einen Charakter erstellen, ohne den Drang zu verspüren, einige Anleitungen zu googeln, da Dutzende von Parametern, Merkmalen und Anfangsszenarien verfügbar sind, ganz zu schweigen von den zahlreichen Variationen von Ereignissen, die während des Spiels auftreten.

Da es sich um ein Spiel mit Open-Source-Code handelt, das in C ++ geschrieben wurde, konnten wir nicht vorbeigehen, ohne es mit unserem statischen Code-Analysator PVS-Studio zu überprüfen, an dessen Entwicklung ich aktiv teilnehme. Der Code des Projekts ist überraschend hochwertig, weist jedoch noch einige kleinere Mängel auf, von denen ich in diesem Artikel auf einige eingehen werde.

Viele Spiele wurden bereits mit PVS-Studio überprüft. Einige Beispiele finden Sie in unserem Artikel " Statische Analyse in der Videospielentwicklung: Top 10 Softwarefehler ".

Logik


Beispiel 1:

Dieses Beispiel zeigt einen klassischen Fehler beim Kopieren und Einfügen.

V501 Links und rechts vom '||' befinden sich identische Unterausdrücke. Operator: rng (2, 7) <abs (z) || rng (2, 7) <abs (z) overmap.cpp 1503

bool overmap::generate_sub( const int z ) { .... if( rng( 2, 7 ) < abs( z ) || rng( 2, 7 ) < abs( z ) ) { .... } .... } 

Der gleiche Zustand wird zweimal überprüft. Der Programmierer hat den Ausdruck kopiert, aber vergessen, die Kopie zu ändern. Ich bin nicht sicher, ob dies ein kritischer Fehler ist, aber Tatsache ist, dass die Überprüfung nicht so funktioniert, wie es beabsichtigt war.

Ein weiterer ähnlicher Fehler:

  • V501 Links und rechts vom Operator '&&' befinden sich identische Unterausdrücke 'one_in (100000 / to_turns <int> (dur))'. player_hardcoded_effects.cpp 547

Bild 11

Beispiel 2:

V728 Eine übermäßige Überprüfung kann vereinfacht werden. Die '(A && B) || (! A &&! B) 'Ausdruck entspricht dem Ausdruck' bool (A) == bool (B) '. inventar_ui.cpp 199

 bool inventory_selector_preset::sort_compare( .... ) const { .... const bool left_fav = g->u.inv.assigned.count( lhs.location->invlet ); const bool right_fav = g->u.inv.assigned.count( rhs.location->invlet ); if( ( left_fav && right_fav ) || ( !left_fav && !right_fav ) ) { return .... } .... } 

Dieser Zustand ist logisch korrekt, aber zu kompliziert. Wer diesen Code geschrieben hat, sollte Mitleid mit seinen Programmierkollegen haben, die ihn pflegen werden. Es könnte in einer einfacheren Form umgeschrieben werden: if (left_fav == right_fav) .

Ein weiterer ähnlicher Fehler:

  • V728 Eine übermäßige Überprüfung kann vereinfacht werden. Das '(A &&! B) || (! A && B) 'Ausdruck entspricht dem Ausdruck' bool (A)! = Bool (B) '. iuse_actor.cpp 2653

Exkurs i


Ich war überrascht zu entdecken, dass Spiele, die heute unter dem Namen "Roguelikes" bekannt sind, nur moderatere Vertreter des alten Genres der Roguelike-Spiele sind. Alles begann mit dem Kult-Spiel Rogue von 1980, das viele Studenten und Programmierer dazu inspirierte, ihre eigenen Spiele mit ähnlichen Elementen zu entwickeln. Ich denke, viel Einfluss kam auch von der Community des Tabletop-Spiels DnD und seinen Variationen.

Bild 8

Mikrooptimierungen


Beispiel 3:

Warnungen dieser Gruppe weisen auf Stellen hin, die möglicherweise optimiert werden könnten, anstatt auf Fehler.

V801 Leistungsminderung. Es ist besser, das zweite Funktionsargument als Referenz neu zu definieren. Ersetzen Sie 'const ... type' durch 'const ... & type'. map.cpp 4644

 template <typename Stack> std::list<item> use_amount_stack( Stack stack, const itype_id type ) { std::list<item> ret; for( auto a = stack.begin(); a != stack.end() && quantity > 0; ) { if( a->use_amount( type, ret ) ) { a = stack.erase( a ); } else { ++a; } } return ret; } 

In diesem Code ist itype_id tatsächlich ein getarnter std :: string . Da das Argument ohnehin als Konstante übergeben wird, was bedeutet, dass es unveränderlich ist, würde die einfache Übergabe eines Verweises auf die Variable dazu beitragen, die Leistung zu verbessern und Rechenressourcen zu sparen, indem der Kopiervorgang vermieden wird. Und obwohl es unwahrscheinlich ist, dass die Zeichenfolge lang ist, ist es eine schlechte Idee, sie jedes Mal ohne guten Grund zu kopieren - zumal diese Funktion von verschiedenen Aufrufern aufgerufen wird, die wiederum auch von außen typisiert werden und haben um es zu kopieren.

Ähnliche Probleme:

  • V801 Leistungsminderung. Es ist besser, das dritte Funktionsargument als Referenz neu zu definieren. Ersetzen Sie 'const ... evt_filter' durch 'const ... & evt_filter'. input.cpp 691
  • V801 Leistungsminderung. Es ist besser, das fünfte Funktionsargument als Referenz neu zu definieren. Ersetzen Sie 'const ... color' durch 'const ... & color'. output.h 207
  • Der Analysator gab insgesamt 32 Warnungen dieses Typs aus.

Beispiel 4:

V813 Leistungsminderung. Das Argument 'str' sollte wahrscheinlich als konstante Referenz wiedergegeben werden. catacharset.cpp 256

 std::string base64_encode( std::string str ) { if( str.length() > 0 && str[0] == '#' ) { return str; } int input_length = str.length(); std::string encoded_data( output_length, '\0' ); .... for( int i = 0, j = 0; i < input_length; ) { .... } for( int i = 0; i < mod_table[input_length % 3]; i++ ) { encoded_data[output_length - 1 - i] = '='; } return "#" + encoded_data; } 

Obwohl das Argument nicht konstant ist, ändert es sich in keiner Weise im Funktionskörper. Aus Gründen der Optimierung wäre es daher eine bessere Lösung, diese als konstante Referenz zu übergeben, anstatt den Compiler zu zwingen, lokale Kopien zu erstellen.

Diese Warnung kam auch nicht alleine; Die Gesamtzahl der Warnungen dieses Typs beträgt 26.

Bild 7

Ähnliche Probleme:

  • V813 Leistungsminderung. Das Argument 'message' sollte wahrscheinlich als konstante Referenz gerendert werden. json.cpp 1452
  • V813 Leistungsminderung. Das Argument 's' sollte wahrscheinlich als konstante Referenz wiedergegeben werden. catacharset.cpp 218
  • Und so weiter ...

Exkurs ii


Einige der klassischen Roguelike-Spiele befinden sich noch in der aktiven Entwicklung. Wenn Sie die GitHub-Repositorys von Cataclysm DDA oder NetHack überprüfen, werden Sie feststellen , dass Änderungen jeden Tag übermittelt werden. NetHack ist tatsächlich das älteste Spiel, das noch entwickelt wird: Es wurde im Juli 1987 veröffentlicht und die letzte Version stammt aus dem Jahr 2018.

Dwarf Fortress ist eines der beliebtesten - wenn auch jüngeren - Spiele des Genres. Die Entwicklung begann im Jahr 2002 und die erste Version wurde im Jahr 2006 veröffentlicht. Das Motto "Verlieren macht Spaß" spiegelt die Tatsache wider, dass es unmöglich ist, in diesem Spiel zu gewinnen. Im Jahr 2007 wurde die Zwergenfestung durch eine jährliche Abstimmung auf der ASCII GAMES-Website als "Bestes Roguelike-Spiel des Jahres" ausgezeichnet.

Bild 6

Übrigens könnten Fans froh sein zu wissen, dass Dwarf Fortress mit verbesserten 32-Bit-Grafiken, die von zwei erfahrenen Moddern hinzugefügt wurden, zu Steam kommt. Die Premium-Version erhält außerdem zusätzliche Musiktitel und Steam Workshop-Unterstützung. Besitzer von kostenpflichtigen Kopien können auf Wunsch zu den alten ASCII-Grafiken wechseln. Mehr

Überschreiben des Zuweisungsoperators


Beispiele 5, 6:

Hier sind einige interessante Warnungen.

V690 Die Klasse 'JsonObject' implementiert einen Kopierkonstruktor, es fehlt jedoch der Operator '='. Es ist gefährlich, eine solche Klasse zu benutzen. json.h 647

 class JsonObject { private: .... JsonIn *jsin; .... public: JsonObject( JsonIn &jsin ); JsonObject( const JsonObject &jsobj ); JsonObject() : positions(), start( 0 ), end( 0 ), jsin( NULL ) {} ~JsonObject() { finish(); } void finish(); // moves the stream to the end of the object .... void JsonObject::finish() { .... } .... } 

Diese Klasse verfügt über einen Kopierkonstruktor und einen Destruktor, überschreibt jedoch den Zuweisungsoperator nicht. Das Problem ist, dass ein automatisch generierter Zuweisungsoperator den Zeiger nur JsonIn zuweisen kann. Infolgedessen würden beide Objekte der Klasse JsonObject auf dasselbe JsonIn verweisen . Ich kann nicht sicher sagen, ob eine solche Situation in der aktuellen Version auftreten könnte, aber eines Tages wird sicherlich jemand in diese Falle tappen.

Die nächste Klasse hat ein ähnliches Problem.

V690 Die Klasse 'JsonArray' implementiert einen Kopierkonstruktor, es fehlt jedoch der Operator '='. Es ist gefährlich, eine solche Klasse zu benutzen. json.h 820

 class JsonArray { private: .... JsonIn *jsin; .... public: JsonArray( JsonIn &jsin ); JsonArray( const JsonArray &jsarr ); JsonArray() : positions(), ...., jsin( NULL ) {}; ~JsonArray() { finish(); } void finish(); // move the stream position to the end of the array void JsonArray::finish() { .... } } 

Die Gefahr, den Zuweisungsoperator in einer komplexen Klasse nicht zu überschreiben, wird im Artikel " Das Gesetz der großen Zwei " ausführlich erläutert.

Beispiele 7, 8:

Diese beiden befassen sich auch mit dem Überschreiben von Zuweisungsoperatoren, diesmal jedoch mit spezifischen Implementierungen.

V794 Der Zuweisungsoperator sollte vor dem Fall 'this == & other' geschützt werden. mattack_common.h 49

 class StringRef { public: .... private: friend struct StringRefTestAccess; char const* m_start; size_type m_size; char* m_data = nullptr; .... auto operator = ( StringRef const &other ) noexcept -> StringRef& { delete[] m_data; m_data = nullptr; m_start = other.m_start; m_size = other.m_size; return *this; } 

Diese Implementierung bietet keinen Schutz vor einer möglichen Selbstzuweisung, was eine unsichere Praxis ist. Das heißt, das Übergeben eines * dieser Referenz an diesen Operator kann zu einem Speicherverlust führen.

Hier ist ein ähnliches Beispiel für einen nicht ordnungsgemäß überschriebenen Zuweisungsoperator mit einer besonderen Nebenwirkung:

V794 Der Zuweisungsoperator sollte vor dem Fall 'this == & rhs' geschützt werden. player_activity.cpp 38

 player_activity &player_activity::operator=( const player_activity &rhs ) { type = rhs.type; .... targets.clear(); targets.reserve( rhs.targets.size() ); std::transform( rhs.targets.begin(), rhs.targets.end(), std::back_inserter( targets ), []( const item_location & e ) { return e.clone(); } ); return *this; } 

Dieser Code hat auch keine Prüfung gegen Selbstzuweisung und zusätzlich einen zu füllenden Vektor. Bei dieser Implementierung des Zuweisungsoperators führt das Zuweisen eines Objekts zu sich selbst zu einer Verdoppelung des Vektors im Zielfeld , wobei einige der Elemente beschädigt werden. Der Transformation geht jedoch clear voraus, wodurch der Vektor des Objekts gelöscht wird, was zu Datenverlust führt.

Bild 3

Exkurs iii


Im Jahr 2008 erhielten Roguelikes sogar eine formale Definition, die unter dem epischen Titel "Berlin Interpretation" bekannt ist. Demnach teilen alle diese Spiele die folgenden Elemente:

  • Zufällig erzeugte Welt, die die Wiederspielbarkeit erhöht;
  • Permadeath: Wenn dein Charakter stirbt, stirbt er für immer und alle seine Gegenstände gehen verloren.
  • Rundenbasiertes Gameplay: Änderungen treten nur zusammen mit den Aktionen des Spielers auf. Der Zeitfluss wird unterbrochen, bis der Spieler eine Aktion ausführt.
  • Überleben: Ressourcen sind knapp.

Schließlich ist das wichtigste Merkmal von Roguelikes, sich hauptsächlich darauf zu konzentrieren, die Welt zu erkunden, neue Verwendungsmöglichkeiten für Gegenstände zu finden und Dungeons zu kriechen.

In Cataclysm DDA ist es eine häufige Situation, dass Ihr Charakter bis auf die Knochen gefroren, hungrig und durstig ist und, um das Ganze abzurunden, seine beiden Beine durch sechs Tentakel ersetzt wird.

Bild 15

Details, die wichtig sind


Beispiel 9:

V1028 Möglicher Überlauf. Ziehen Sie in Betracht, Operanden des Operators 'start + large' in den Typ 'size_t' umzuwandeln, nicht in das Ergebnis. worldfactory.cpp 638

 void worldfactory::draw_mod_list( int &start, .... ) { .... int larger = ....; unsigned int iNum = ....; .... for( .... ) { if( iNum >= static_cast<size_t>( start ) && iNum < static_cast<size_t>( start + larger ) ) { .... } .... } .... } 

Es sieht so aus, als wollte der Programmierer Vorsichtsmaßnahmen gegen einen Überlauf treffen. Das Heraufstufen des Summentyps macht jedoch keinen Unterschied, da der Überlauf vorher beim Hinzufügen der Werte auftritt und das Heraufstufen über einen bedeutungslosen Wert erfolgt. Um dies zu vermeiden, sollte nur eines der Argumente in einen breiteren Typ umgewandelt werden: (static_cast <size_t> (start) + größer) .

Beispiel 10:

V530 Der Rückgabewert der Funktion 'Größe' muss verwendet werden. worldfactory.cpp 1340

 bool worldfactory::world_need_lua_build( std::string world_name ) { #ifndef LUA .... #endif // Prevent unused var error when LUA and RELEASE enabled. world_name.size(); return false; } 

Für solche Fälle gibt es einen Trick. Wenn Sie am Ende eine nicht verwendete Variable haben und die Compiler-Warnung unterdrücken möchten, schreiben Sie einfach (void) world_name, anstatt Methoden für diese Variable aufzurufen.

Beispiel 11:

V812 Leistungsminderung. Ineffektive Verwendung der Zählfunktion. Es kann möglicherweise durch den Aufruf der Funktion 'find' ersetzt werden. player.cpp 9600

 bool player::read( int inventory_position, const bool continuous ) { .... player_activity activity; if( !continuous || !std::all_of( learners.begin(), learners.end(), [&]( std::pair<npc *, std::string> elem ) { return std::count( activity.values.begin(), activity.values.end(), elem.first->getID() ) != 0; } ) { .... } .... } 

Die Tatsache, dass count mit null verglichen wird, legt nahe, dass der Programmierer herausfinden wollte, ob die Aktivität mindestens ein erforderliches Element enthält. Die Zählung muss jedoch durch den gesamten Container laufen, da alle Vorkommen des Elements gezählt werden. Die Arbeit könnte schneller erledigt werden, indem find verwendet wird , das stoppt, sobald das erste Vorkommen gefunden wurde.

Beispiel 12:

Dieser Fehler ist leicht zu finden, wenn Sie ein schwieriges Detail über den Zeichentyp kennen .

V739 EOF sollte nicht mit einem Wert vom Typ 'char' verglichen werden. Das 'ch' sollte vom Typ 'int' sein. json.cpp 762

 void JsonIn::skip_separator() { signed char ch; .... if (ch == ',') { if( ate_separator ) { .... } .... } else if (ch == EOF) { .... } 

Bild 13

Dies ist einer der Fehler, die Sie nur dann leicht erkennen können, wenn Sie wissen, dass EOF als -1 definiert ist. Wenn Sie es mit einer Variablen vom Typ char mit Vorzeichen vergleichen, wird die Bedingung daher in fast allen Fällen als falsch ausgewertet. Die einzige Ausnahme ist das Zeichen, dessen Code 0xFF (255) ist. Bei Verwendung in einem Vergleich wird -1 angezeigt, wodurch die Bedingung erfüllt wird.

Beispiel 13:

Dieser kleine Fehler kann eines Tages kritisch werden. Es gibt schließlich gute Gründe dafür, dass es auf der CWE-Liste als CWE-834 steht . Beachten Sie, dass das Projekt diese Warnung fünfmal ausgelöst hat.

V663 Endlosschleife ist möglich. Die Bedingung 'cin.eof ()' reicht nicht aus, um die Schleife zu verlassen. Fügen Sie dem bedingten Ausdruck möglicherweise den Funktionsaufruf 'cin.fail ()' hinzu. action.cpp 46

 void parse_keymap( std::istream &keymap_txt, .... ) { while( !keymap_txt.eof() ) { .... } } 

Wie in der Warnung angegeben, reicht es nicht aus, beim Lesen aus der Datei nach EOF zu suchen . Sie müssen auch nach einem Eingabefehler suchen, indem Sie cin.fail () aufrufen. Korrigieren wir den Code, um ihn sicherer zu machen:

 while( !keymap_txt.eof() ) { if(keymap_txt.fail()) { keymap_txt.clear(); keymap_txt.ignore(numeric_limits<streamsize>::max(),'\n'); break; } .... } 

Der Zweck von keymap_txt.clear () besteht darin, den Fehlerstatus (Flag) im Stream zu löschen, nachdem ein Lesefehler aufgetreten ist, damit Sie den Rest des Textes lesen können. Wenn Sie keymap_txt.ignore mit den Parametern numeric_limits <streamsize> :: max () und Newline-Zeichen aufrufen, können Sie den verbleibenden Teil der Zeichenfolge überspringen.

Es gibt eine viel einfachere Möglichkeit, das Lesen zu stoppen:

 while( !keymap_txt ) { .... } 

Im logischen Kontext wandelt sich der Stream in einen Wert um, der true entspricht, bis EOF erreicht ist.

Exkurs iv


Die beliebtesten Roguelike-bezogenen Spiele unserer Zeit kombinieren die Elemente von Original-Roguelike-Spielen und anderen Genres wie Plattformspielern, Strategien usw. Solche Spiele sind als "roguelike-like" oder "roguelite" bekannt geworden. Unter diesen sind so berühmte Titel wie Don't Starve , Die Bindung von Isaac , FTL: Schneller als Licht , Darkest Dungeon und sogar Diablo .

Der Unterschied zwischen Roguelike und Roguelite kann jedoch manchmal so gering sein, dass man nicht genau sagen kann, in welche Kategorie das Spiel gehört. Einige argumentieren, dass die Zwergenfestung im engeren Sinne kein Roguelike ist, während andere glauben, dass Diablo ein klassisches Roguelike-Spiel ist.

Bild 1

Fazit


Obwohl sich das Projekt im Allgemeinen als qualitativ hochwertig erwiesen hat und nur wenige schwerwiegende Mängel aufweist, bedeutet dies nicht, dass es auf statische Analysen verzichten kann. Die Möglichkeiten der statischen Analyse werden regelmäßig genutzt und nicht einmalig überprüft, wie wir sie für die Popularisierung durchführen. Bei regelmäßiger Verwendung helfen Ihnen statische Analysegeräte dabei, Fehler in der frühesten Entwicklungsphase zu erkennen und sie daher billiger zu beheben. Beispielberechnungen .

Bild 2

Das Spiel wird noch intensiv entwickelt und von einer aktiven Modder-Community bearbeitet. Übrigens wurde es auf mehrere Plattformen portiert, darunter iOS und Android. Also, wenn Sie interessiert sind, probieren Sie es aus!

Source: https://habr.com/ru/post/de449488/


All Articles