Cataclysm Dark Days Ahead: Analisis Statis dan Permainan Roguelike

Gambar 5

Anda pasti sudah menebak dari judul bahwa artikel hari ini akan berfokus pada bug dalam kode sumber perangkat lunak. Tapi tidak hanya itu. Jika Anda tidak hanya tertarik pada C ++ dan membaca tentang bug dalam kode pengembang lain, tetapi juga menggali video game yang tidak biasa dan bertanya-tanya apa itu "roguelikes" dan bagaimana Anda memainkannya, maka selamat datang untuk membaca!

Saat mencari gim yang tidak biasa, saya menemukan Cataclysm Dark Days Ahead , yang menonjol di antara gim lain berkat grafisnya yang didasarkan pada karakter ASCII dengan berbagai warna yang tersusun pada latar belakang hitam.

Satu hal yang mengherankan Anda tentang hal ini dan permainan serupa lainnya adalah seberapa banyak fungsionalitas yang dibangun di dalamnya. Khususnya di Cataclysm , misalnya, Anda bahkan tidak dapat membuat karakter tanpa merasakan dorongan untuk google beberapa panduan karena lusinan parameter, sifat, dan skenario awal yang tersedia, belum lagi berbagai variasi peristiwa yang terjadi sepanjang permainan.

Karena ini adalah permainan dengan kode sumber terbuka, dan yang ditulis dalam C ++, kami tidak bisa berjalan tanpa memeriksanya dengan penganalisa kode statis kami PVS-Studio, yang dalam perkembangannya saya berpartisipasi aktif. Kode proyek ini ternyata berkualitas tinggi, tetapi masih memiliki beberapa cacat kecil, beberapa di antaranya akan saya bahas dalam artikel ini.

Cukup banyak game yang sudah diperiksa dengan PVS-Studio. Anda dapat menemukan beberapa contoh di artikel kami " Analisis Statis dalam Pengembangan Video Game: 10 Bug Perangkat Lunak Teratas ".

Logika


Contoh 1:

Contoh ini menunjukkan kesalahan salin-tempel klasik.

V501 Ada sub-ekspresi identik ke kiri dan ke kanan '||' 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 ) ) { .... } .... } 

Kondisi yang sama diperiksa dua kali. Pemrogram menyalin ekspresi tetapi lupa untuk memodifikasi salinan. Saya tidak yakin apakah ini bug yang kritis, tetapi faktanya cek itu tidak berfungsi sebagaimana mestinya.

Kesalahan serupa lainnya:

  • V501 Ada sub-ekspresi identik 'one_in (100000 / to_turns <int> (dur))' di sebelah kiri dan di sebelah kanan operator '&&'. player_hardcoded_effects.cpp 547

Gambar 11

Contoh 2:

V728 Pemeriksaan berlebihan dapat disederhanakan. The '(A && B) || Ekspresi (! A &&! B) 'sama dengan ekspresi' 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 .... } .... } 

Kondisi ini secara logis benar, tetapi terlalu rumit. Siapa pun yang menulis kode ini seharusnya mengasihani sesama programmer yang akan mempertahankannya. Itu dapat ditulis ulang dalam bentuk yang lebih sederhana: if (left_fav == right_fav) .

Kesalahan serupa lainnya:

  • V728 Pemeriksaan berlebihan dapat disederhanakan. The '(A &&! B) || Ekspresi (! A && B) 'sama dengan ekspresi' bool (A)! = Bool (B) '. iuse_actor.cpp 2653

Digresi i


Saya terkejut menemukan bahwa game yang menggunakan nama "roguelikes" saat ini hanyalah perwakilan yang lebih moderat dari genre lama game roguelike. Semuanya dimulai dengan game cult Rogue of 1980, yang menginspirasi banyak siswa dan programmer untuk membuat game mereka sendiri dengan elemen serupa. Saya kira banyak pengaruh juga datang dari komunitas game meja DnD dan variasinya.

Gambar 8

Optimalisasi mikro


Contoh 3:

Peringatan pada grup ini menunjukkan titik-titik yang berpotensi dioptimalkan daripada bug.

V801 Menurunkan Kinerja. Lebih baik mendefinisikan kembali argumen fungsi kedua sebagai referensi. Pertimbangkan untuk mengganti 'const ... ketik' dengan 'const ... & ketik'. 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; } 

