Cataclysm Dark Days Ahead: analyse statique et jeux Roguelike

Image 5

Vous devez avoir deviné à partir du titre que l'article d'aujourd'hui se concentrera sur les bogues dans le code source du logiciel. Mais pas seulement ça. Si vous êtes non seulement intéressé par le C ++ et par la lecture de bogues dans le code d'autres développeurs, mais aussi que vous creusez des jeux vidéo inhabituels et que vous vous demandez ce que sont les "roguelikes" et comment vous les jouez, alors n'hésitez pas à lire!

En recherchant des jeux insolites, je suis tombé sur Cataclysm Dark Days Ahead , qui se démarque parmi d'autres jeux grâce à ses graphismes basés sur des caractères ASCII de différentes couleurs disposés sur le fond noir.

Une chose qui vous étonne à propos de ce jeu et d'autres jeux similaires est la quantité de fonctionnalités qui y sont intégrées. En particulier dans Cataclysm , par exemple, vous ne pouvez même pas créer un personnage sans ressentir le besoin de rechercher des guides sur Google en raison des dizaines de paramètres, de traits et de scénarios initiaux disponibles, sans parler des multiples variations d'événements qui se produisent tout au long du jeu.

Puisqu'il s'agit d'un jeu avec du code open-source, et un écrit en C ++, nous ne pouvions pas passer sans le vérifier avec notre analyseur de code statique PVS-Studio, au développement duquel je participe activement. Le code du projet est étonnamment de haute qualité, mais il a encore quelques défauts mineurs, dont je parlerai dans cet article.

Beaucoup de jeux ont déjà été vérifiés avec PVS-Studio. Vous pouvez trouver quelques exemples dans notre article " Analyse statique dans le développement de jeux vidéo: Top 10 des bugs logiciels ".

La logique


Exemple 1:

Cet exemple montre une erreur de copier-coller classique.

V501 Il existe des sous-expressions identiques à gauche et à droite de '||' opérateur: 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 ) ) { .... } .... } 

La même condition est vérifiée deux fois. Le programmeur a copié l'expression mais a oublié de modifier la copie. Je ne sais pas s'il s'agit d'un bogue critique, mais le fait est que la vérification ne fonctionne pas comme prévu.

Une autre erreur similaire:

  • V501 Il existe des sous-expressions identiques «one_in (100000 / to_turns <int> (dur))» à gauche et à droite de l'opérateur «&&». player_hardcoded_effects.cpp 547

Image 11

Exemple 2:

V728 Un contrôle excessif peut être simplifié. Le '(A && B) || (! A &&! B) 'est équivalente à l'expression' bool (A) == bool (B) '. inventaire_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 .... } .... } 

Cette condition est logiquement correcte, mais elle est trop compliquée. Celui qui a écrit ce code aurait dû avoir pitié de leurs collègues programmeurs qui le maintiendront. Il pourrait être réécrit sous une forme plus simple: if (left_fav == right_fav) .

Une autre erreur similaire:

  • V728 Un contrôle excessif peut être simplifié. Le '(A &&! B) || (! A && B) 'est équivalente à l'expression' bool (A)! = Bool (B) '. iuse_actor.cpp 2653

Digression i


J'ai été surpris de découvrir que les jeux portant le nom de "roguelikes" ne sont aujourd'hui que des représentants plus modérés de l'ancien genre des jeux roguelike. Tout a commencé avec le jeu culte Rogue de 1980, qui a inspiré de nombreux étudiants et programmeurs à créer leurs propres jeux avec des éléments similaires. Je suppose que beaucoup d'influence est également venue de la communauté du jeu de table DnD et de ses variantes.

Image 8

Micro-optimisations


Exemple 3:

Les avertissements de ce groupe pointent vers des spots qui pourraient potentiellement être optimisés plutôt que des bugs.

V801 Diminution des performances. Il est préférable de redéfinir le deuxième argument de fonction comme référence. Pensez à remplacer "const ... type" par "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; } 

Dans ce code, itype_id est en fait une chaîne std :: déguisée. Comme l'argument est de toute façon passé comme une constante, ce qui signifie qu'il est immuable, le simple fait de passer une référence à la variable aiderait à améliorer les performances et à économiser les ressources de calcul en évitant l'opération de copie. Et même s'il est peu probable que la chaîne soit longue, la copier à chaque fois sans bonne raison est une mauvaise idée - d'autant plus que cette fonction est appelée par divers appelants, qui, à leur tour, obtiennent également un type de l'extérieur et ont pour le copier.

Problèmes similaires:

  • V801 Diminution des performances. Il est préférable de redéfinir le troisième argument de fonction comme référence. Pensez à remplacer 'const ... evt_filter' par 'const ... & evt_filter'. input.cpp 691
  • V801 Diminution des performances. Il est préférable de redéfinir le cinquième argument de fonction comme référence. Pensez à remplacer «const ... color» par «const ... & color». output.h 207
  • L'analyseur a émis un total de 32 avertissements de ce type.

Exemple 4:

V813 Performances réduites . L'argument 'str' devrait probablement être rendu comme une référence constante. 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; } 

