Cataclysm Dark Days Ahead: Análisis estático y juegos Roguelike

Cuadro 5

Ya debe haber adivinado por el título que el artículo de hoy se centrará en los errores en el código fuente del software. Pero no solo eso. Si no solo está interesado en C ++ y en leer sobre errores en el código de otros desarrolladores, sino también en cavar videojuegos inusuales y preguntarse qué son los "roguelikes" y cómo los juega, ¡bienvenido a seguir leyendo!

Mientras buscaba juegos inusuales, me topé con Cataclysm Dark Days Ahead , que se destaca entre otros juegos gracias a sus gráficos basados ​​en caracteres ASCII de varios colores dispuestos en el fondo negro.

Una cosa que te sorprende de este y otros juegos similares es cuánta funcionalidad está integrada en ellos. Particularmente en Cataclysm , por ejemplo, ni siquiera puedes crear un personaje sin sentir la necesidad de buscar en Google algunas guías debido a las docenas de parámetros, rasgos y escenarios iniciales disponibles, sin mencionar las múltiples variaciones de eventos que ocurren a lo largo del juego.

Dado que es un juego con código de código abierto y uno escrito en C ++, no podríamos pasar sin verificarlo con nuestro analizador de código estático PVS-Studio, en cuyo desarrollo participo activamente. El código del proyecto es sorprendentemente de alta calidad, pero aún tiene algunos defectos menores, de los cuales hablaré en este artículo.

Ya se han comprobado muchos juegos con PVS-Studio. Puede encontrar algunos ejemplos en nuestro artículo " Análisis estático en el desarrollo de videojuegos: Top 10 errores de software ".

La lógica


Ejemplo 1:

Este ejemplo muestra un error clásico de copiar y pegar.

V501 Hay subexpresiones idénticas a la izquierda y a la derecha de '||' operador: 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 misma condición se verifica dos veces. El programador copió la expresión pero olvidó modificar la copia. No estoy seguro de si se trata de un error crítico, pero el hecho es que la verificación no funciona como estaba previsto.

Otro error similar:

  • V501 Hay subexpresiones idénticas 'one_in (100000 / to_turns <int> (dur))' a la izquierda y a la derecha del operador '&&'. player_hardcoded_effects.cpp 547

Cuadro 11

Ejemplo 2

V728 Se puede simplificar una verificación excesiva. El '(A y B) || (! A &&! B) 'expresión es equivalente a la expresión' bool (A) == bool (B) '. Inventory_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 .... } .... } 

Esta condición es lógicamente correcta, pero es demasiado complicada. Quien escribió este código debería haber tenido lástima de sus compañeros programadores que lo mantendrán. Podría reescribirse en una forma más simple: if (left_fav == right_fav) .

Otro error similar:

  • V728 Se puede simplificar una verificación excesiva. El '(A &&! B) || (! A && B) 'expresión es equivalente a la expresión' bool (A)! = Bool (B) '. iuse_actor.cpp 2653

Digresión i


Me sorprendió descubrir que los juegos que hoy se conocen con el nombre de "roguelikes" son solo representantes más moderados del antiguo género de los juegos de roguelike. Todo comenzó con el juego de culto Rogue de 1980, que inspiró a muchos estudiantes y programadores a crear sus propios juegos con elementos similares. Supongo que también influyó mucho la comunidad del juego de mesa DnD y sus variaciones.

Cuadro 8

Micro optimizaciones


Ejemplo 3

Las advertencias de este grupo apuntan a puntos que podrían optimizarse en lugar de errores.

V801 Disminución del rendimiento. Es mejor redefinir el argumento de la segunda función como referencia. Considere reemplazar 'const ... type' con '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; } 

En este código, itype_id es en realidad un std :: string disfrazado. Dado que el argumento se pasa como una constante de todos modos, lo que significa que es inmutable, simplemente pasar una referencia a la variable ayudaría a mejorar el rendimiento y ahorrar recursos computacionales al evitar la operación de copia. Y a pesar de que es poco probable que la cadena sea larga, copiarla cada vez sin una buena razón es una mala idea, más aún porque varias personas llaman a esta función, que, a su vez, también teclean desde afuera y tienen para copiarlo

Problemas similares

  • V801 Disminución del rendimiento. Es mejor redefinir el argumento de la tercera función como referencia. Considere reemplazar 'const ... evt_filter' con 'const ... & evt_filter'. input.cpp 691
  • V801 Disminución del rendimiento. Es mejor redefinir el argumento de la quinta función como referencia. Considere reemplazar 'const ... color' con 'const ... & color'. salida.h 207
  • El analizador emitió un total de 32 advertencias de este tipo.

Ejemplo 4

V813 Disminución del rendimiento. El argumento 'str' probablemente debería representarse como una referencia 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; } 