Dalam kode ini, itype_id sebenarnya adalah std :: string yang disamarkan. Karena argumen dilewatkan sebagai konstanta, yang artinya tidak dapat diubah, hanya dengan memberikan referensi ke variabel akan membantu meningkatkan kinerja dan menghemat sumber daya komputasi dengan menghindari operasi penyalinan. Dan meskipun string itu tidak mungkin panjang, menyalinnya setiap kali tanpa alasan yang baik adalah ide yang buruk - terlebih lagi karena fungsi ini dipanggil oleh berbagai penelepon, yang, pada gilirannya, juga mendapatkan tipe dari luar dan memiliki untuk menyalinnya.

Masalah serupa:

  • V801 Menurunkan Kinerja. Lebih baik mendefinisikan kembali argumen fungsi ketiga sebagai referensi. Pertimbangkan untuk mengganti 'const ... evt_filter' dengan 'const ... & evt_filter'. input.cpp 691
  • V801 Menurunkan Kinerja. Lebih baik mendefinisikan kembali argumen fungsi kelima sebagai referensi. Pertimbangkan untuk mengganti 'const ... color' dengan 'const ... & color'. output.h 207
  • Penganalisa mengeluarkan total 32 peringatan dari jenis ini.

Contoh 4:

V813 Menurunkan Kinerja. Argumen 'str' mungkin harus diterjemahkan sebagai referensi konstan. 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; } 

Meskipun argumennya tidak konstan, ia tidak berubah dalam fungsi tubuh dengan cara apa pun. Oleh karena itu, demi optimasi, solusi yang lebih baik adalah memberikannya dengan referensi konstan daripada memaksa kompiler untuk membuat salinan lokal.

Peringatan ini tidak datang sendiri juga; jumlah total peringatan jenis ini adalah 26.

Gambar 7

Masalah serupa:

  • V813 Menurunkan Kinerja. Argumen 'pesan' mungkin harus diterjemahkan sebagai referensi konstan. json.cpp 1452
  • V813 Menurunkan Kinerja. Argumen 's' mungkin harus diterjemahkan sebagai referensi konstan. catacharset.cpp 218
  • Dan seterusnya ...

Digresi ii


Beberapa game roguelike klasik masih dalam pengembangan aktif. Jika Anda memeriksa gudang GitHub dari Cataclysm DDA atau NetHack , Anda akan melihat bahwa perubahan dikirimkan setiap hari. NetHack sebenarnya adalah game tertua yang masih dikembangkan: dirilis pada Juli 1987, dan versi terakhir kembali ke 2018.

Dwarf Fortress adalah salah satu permainan genre yang paling populer - meskipun lebih muda. Pengembangan dimulai pada tahun 2002 dan versi pertama dirilis pada tahun 2006. Moto nya "Kehilangan menyenangkan" mencerminkan fakta bahwa tidak mungkin untuk menang dalam permainan ini. Pada tahun 2007, Dwarf Fortress dianugerahi "Game Roguelike Terbaik Tahun Ini" dengan pemungutan suara yang diadakan setiap tahun di situs ASCII GAMES.

Gambar 6

Ngomong-ngomong, penggemar mungkin senang mengetahui bahwa Dwarf Fortress datang ke Steam dengan grafis 32-bit yang ditingkatkan ditambah oleh dua modder berpengalaman. Versi premium juga akan mendapatkan trek musik tambahan dan dukungan Steam Workshop. Pemilik salinan berbayar akan dapat beralih ke grafik ASCII lama jika mereka mau. Lebih banyak

Mengganti operator penugasan


Contoh 5, 6:

Berikut beberapa peringatan menarik.

V690 Kelas 'JsonObject' mengimplementasikan copy constructor, tetapi tidak memiliki operator '='. Berbahaya menggunakan kelas semacam itu. 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() { .... } .... } 

Kelas ini memiliki konstruktor salinan dan destruktor tetapi tidak menimpa operator penugasan. Masalahnya adalah bahwa operator penugasan yang dibuat secara otomatis dapat menetapkan penunjuk hanya untuk JsonIn . Akibatnya, kedua objek kelas JsonObject akan menunjuk ke JsonIn yang sama. Saya tidak bisa mengatakan dengan pasti apakah situasi seperti itu dapat terjadi dalam versi saat ini, tetapi seseorang pasti akan jatuh ke dalam perangkap ini suatu hari nanti.