Bien que l'argument ne soit pas constant, il ne change en aucune façon dans le corps de la fonction. Par conséquent, dans un souci d'optimisation, une meilleure solution serait de le passer par référence constante plutôt que de forcer le compilateur à créer des copies locales.

Cet avertissement n'est pas venu seul non plus; le nombre total d'avertissements de ce type est de 26.

Image 7

Problèmes similaires:

  • V813 Performances réduites. L'argument 'message' devrait probablement être restitué comme une référence constante. json.cpp 1452
  • V813 Performances réduites. L'argument 's' devrait probablement être rendu comme une référence constante. catacharset.cpp 218
  • Et ainsi de suite ...

Digression ii


Certains des jeux roguelike classiques sont toujours en développement actif. Si vous vérifiez les référentiels GitHub de Cataclysm DDA ou NetHack , vous verrez que les modifications sont soumises tous les jours. NetHack est en fait le plus ancien jeu en cours de développement: il est sorti en juillet 1987 et la dernière version remonte à 2018.

Dwarf Fortress est l'un des jeux les plus populaires - bien que plus jeunes - du genre. Le développement a commencé en 2002 et la première version est sortie en 2006. Sa devise "Losing is fun" reflète le fait qu'il est impossible de gagner dans ce jeu. En 2007, Dwarf Fortress a reçu le prix du meilleur jeu Roguelike de l'année en votant chaque année sur le site ASCII GAMES.

Image 6

Soit dit en passant, les fans pourraient être heureux de savoir que Dwarf Fortress arrive sur Steam avec des graphiques 32 bits améliorés ajoutés par deux modders expérimentés. La version premium bénéficiera également de morceaux de musique supplémentaires et de la prise en charge de Steam Workshop. Les propriétaires de copies payantes pourront passer aux anciens graphiques ASCII s'ils le souhaitent. En savoir plus

Substitution de l'opérateur d'affectation


Exemples 5, 6:

Voici quelques avertissements intéressants.

V690 La classe 'JsonObject' implémente un constructeur de copie, mais il manque l'opérateur '='. Il est dangereux d'utiliser une telle classe. 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() { .... } .... } 

Cette classe a un constructeur de copie et un destructeur mais ne remplace pas l'opérateur d'affectation. Le problème est qu'un opérateur d'affectation généré automatiquement ne peut affecter le pointeur qu'à JsonIn . Par conséquent, les deux objets de la classe JsonObject pointeraient vers le même JsonIn . Je ne peux pas dire avec certitude si une telle situation pourrait se produire dans la version actuelle, mais quelqu'un tombera sûrement dans ce piège un jour.

La classe suivante a un problème similaire.

V690 La classe 'JsonArray' implémente un constructeur de copie, mais il manque l'opérateur '='. Il est dangereux d'utiliser une telle classe. 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() { .... } } 

Le danger de ne pas remplacer l'opérateur d'affectation dans une classe complexe est expliqué en détail dans l'article " La loi des deux grands ".

Exemples 7, 8:

Ces deux éléments traitent également de la priorité des opérateurs d'affectation, mais cette fois-ci des implémentations spécifiques.

V794 L'opérateur d'affectation doit être protégé contre le cas de 'this == & other'. 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; } 

Cette implémentation n'a aucune protection contre une auto-affectation potentielle, ce qui est une pratique dangereuse. Autrement dit, le fait de passer une * cette référence à cet opérateur peut provoquer une fuite de mémoire.

Voici un exemple similaire d'un opérateur d'affectation incorrectement remplacé avec un effet secondaire particulier:

V794 L'opérateur d'affectation doit être protégé contre le cas de 'this == & rhs'. 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; } 

Ce code n'a aucune vérification contre l'auto-affectation non plus, et en plus, il a un vecteur à remplir. Avec cette implémentation de l'opérateur d'affectation, l'attribution d'un objet à lui-même se traduira par un doublement du vecteur dans le champ cibles , avec certains éléments corrompus. Cependant, la transformation est précédée de clear , qui effacera le vecteur de l'objet, entraînant ainsi une perte de données.

Image 3

Digression iii


En 2008, les roguelikes ont même obtenu une définition formelle connue sous le titre épique "Interprétation de Berlin". Selon lui, tous ces jeux partagent les éléments suivants:

  • Monde généré aléatoirement, ce qui augmente la rejouabilité;
  • Permadeath: si votre personnage meurt, il meurt pour de bon et tous ses objets sont perdus;
  • Gameplay au tour par tour: tout changement ne se produit qu'avec les actions du joueur; l'écoulement du temps est suspendu jusqu'à ce que le joueur effectue une action;
  • Survie: les ressources sont rares.

Enfin, la caractéristique la plus importante des roguelikes se concentre principalement sur l'exploration du monde, la recherche de nouvelles utilisations pour les objets et l'exploration de donjons.

C'est une situation courante dans Cataclysm DDA pour que votre personnage se retrouve gelé jusqu'à l'os, affamé, assoiffé et, pour couronner le tout, avoir ses deux jambes remplacées par six tentacules.

Image 15