Aunque el argumento no es constante, no cambia en el cuerpo de la función de ninguna manera. Por lo tanto, en aras de la optimización, una mejor solución sería pasarlo por referencia constante en lugar de forzar al compilador a crear copias locales.

Esta advertencia tampoco vino sola; El número total de advertencias de este tipo es 26.

Cuadro 7

Problemas similares

  • V813 Disminución del rendimiento. El argumento 'mensaje' probablemente debería representarse como una referencia constante. json.cpp 1452
  • V813 Disminución del rendimiento. El argumento 's' probablemente debería representarse como una referencia constante. catacharset.cpp 218
  • Y así sucesivamente ...

Digresión ii


Algunos de los juegos clásicos de roguelike todavía están en desarrollo activo. Si revisa los repositorios GitHub de Cataclysm DDA o NetHack , verá que los cambios se envían todos los días. NetHack es en realidad el juego más antiguo que aún se está desarrollando: se lanzó en julio de 1987 y la última versión data de 2018.

Dwarf Fortress es uno de los juegos más populares, aunque más jóvenes, del género. El desarrollo comenzó en 2002 y la primera versión se lanzó en 2006. Su lema "Perder es divertido" refleja el hecho de que es imposible ganar en este juego. En 2007, Dwarf Fortress fue galardonado con el "Mejor juego de Roguelike del año" al votar anualmente en el sitio ASCII GAMES.

Cuadro 6

Por cierto, los fanáticos podrían estar contentos de saber que Dwarf Fortress llegará a Steam con gráficos mejorados de 32 bits agregados por dos modders experimentados. La versión premium también obtendrá pistas de música adicionales y soporte para Steam Workshop. Los propietarios de copias pagas podrán cambiar a los gráficos ASCII antiguos si lo desean. Más

Anular el operador de asignación


Ejemplos 5, 6:

Aquí hay un par de advertencias interesantes.

V690 La clase 'JsonObject' implementa un constructor de copia, pero carece del operador '='. Es peligroso usar tal clase. 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() { .... } .... } 

Esta clase tiene un constructor de copia y un destructor, pero no anula el operador de asignación. El problema es que un operador de asignación generado automáticamente puede asignar el puntero solo a JsonIn . Como resultado, ambos objetos de la clase JsonObject estarían apuntando al mismo JsonIn . No puedo decir con certeza si tal situación podría ocurrir en la versión actual, pero alguien seguramente caerá en esta trampa algún día.

La próxima clase tiene un problema similar.

V690 La clase 'JsonArray' implementa un constructor de copia, pero carece del operador '='. Es peligroso usar tal clase. 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() { .... } } 

El peligro de no anular el operador de asignación en una clase compleja se explica en detalle en el artículo " La ley de los dos grandes ".

Ejemplos 7, 8:

Estos dos también se ocupan de la anulación del operador de asignación, pero esta vez implementaciones específicas de la misma.

V794 El operador de asignación debe estar protegido del caso 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; } 

Esta implementación no tiene protección contra la posible autoasignación, lo cual es una práctica insegura. Es decir, pasar un * esta referencia a este operador puede causar una pérdida de memoria.

Aquí hay un ejemplo similar de un operador de asignación anulado incorrectamente con un efecto secundario peculiar:

V794 El operador de asignación debe estar protegido del caso 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; } 

Este código tampoco verifica la autoasignación y, además, tiene un vector que se debe completar. Con esta implementación del operador de asignación, la asignación de un objeto a sí mismo dará como resultado la duplicación del vector en el campo de objetivos , y algunos de los elementos se corromperán. Sin embargo, la transformación está precedida por clear , que borrará el vector del objeto, lo que conducirá a la pérdida de datos.

Cuadro 3

Digresión iii


En 2008, roguelikes incluso obtuvo una definición formal conocida bajo el título épico "Interpretación de Berlín". Según él, todos estos juegos comparten los siguientes elementos:

  • Mundo generado aleatoriamente, lo que aumenta la capacidad de reproducción;
  • Permadeath: si tu personaje muere, muere para siempre, y todos sus objetos se pierden;
  • Juego basado en turnos: cualquier cambio ocurre solo junto con las acciones del jugador; el flujo de tiempo se suspende hasta que el jugador realiza una acción;
  • Supervivencia: los recursos son escasos.

Finalmente, la característica más importante de los roguelikes se centra principalmente en explorar el mundo, encontrar nuevos usos para los artículos y en el rastreo de mazmorras.

Es una situación común en Cataclysm DDA que tu personaje termine congelado hasta los huesos, hambriento, sediento y, para colmo, reemplazando sus dos patas con seis tentáculos.

Cuadro 15

Detalles que importan