Kelas selanjutnya memiliki masalah yang sama.

V690 Kelas 'JsonArray' mengimplementasikan copy constructor, tetapi tidak memiliki operator '='. Berbahaya menggunakan kelas semacam itu. 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() { .... } } 

Bahaya tidak mengesampingkan operator penugasan di kelas yang kompleks dijelaskan secara rinci dalam artikel " Hukum Dua Besar ".

Contoh 7, 8:

Keduanya juga menangani penugasan operator penugasan, tetapi kali ini implementasi spesifik dari itu.

V794 Operator penugasan harus dilindungi dari kasus '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; } 

Implementasi ini tidak memiliki perlindungan terhadap penugasan potensial, yang merupakan praktik yang tidak aman. Artinya, melewatkan * referensi ini ke operator ini dapat menyebabkan kebocoran memori.

Berikut adalah contoh yang mirip dari operator penugasan yang ditimpa secara tidak benar dengan efek samping yang aneh:

V794 Operator penugasan harus dilindungi dari kasus '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; } 

Kode ini tidak memiliki pengecekan terhadap penugasan sendiri, dan selain itu, memiliki vektor yang harus diisi. Dengan implementasi operator penugasan ini, menetapkan objek untuk dirinya sendiri akan menghasilkan penggandaan vektor di bidang target , dengan beberapa elemen menjadi rusak. Namun, transformasi diawali dengan jelas , yang akan menghapus vektor objek, sehingga menyebabkan hilangnya data.

Gambar 3

Digresi iii


Pada 2008, roguelikes bahkan mendapat definisi formal yang dikenal dengan judul epik "Berlin Interpretation". Menurutnya, semua game tersebut berbagi elemen berikut:

  • Dunia yang dihasilkan secara acak, yang meningkatkan replayability;
  • Permadeath: jika karakter Anda mati, mereka mati untuk selamanya, dan semua barang mereka hilang;
  • Gameplay berbasis giliran: setiap perubahan hanya terjadi bersamaan dengan tindakan pemain; aliran waktu ditangguhkan sampai pemain melakukan suatu tindakan;
  • Kelangsungan hidup: sumber daya sangat sedikit.

Akhirnya, fitur paling penting dari roguelikes adalah fokus utamanya pada penjelajahan dunia, menemukan kegunaan baru untuk barang-barang, dan merangkak di bawah tanah.

Ini adalah situasi umum dalam Cataclysm DDA agar karakter Anda berakhir beku sampai ke tulang, kelaparan, haus, dan, untuk melengkapi semuanya, kedua kaki mereka diganti dengan enam tentakel.

Gambar 15

Detail itu penting


Contoh 9:

V1028 Kemungkinan meluap. Pertimbangkan casting operan dari operator 'mulai + lebih besar' ke tipe 'size_t', bukan hasilnya. 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 ) ) { .... } .... } .... } 

Sepertinya programmer ingin mengambil tindakan pencegahan terhadap overflow. Namun, mempromosikan jenis jumlah tidak akan membuat perbedaan karena limpahan akan terjadi sebelum itu, pada langkah menambahkan nilai, dan promosi akan dilakukan di atas nilai yang tidak berarti. Untuk menghindari ini, hanya satu dari argumen yang harus dilemparkan ke tipe yang lebih luas: (static_cast <size_t> (start) + lebih besar) .

Contoh 10:

V530 Nilai balik fungsi 'ukuran' diperlukan untuk digunakan. 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; } 

Ada satu trik untuk kasus seperti ini. Jika Anda berakhir dengan variabel yang tidak digunakan dan Anda ingin menekan peringatan kompiler, cukup tulis (kosong) world_name alih-alih memanggil metode pada variabel itu.

Contoh 11:

V812 Menurunkan Performa. Penggunaan fungsi 'hitungan' yang tidak efektif. Itu mungkin dapat diganti dengan panggilan ke fungsi 'temukan'. 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; } ) { .... } .... } 