Des détails qui comptent


Exemple 9:

V1028 Débordement possible. Envisagez de transposer des opérandes de l'opérateur 'start + large' en type 'size_t', pas le résultat. 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 ) ) { .... } .... } .... } 

Il semble que le programmeur ait voulu prendre des précautions contre un débordement. Cependant, la promotion du type de la somme ne fera aucune différence car le débordement se produira avant cela, à l'étape d'ajout des valeurs, et la promotion se fera sur une valeur vide de sens. Pour éviter cela, un seul des arguments doit être converti en un type plus large: (static_cast <size_t> (start) + large) .

Exemple 10:

V530 La valeur de retour de la fonction 'taille' doit être utilisée. 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; } 

Il y a une astuce pour des cas comme celui-ci. Si vous vous retrouvez avec une variable inutilisée et que vous souhaitez supprimer l'avertissement du compilateur, écrivez simplement (void) world_name au lieu d'appeler des méthodes sur cette variable.

Exemple 11:

V812 Performances réduites . Utilisation inefficace de la fonction «comptage». Il peut éventuellement être remplacé par l'appel à la fonction 'find'. 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; } ) { .... } .... } 

Le fait que le nombre soit comparé à zéro suggère que le programmeur voulait savoir si l' activité contenait au moins un élément requis. Mais count doit parcourir tout le conteneur car il compte toutes les occurrences de l'élément. Le travail pourrait être effectué plus rapidement en utilisant find , qui s'arrête une fois que la première occurrence a été trouvée.

Exemple 12:

Ce bug est facile à trouver si vous connaissez un détail délicat sur le type de caractère.

V739 EOF ne doit pas être comparé à une valeur de type 'char'. Le «ch» doit être de type «int». json.cpp 762

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

Image 13

C'est l'une des erreurs que vous ne repérerez pas facilement, sauf si vous savez que EOF est défini comme -1. Par conséquent, lors de la comparaison avec une variable de type signé char , la condition est évaluée à false dans presque tous les cas. La seule exception concerne le caractère dont le code est 0xFF (255). Lorsqu'il est utilisé dans une comparaison, il passe à -1, ce qui rend la condition vraie.

Exemple 13:

Ce petit bug peut devenir critique un jour. Il y a de bonnes raisons, après tout, qu'il se trouve sur la liste CWE en tant que CWE-834 . Notez que le projet a déclenché cet avertissement cinq fois.

La boucle infinie V663 est possible. La condition «cin.eof ()» est insuffisante pour rompre la boucle. Pensez à ajouter l'appel de fonction 'cin.fail ()' à l'expression conditionnelle. action.cpp 46

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

Comme le dit l'avertissement, il ne suffit pas de vérifier EOF lors de la lecture du fichier - vous devez également vérifier un échec d'entrée en appelant cin.fail () . Corrigeons le code pour le rendre plus sûr:

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

Le but de keymap_txt.clear () est d'effacer l'état d'erreur (indicateur) sur le flux après qu'une erreur de lecture se soit produite afin que vous puissiez lire le reste du texte. L'appel de keymap_txt.ignore avec les paramètres numeric_limits <streamsize> :: max () et le caractère de nouvelle ligne vous permet d'ignorer la partie restante de la chaîne.

Il existe un moyen beaucoup plus simple d'arrêter la lecture:

 while( !keymap_txt ) { .... } 

Lorsqu'il est placé dans un contexte logique, le flux se convertira en une valeur équivalente à true jusqu'à ce que EOF soit atteint.

Digression iv


Les jeux liés aux roguelikes les plus populaires de notre temps combinent les éléments des roguelikes originaux et d'autres genres tels que les jeux de plateforme, les stratégies, etc. Ces jeux sont devenus connus sous le nom de "roguelike-like" ou "roguelite". Parmi ceux-ci figurent des titres célèbres tels que Don't Starve , The Binding of Isaac , FTL: Faster Than Light , Darkest Dungeon et même Diablo .

Cependant, la distinction entre roguelike et roguelite peut parfois être si minuscule que vous ne pouvez pas savoir avec certitude dans quelle catégorie le jeu appartient. Certains soutiennent que Dwarf Fortress n'est pas un roguelike au sens strict, tandis que d'autres pensent que Diablo est un jeu roguelike classique.

Image 1

Conclusion


Même si le projet s'est avéré généralement de haute qualité, avec seulement quelques défauts graves, cela ne signifie pas qu'il peut se passer d'une analyse statique. La puissance de l'analyse statique est utilisée régulièrement plutôt que des contrôles ponctuels comme ceux que nous faisons pour la vulgarisation. Lorsqu'ils sont utilisés régulièrement, les analyseurs statiques vous aident à détecter les bogues dès les premiers stades de développement et, par conséquent, les rendent moins coûteux à corriger. Exemples de calculs .

Image 2

Le jeu est toujours en cours de développement intensif, avec une communauté active de moddeurs qui y travaillent. Soit dit en passant, il a été porté sur plusieurs plates-formes, y compris iOS et Android. Donc, si vous êtes intéressé, essayez-le!

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


All Articles