Ejemplo 9

V1028 Posible desbordamiento. Considere convertir los operandos del operador 'inicio + más grande' al tipo 'size_t', no el resultado. 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 ) ) { .... } .... } .... } 

Parece que el programador quería tomar precauciones contra un desbordamiento. Sin embargo, promover el tipo de suma no hará ninguna diferencia porque el desbordamiento ocurrirá antes de eso, en el paso de agregar los valores, y la promoción se realizará sobre un valor sin sentido. Para evitar esto, solo uno de los argumentos debe convertirse a un tipo más amplio: (static_cast <size_t> (inicio) + más grande) .

Ejemplo 10

V530 Se requiere utilizar el valor de retorno de la función 'tamaño'. 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; } 

Hay un truco para casos como este. Si termina con una variable no utilizada y desea suprimir la advertencia del compilador, simplemente escriba (void) world_name en lugar de llamar a métodos en esa variable.

Ejemplo 11

V812 Disminución del rendimiento. Uso ineficaz de la función 'contar'. Posiblemente puede ser reemplazado por la llamada a la función 'buscar'. 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; } ) { .... } .... } 

El hecho de que el conteo se compara con cero sugiere que el programador quería averiguar si la actividad contenía al menos un elemento requerido. Pero count tiene que recorrer todo el contenedor ya que cuenta todas las ocurrencias del elemento. El trabajo se puede hacer más rápido usando find , que se detiene una vez que se ha encontrado la primera aparición.

Ejemplo 12

Este error es fácil de encontrar si conoce un detalle complicado sobre el tipo char .

V739 EOF no debe compararse con un valor del tipo 'char'. El 'ch' debe ser del tipo 'int'. json.cpp 762

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

Cuadro 13

Este es uno de los errores que no detectará fácilmente a menos que sepa que EOF se define como -1. Por lo tanto, al compararlo con una variable de tipo con signo char , la condición se evalúa como falsa en casi todos los casos. La única excepción es con el carácter cuyo código es 0xFF (255). Cuando se usa en una comparación, se convertirá en -1, lo que hace que la condición sea verdadera.

Ejemplo 13

Este pequeño error puede volverse crítico algún día. Hay buenas razones, después de todo, que se encuentra en la lista de CWE como CWE-834 . Tenga en cuenta que el proyecto ha activado esta advertencia cinco veces.

V663 Infinite loop es posible. La condición 'cin.eof ()' es insuficiente para romper el bucle. Considere agregar la llamada a la función 'cin.fail ()' a la expresión condicional. action.cpp 46

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

Como dice la advertencia, no es suficiente verificar si hay EOF al leer el archivo; también debe verificar si hay un error de entrada llamando a cin.fail () . Arreglemos el código para hacerlo más seguro:

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

El propósito de keymap_txt.clear () es borrar el estado de error (bandera) en la secuencia después de que ocurra un error de lectura para que pueda leer el resto del texto. Llamar a keymap_txt.ignore con los parámetros numeric_limits <streamsize> :: max () y el carácter de nueva línea le permite omitir la parte restante de la cadena.

Hay una manera mucho más simple de detener la lectura:

 while( !keymap_txt ) { .... } 

Cuando se pone en contexto lógico, la secuencia se convertirá en un valor equivalente a verdadero hasta que se alcance EOF .

Digresión iv


Los juegos más populares relacionados con roguelike de nuestro tiempo combinan los elementos de roguelikes originales y otros géneros como plataformas, estrategias, etc. Dichos juegos se conocen como "roguelike-like" o "roguelite". Entre estos se encuentran títulos tan famosos como Don't Starve , The Binding of Isaac , FTL: Faster Than Light , Darkest Dungeon e incluso Diablo .

Sin embargo, la distinción entre roguelike y roguelite a veces puede ser tan pequeña que no se puede saber con seguridad a qué categoría pertenece el juego. Algunos argumentan que Dwarf Fortress no es un roguelike en sentido estricto, mientras que otros creen que Diablo es un clásico juego de roguelike.

Imagen 1

Conclusión


Aunque el proyecto demostró ser generalmente de alta calidad, con solo unos pocos defectos graves, no significa que pueda prescindir del análisis estático. El poder del análisis estático se usa regularmente en lugar de verificaciones únicas como las que hacemos para popularización. Cuando se usan regularmente, los analizadores estáticos lo ayudan a detectar errores en la primera etapa de desarrollo y, por lo tanto, los hacen más baratos de solucionar. Ejemplos de cálculos .

Imagen 2

El juego aún se está desarrollando intensamente, con una comunidad modder activa trabajando en él. Por cierto, se ha portado a múltiples plataformas, incluidas iOS y Android. Entonces, si estás interesado, ¡pruébalo!

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


All Articles