Fakta bahwa hitungan dibandingkan dengan nol menunjukkan bahwa programmer ingin mengetahui apakah aktivitas mengandung setidaknya satu elemen yang diperlukan. Tetapi hitungan harus berjalan melalui seluruh wadah karena menghitung semua kejadian elemen. Pekerjaan dapat dilakukan lebih cepat dengan menggunakan find , yang berhenti setelah kejadian pertama ditemukan.

Contoh 12:

Bug ini mudah ditemukan jika Anda mengetahui satu detail rumit tentang tipe char .

V739 EOF tidak boleh dibandingkan dengan nilai tipe 'char'. 'Ch' harus dari tipe 'int'. json.cpp 762

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

Gambar 13

Ini adalah salah satu kesalahan yang tidak akan Anda temukan dengan mudah kecuali Anda tahu bahwa EOF didefinisikan sebagai -1. Oleh karena itu, ketika membandingkannya dengan variabel tipe char yang ditandatangani , kondisi tersebut bernilai false di hampir setiap kasus. Satu-satunya pengecualian adalah dengan karakter yang kodenya 0xFF (255). Ketika digunakan dalam perbandingan, itu akan berubah menjadi -1, sehingga membuat kondisi ini benar.

Contoh 13:

Bug kecil ini mungkin menjadi kritis suatu hari nanti. Bagaimanapun, ada alasan bagus bahwa itu ditemukan dalam daftar CWE sebagai CWE-834 . Perhatikan bahwa proyek telah memicu peringatan ini lima kali.

V663 Infinite loop dimungkinkan. Kondisi 'cin.eof ()' tidak cukup untuk memutuskan dari loop. Pertimbangkan untuk menambahkan pemanggilan fungsi 'cin.fail ()' ke ekspresi kondisional. action.cpp 46

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

Seperti peringatan itu katakan, itu tidak cukup untuk memeriksa EOF saat membaca dari file - Anda juga harus memeriksa kegagalan input dengan memanggil cin.fail () . Mari kita perbaiki kode untuk membuatnya lebih aman:

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

Tujuan keymap_txt.clear () adalah untuk menghapus status kesalahan (tanda) pada aliran setelah kesalahan baca terjadi sehingga Anda bisa membaca sisa teks. Memanggil keymap_txt.ignore dengan parameter numeric_limits <streamsize> :: max () dan karakter baris baru memungkinkan Anda untuk melewatkan bagian string yang tersisa.

Ada cara yang lebih sederhana untuk menghentikan pembacaan:

 while( !keymap_txt ) { .... } 

Ketika dimasukkan ke dalam konteks logika, stream akan mengubah dirinya menjadi nilai yang setara dengan true hingga EOF tercapai.

Digresi iv


Game-game terkait roguelike paling populer saat ini menggabungkan unsur-unsur roguelike asli dan genre lain seperti platformer, strategi, dan sebagainya. Game seperti itu dikenal sebagai "roguelike-like" atau "roguelite". Di antara ini adalah judul terkenal seperti Don't Starve , The Binding of Isaac , FTL: Faster Than Light , Darkest Dungeon , dan bahkan Diablo .

Namun, perbedaan antara roguelike dan roguelite kadang-kadang bisa sangat kecil sehingga Anda tidak bisa memastikan kategori mana yang termasuk dalam game. Beberapa berpendapat bahwa Dwarf Fortress bukan roguelike dalam arti yang ketat, sementara yang lain percaya Diablo adalah gim klasik roguelike.

Gambar 1

Kesimpulan


Meskipun proyek ini terbukti berkualitas tinggi secara umum, dengan hanya beberapa cacat serius, itu tidak berarti dapat dilakukan tanpa analisis statis. Kekuatan analisis statis digunakan secara teratur daripada pemeriksaan satu kali seperti yang kami lakukan untuk mempopulerkan. Ketika digunakan secara teratur, analisa statis membantu Anda mendeteksi bug pada tahap pengembangan paling awal dan, karenanya, membuatnya lebih murah untuk diperbaiki. Contoh perhitungan .

Gambar 2

Gim ini masih terus dikembangkan, dengan komunitas modder aktif sedang mengerjakannya. Omong-omong, itu telah porting ke beberapa platform, termasuk iOS dan Android. Jadi, jika Anda tertarik, cobalah!

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


All Articles