Você já deve ter adivinhado a partir do título que o artigo de hoje se concentrará em erros no código-fonte do software. Mas não é só isso. Se você não está apenas interessado em C ++ e em ler sobre bugs no código de outros desenvolvedores, mas também curte jogos de vídeo incomuns e se pergunta o que são "roguelikes" e como você os joga, então continue a ler!
Enquanto procurava por jogos incomuns, me deparei com o
Cataclysm Dark Days Ahead , que se destaca entre outros jogos graças aos seus gráficos baseados em caracteres ASCII de várias cores dispostas em fundo preto.
Uma coisa que surpreende você sobre esse e outros jogos similares é a quantidade de funcionalidade incorporada a eles. Particularmente no
Cataclysm , por exemplo, você não pode nem criar um personagem sem ter vontade de pesquisar alguns guias por causa das dezenas de parâmetros, características e cenários iniciais disponíveis, sem mencionar as múltiplas variações de eventos que ocorrem ao longo do jogo.
Como é um jogo com código-fonte aberto e um escrito em C ++, não conseguimos passar sem verificá-lo com o nosso analisador de código estático PVS-Studio, no desenvolvimento do qual estou participando ativamente. O código do projeto é surpreendentemente de alta qualidade, mas ainda possui alguns defeitos menores, sobre os quais falarei neste artigo.
Muitos jogos já foram verificados com o PVS-Studio. Você pode encontrar alguns exemplos em nosso artigo "
Análise estática no desenvolvimento de videogames: os 10 principais erros de software ".
Lógica
Exemplo 1:Este exemplo mostra um erro clássico de copiar e colar.
V501 Existem
subexpressões idênticas à esquerda e à direita do '||' 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 ) ) { .... } .... }
A mesma condição é verificada duas vezes. O programador copiou a expressão, mas esqueceu de modificar a cópia. Não tenho certeza se esse é um bug crítico, mas o fato é que a verificação não funciona como deveria.
Outro erro semelhante:
- V501 Existem subexpressões idênticas 'one_in (100000 / to_turns <int> (dur))' à esquerda e à direita do operador '&&'. player_hardcoded_effects.cpp 547
Exemplo 2:V728 Uma verificação excessiva pode ser simplificada. O '(A && B) || (! A &&! B) 'expressão é equivalente à expressão' bool (A) == bool (B) '. inventário_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 .... } .... }
Essa condição é logicamente correta, mas é complicada demais. Quem escreveu esse código deveria ter pena de seus colegas programadores que o manterão. Pode ser reescrito de uma forma mais simples:
if (left_fav == right_fav) .
Outro erro semelhante:
- V728 Uma verificação excessiva pode ser simplificada. O '(A &&! B) || (! A && B) 'expressão é equivalente à expressão' bool (A)! = Bool (B) '. iuse_actor.cpp 2653
Digressão i
Fiquei surpreso ao descobrir que jogos com o nome de "roguelikes" hoje são apenas representantes mais moderados do velho gênero de jogos roguelike. Tudo começou com o jogo de culto
Rogue de 1980, que inspirou muitos estudantes e programadores a criar seus próprios jogos com elementos semelhantes. Eu acho que muita influência também veio da comunidade do jogo de mesa
DnD e suas variações.
Micro-otimizações
Exemplo 3:Os avisos desse grupo apontam para pontos que podem ser potencialmente otimizados em vez de bugs.
V801 desempenho reduzido. É melhor redefinir o argumento da segunda função como referência. Considere substituir 'const ... type' por '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; }
Nesse código,
itype_id é na verdade uma
std :: string disfarçada. Como o argumento é passado como uma constante de qualquer maneira, o que significa que é imutável, simplesmente passar uma referência à variável ajudaria a melhorar o desempenho e economizar recursos computacionais, evitando a operação de cópia. E mesmo que seja improvável que a sequência seja longa, copiá-la sempre sem uma boa razão é uma má idéia - ainda mais porque essa função é chamada por vários chamadores, que, por sua vez, também recebem o
tipo de fora e têm para copiá-lo.
Problemas semelhantes:
- V801 desempenho reduzido. É melhor redefinir o argumento da terceira função como referência. Considere substituir 'const ... evt_filter' por 'const ... & evt_filter'. input.cpp 691
- V801 desempenho reduzido. É melhor redefinir o argumento da quinta função como referência. Considere substituir 'const ... color' por 'const ... & color'. output.h 207
- O analisador emitiu um total de 32 avisos desse tipo.
Exemplo 4:V813 Desempenho Diminuído. O argumento 'str' provavelmente deve ser renderizado como uma referência 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; }
Embora o argumento não seja constante, ele não muda de maneira alguma no corpo da função. Portanto, por uma questão de otimização, uma solução melhor seria passar por referência constante, em vez de forçar o compilador a criar cópias locais.
Este aviso também não veio sozinho; o número total de avisos desse tipo é 26.
Problemas semelhantes:
- V813 Desempenho Diminuído. O argumento 'message' provavelmente deve ser renderizado como uma referência constante. json.cpp 1452
- V813 Desempenho Diminuído. O argumento 's' provavelmente deve ser renderizado como uma referência constante. catacharset.cpp 218
- E assim por diante ...
Digressão ii
Alguns dos clássicos jogos roguelike ainda estão em desenvolvimento. Se você verificar os repositórios GitHub do
Cataclysm DDA ou
NetHack , verá que as alterações são enviadas todos os dias.
O NetHack é realmente o jogo mais antigo que ainda está sendo desenvolvido: lançado em julho de 1987, e a última versão remonta a 2018.
Dwarf Fortress é um dos jogos mais populares - embora mais jovens - do gênero. O desenvolvimento começou em 2002 e a primeira versão foi lançada em 2006. Seu lema "Perder é divertido" reflete o fato de que é impossível vencer neste jogo. Em 2007, o
Dwarf Fortress foi premiado como "Melhor Jogo Roguelike do Ano" por votação realizada anualmente no site ASCII GAMES.
A propósito, os fãs podem ficar felizes em saber que
Dwarf Fortress está chegando ao Steam com gráficos aprimorados de 32 bits adicionados por dois modders experientes. A versão premium também terá faixas de música adicionais e suporte para o Steam Workshop. Os proprietários de cópias pagas poderão mudar para os gráficos ASCII antigos, se desejarem.
MaisSubstituindo o operador de atribuição
Exemplos 5, 6:Aqui estão alguns avisos interessantes.
V690 A classe 'JsonObject' implementa um construtor de cópia, mas não possui o operador '='. É perigoso usar essa 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();
Essa classe tem um construtor de cópia e um destruidor, mas não substitui o operador de atribuição. O problema é que um operador de atribuição gerado automaticamente pode atribuir o ponteiro apenas ao
JsonIn . Como resultado, os dois objetos da classe
JsonObject apontariam para o mesmo
JsonIn . Não posso dizer com certeza se tal situação pode ocorrer na versão atual, mas alguém certamente cairá nessa armadilha um dia.
A próxima aula tem um problema semelhante.
V690 A classe 'JsonArray' implementa um construtor de cópia, mas não possui o operador '='. É perigoso usar essa 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();
O perigo de não substituir o operador de atribuição em uma classe complexa é explicado em detalhes no artigo "
A lei das duas grandes ".
Exemplos 7, 8:Esses dois também lidam com a substituição do operador de atribuição, mas desta vez com implementações específicas.
V794 O operador de atribuição deve ser protegido do caso '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; }
Essa implementação não tem proteção contra a auto-atribuição potencial, o que é uma prática insegura. Ou seja, passar uma
* esta referência a este operador pode causar um vazamento de memória.
Aqui está um exemplo semelhante de um operador de atribuição substituído incorretamente com um efeito colateral peculiar:
V794 O operador de atribuição deve ser protegido do 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; }
Esse código também não possui verificação de auto-atribuição e, além disso, possui um vetor a ser preenchido. Com esta implementação do operador de atribuição, atribuir um objeto a si próprio resultará na duplicação do vetor no campo de
destinos , com alguns dos elementos sendo corrompidos. No entanto, a
transformação é precedida por
clear , que limpará o vetor do objeto, levando à perda de dados.
Digressão iii
Em 2008, roguelikes chegou a ter uma definição formal conhecida sob o épico título "Interpretação de Berlim". Segundo ele, todos esses jogos compartilham os seguintes elementos:
- Mundo gerado aleatoriamente, o que aumenta a repetibilidade;
- Permadeath: se seu personagem morre, eles morrem para sempre, e todos os seus itens são perdidos;
- Jogabilidade baseada em turnos: quaisquer alterações ocorrem apenas junto com as ações do jogador; o fluxo de tempo é suspenso até o jogador executar uma ação;
- Sobrevivência: os recursos são escassos.
Finalmente, a característica mais importante dos roguelikes está focada principalmente em explorar o mundo, encontrar novos usos para itens e rastrear masmorras.
É uma situação comum no
Cataclysm DDA para você acabar congelando até os ossos, morrendo de fome, com sede e, ainda por cima, tendo as duas pernas substituídas por seis tentáculos.
Detalhes importantes
Exemplo 9:V1028 Possível estouro. Considere converter operandos do operador 'start + maior' para o tipo 'size_t', não o 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 o programador queria tomar precauções contra um estouro. No entanto, promover o tipo da soma não fará nenhuma diferença, pois o estouro ocorrerá antes disso, na etapa de adição dos valores, e a promoção será realizada por um valor sem sentido. Para evitar isso, apenas um dos argumentos deve ser convertido em um tipo mais amplo:
(static_cast <size_t> (start) + maior) .
Exemplo 10:V530 O valor de retorno da função 'tamanho' deve ser utilizado. worldfactory.cpp 1340
bool worldfactory::world_need_lua_build( std::string world_name ) { #ifndef LUA .... #endif
Há um truque para casos como este. Se você acabar com uma variável não utilizada e deseja suprimir o aviso do compilador, basta escrever
(void) world_name em vez de chamar métodos nessa variável.
Exemplo 11:V812 Desempenho Diminuído. Uso ineficaz da função 'count'. Pode ser substituído pela chamada para a função 'localizar'. 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; } ) { .... } .... }
O fato de a
contagem ser comparada com zero sugere que o programador quis descobrir se a
atividade continha pelo menos um elemento necessário. Mas
count precisa percorrer todo o contêiner, pois conta todas as ocorrências do elemento. O trabalho pode ser feito mais rapidamente usando o
find , que para quando a primeira ocorrência é encontrada.
Exemplo 12:É fácil encontrar esse bug se você souber um detalhe complicado sobre o tipo de
caractere .
O V739 EOF não deve ser comparado com um valor do tipo 'char'. O 'ch' deve ser do tipo 'int'. json.cpp 762
void JsonIn::skip_separator() { signed char ch; .... if (ch == ',') { if( ate_separator ) { .... } .... } else if (ch == EOF) { .... }
Este é um dos erros que você não encontrará facilmente, a menos que saiba que o
EOF está definido como -1. Portanto, ao compará-lo com uma variável do tipo
char assinado , a condição é avaliada como
falsa em quase todos os casos. A única exceção é com o caractere cujo código é 0xFF (255). Quando usado em uma comparação, ele se tornará -1, tornando a condição verdadeira.
Exemplo 13:Este pequeno bug pode se tornar crítico algum dia. Afinal, existem boas razões para ele ser encontrado na lista da CWE como
CWE-834 . Observe que o projeto disparou esse aviso cinco vezes.
V663 Loop infinito é possível. A condição 'cin.eof ()' é insuficiente para interromper o loop. Considere adicionar a chamada de função 'cin.fail ()' à expressão condicional. action.cpp 46
void parse_keymap( std::istream &keymap_txt, .... ) { while( !keymap_txt.eof() ) { .... } }
Como o aviso diz, não é suficiente verificar o EOF ao ler o arquivo - você também precisa verificar se há uma falha de entrada chamando
cin.fail () . Vamos corrigir o código para torná-lo mais seguro:
while( !keymap_txt.eof() ) { if(keymap_txt.fail()) { keymap_txt.clear(); keymap_txt.ignore(numeric_limits<streamsize>::max(),'\n'); break; } .... }
O objetivo de
keymap_txt.clear () é limpar o estado do erro (sinalizador) no fluxo após a ocorrência de um erro de leitura, para que você possa ler o restante do texto. Chamar
keymap_txt.ignore com os parâmetros
numeric_limits <streamsize> :: max () e o caractere de nova linha permite ignorar a parte restante da sequência.
Há uma maneira muito mais simples de interromper a leitura:
while( !keymap_txt ) { .... }
Quando colocado no contexto lógico, o fluxo se converterá em um valor equivalente a
true até que o
EOF seja alcançado.
Digressão iv
Os jogos mais populares relacionados a roguelike do nosso tempo combinam os elementos de roguelike original e outros gêneros, como plataformas, estratégias e assim por diante. Esses jogos tornaram-se conhecidos como "roguelike-like" ou "roguelite". Entre esses, estão títulos famosos como
Don't Starve ,
The Binding of Isaac ,
FTL: Faster Than Light ,
Darkest Dungeon e até
Diablo .
No entanto, às vezes a distinção entre roguelike e roguelite pode ser tão pequena que você não sabe ao certo em qual categoria o jogo pertence. Alguns argumentam que o
Dwarf Fortress não é do tipo roguel no sentido estrito, enquanto outros acreditam que
Diablo é um jogo clássico do tipo roguel.
Conclusão
Embora o projeto tenha provado ser geralmente de alta qualidade, com apenas alguns defeitos graves, isso não significa que ele pode ficar sem análise estática. O poder da análise estática está em uso regular, em vez de verificações únicas, como as que fazemos para popularização. Quando usados regularmente, os analisadores estáticos ajudam a detectar erros no estágio inicial de desenvolvimento e, portanto, tornam mais baratos a correção.
Cálculos de exemplo .
O jogo ainda está sendo intensamente desenvolvido, com uma comunidade modder ativa trabalhando nele. A propósito, ele foi portado para várias plataformas, incluindo iOS e Android. Então, se você estiver interessado, tente!