Kode game Command & Conquer: bug dari tahun 90-an. Jilid dua

gambar1.png


Perusahaan Amerika Electronic Arts Inc (EA) telah merilis kode sumber terbuka dari game Command & Conquer: Tiberian Dawn dan Command & Conquer: Red Alert. Beberapa lusin bug ditemukan dalam kode sumber menggunakan penganalisis PVS-Studio, jadi mohon sambut kelanjutan deskripsi cacat yang ditemukan.



pengantar



Command & Conquer adalah rangkaian game komputer bergenre strategi real-time. Game pertama dalam seri ini dirilis pada tahun 1995. Kode sumber game diterbitkan dengan rilis koleksi Command & Conquer Remastered .



Untuk menemukan kesalahan dalam kode, penganalisis PVS-Studio digunakan . Ini adalah alat untuk mengidentifikasi kesalahan dan potensi kerentanan dalam kode sumber program yang ditulis dalam C, C ++, C # dan Java.



Tautan ke ringkasan bug pertama: " Game Command & Conquer: bug dari tahun 90-an. Volume satu ".



Kesalahan dalam kondisi



V583 Operator '?:', Terlepas dari ekspresi kondisionalnya, selalu mengembalikan satu dan nilai yang sama: 3072. STARTUP.CPP 1136



void Read_Setup_Options( RawFileClass *config_file )
{
  ....
  ScreenHeight = ini.Get_Bool("Options", "Resolution", false) ? 3072 : 3072;
  ....
}


Ternyata pengguna tidak dapat mempengaruhi beberapa pengaturan. Lebih tepatnya, mereka melakukan sesuatu, tetapi karena fakta bahwa operator terner selalu mengembalikan satu nilai, sebenarnya tidak ada yang berubah.



V590 Pertimbangkan untuk memeriksa ekspresi 'i <8 && i <4'. Ekspresi tersebut berlebihan atau salah cetak. DLLInterface.cpp 2238



// Maximum number of multi players possible.
#define MAX_PLAYERS 8 // max # of players we can have

for (int i = 0; i < MAX_PLAYERS && i < 4; i++) {
  if (GlyphxPlayerIDs[i] == player_id) {
    MultiplayerStartPositions[i] = XY_Cell(x, y);
  }
}


Karena siklus yang salah, posisi tidak ditetapkan untuk semua pemain. Di satu sisi, kami melihat MAX_PLAYERS 8 konstan dan menganggap bahwa ini adalah jumlah maksimum pemain. Di sisi lain, kita melihat kondisi i <4 dan operator && . Jadi, loop tidak pernah membuat 8 iterasi. Kemungkinan besar, pada tahap awal pengembangan, programmer tidak menggunakan konstanta, dan ketika dia memulai, dia lupa menghapus nomor lama dari kode.



V648 Prioritas operasi '&&' lebih tinggi daripada operasi '||' operasi. INFANTRY.CPP 1003



void InfantryClass::Assign_Target(TARGET target)
{
  ....
  if (building && building->Class->IsCaptureable &&
    (GameToPlay != GAME_NORMAL || *building != STRUCT_EYE && Scenario < 13)) {
    Assign_Destination(target);
  }
  ....
}


Anda bisa membuat kode tidak terlihat (dan kemungkinan besar salah) hanya dengan tidak menentukan prioritas operasi untuk operator || . dan && . Tidak jelas sama sekali di sini apakah ini salah atau tidak. Tetapi mengingat kualitas keseluruhan dari kode proyek ini, mari kita asumsikan bahwa di sini dan di beberapa tempat lain ada kesalahan dengan prioritas operasi:



  • V648 Prioritas operasi '&&' lebih tinggi daripada operasi '||' operasi. TEAM.CPP 456
  • V648 Prioritas operasi '&&' lebih tinggi daripada operasi '||' operasi. TAMPILAN CPP 1160
  • V648 Prioritas operasi '&&' lebih tinggi daripada operasi '||' operasi. TAMPILAN CPP 1571
  • V648 Prioritas operasi '&&' lebih tinggi daripada operasi '||' operasi. RUMAH CPP 2594
  • V648 Prioritas operasi '&&' lebih tinggi daripada operasi '||' operasi. INIT.CPP 2541


V617 Pertimbangkan untuk memeriksa kondisi. Argumen '((1L << STRUCT_CHRONOSPHERE))' dari '|' operasi bitwise berisi nilai bukan nol. RUMAH.CPP 5089



typedef enum StructType : char {
  STRUCT_NONE=-1,
  STRUCT_ADVANCED_TECH,
  STRUCT_IRON_CURTAIN,
  STRUCT_WEAP,
  STRUCT_CHRONOSPHERE, // 3
  ....
}

#define  STRUCTF_CHRONOSPHERE (1L << STRUCT_CHRONOSPHERE)

UrgencyType HouseClass::Check_Build_Power(void) const
{
  ....
  if (State == STATE_THREATENED || State == STATE_ATTACKED) {
    if (BScan | (STRUCTF_CHRONOSPHERE)) {  // <=
      urgency = URGENCY_HIGH;
    }
  }
  ....
}


Untuk memeriksa apakah bit tertentu dalam variabel disetel, gunakan operator &, bukan |. Karena kesalahan ketik pada bagian kode ini, kondisinya selalu benar.



V768 Konstanta pencacahan 'WWKEY_RLS_BIT' digunakan sebagai variabel tipe Boolean. KEYBOARD.CPP 286



typedef enum {
  WWKEY_SHIFT_BIT = 0x100,
  WWKEY_CTRL_BIT  = 0x200,
  WWKEY_ALT_BIT   = 0x400,
  WWKEY_RLS_BIT   = 0x800,
  WWKEY_VK_BIT    = 0x1000,
  WWKEY_DBL_BIT   = 0x2000,
  WWKEY_BTN_BIT   = 0x8000,
} WWKey_Type;

int WWKeyboardClass::To_ASCII(int key)
{
  if ( key && WWKEY_RLS_BIT)
    return(KN_NONE);
  return(key);
}


Saya pikir, dalam parameter kunci , mereka ingin memeriksa bit tertentu yang ditentukan oleh topeng WWKEY_RLS_BIT , tetapi membuat kesalahan ketik. Anda seharusnya menggunakan operator bitwise & daripada && untuk memeriksa kode kunci.



Pemformatan yang mencurigakan



V523 Pernyataan 'then' setara dengan pernyataan 'else'. RADAR.CPP 1827



void RadarClass::Player_Names(bool on)
{
  IsPlayerNames = on;
  IsToRedraw = true;
  if (on) {
    Flag_To_Redraw(true);
//    Flag_To_Redraw(false);
  } else {
    Flag_To_Redraw(true);   // force drawing of the plate
  }
}


Suatu ketika, seorang pengembang mengomentari kode untuk debugging. Sejak itu, kode tersebut tetap menjadi operator bersyarat dengan operator yang sama di cabang yang berbeda.



Ada dua tempat serupa lainnya:



  • V523 Pernyataan 'then' setara dengan pernyataan 'else'. SEL.CPP 1792
  • V523 Pernyataan 'then' setara dengan pernyataan 'else'. RADAR.CPP 2274


V705 Ada kemungkinan bahwa blok 'lain' telah dilupakan atau dikomentari, sehingga mengubah logika operasi program. NETDLG.CPP 1506



static int Net_Join_Dialog(void)
{
  ....
  /*...............................................................
  F4/SEND/'M' = edit a message
  ...............................................................*/
  if (Messages.Get_Edit_Buf()==NULL) {
    ....
  } else

  /*...............................................................
  If we're already editing a message and the user clicks on
  'Send', translate our input to a Return so Messages.Input() will
  work properly.
  ...............................................................*/
  if (input==(BUTTON_SEND | KN_BUTTON)) {
    input = KN_RETURN;
  }
  ....
}


Karena komentar besar, pengembang tidak melihat operator bersyarat yang tidak ditentukan di atas. Sisa kata kunci else membentuk else if dibangun dengan kondisi di bawah , yang kemungkinan besar merupakan perubahan ke logika asli.



V519 Variabel 'ScoresPresent' diberi nilai dua kali berturut-turut. Mungkin ini salah. Periksa baris: 539, 541. INIT.CPP 541



bool Init_Game(int , char *[])
{
  ....
  ScoresPresent = false;
//if (CCFileClass("SCORES.MIX").Is_Available()) {
    ScoresPresent = true;
    if (!ScoreMix) {
      ScoreMix = new MixFileClass("SCORES.MIX");
      ThemeClass::Scan();
    }
//}


Cacat potensial lainnya karena refactoring yang belum selesai. Sekarang tidak jelas apakah variabel ScoresPresent harus benar atau masih salah .



Kesalahan alokasi memori



V611 Memori dialokasikan menggunakan operator 'T baru []' tetapi dilepaskan menggunakan operator 'hapus'. Pertimbangkan untuk memeriksa kode ini. Mungkin lebih baik menggunakan 'delete [] poke_data;'. CCDDE.CPP 410



BOOL Send_Data_To_DDE_Server (char *data, int length, int packet_type)
{
  ....
  char *poke_data = new char [length + 2*sizeof(int)]; // <=
  ....
  if(DDE_Class->Poke_Server( .... ) == FALSE) {
    CCDebugString("C&C95 - POKE failed!\n");
    DDE_Class->Close_Poke_Connection();
    delete poke_data;                                  // <=
    return (FALSE);
  }

  DDE_Class->Close_Poke_Connection();

  delete poke_data;                                    // <=

  return (TRUE);
}


Penganalisis telah mendeteksi kesalahan terkait fakta bahwa memori dapat dialokasikan dan dilepaskan dengan cara yang tidak kompatibel. Untuk mengosongkan memori yang dialokasikan untuk array, Anda harus menggunakan operator delete [] , bukan delete .



Ada beberapa tempat seperti itu, dan semuanya secara bertahap merusak aplikasi yang sedang berjalan (game):



  • V611 Memori dialokasikan menggunakan operator 'T baru []' tetapi dilepaskan menggunakan operator 'hapus'. Pertimbangkan untuk memeriksa kode ini. Mungkin lebih baik menggunakan 'delete [] poke_data;'. CCDDE.CPP 416
  • V611 The memory was allocated using 'new T[]' operator but was released using the 'delete' operator. Consider inspecting this code. It's probably better to use 'delete [] temp_buffer;'. INIT.CPP 1302
  • V611 The memory was allocated using 'new T[]' operator but was released using the 'delete' operator. Consider inspecting this code. It's probably better to use 'delete [] progresspalette;'. MAPSEL.CPP 795
  • V611 The memory was allocated using 'new T[]' operator but was released using the 'delete' operator. Consider inspecting this code. It's probably better to use 'delete [] grey2palette;'. MAPSEL.CPP 796
  • V611 Memori dialokasikan menggunakan operator 'T baru []' tetapi dilepaskan menggunakan operator 'hapus'. Pertimbangkan untuk memeriksa kode ini. Mungkin lebih baik menggunakan 'delete [] poke_data;'. CCDDE.CPP 422
  • V611 Memori dialokasikan menggunakan operator 'T baru []' tetapi dilepaskan menggunakan operator 'hapus'. Pertimbangkan untuk memeriksa kode ini. Mungkin lebih baik menggunakan 'delete [] temp_buffer;'. INIT.CPP 1139


V772 Memanggil operator 'hapus' untuk pointer kosong akan menyebabkan perilaku tidak terdefinisi. ENDING.CPP 254



void GDI_Ending(void)
{
  ....
  void * localpal = Load_Alloc_Data(CCFileClass("SATSEL.PAL"));
  ....
  delete [] localpal;
  ....
}


Operator delete dan delete [] dipisahkan karena suatu alasan. Mereka melakukan pekerjaan berbeda untuk membersihkan memori. Dan saat menggunakan pointer yang tidak diketik, kompilator tidak tahu tipe data apa yang ditunjuk pointer. Dalam standar bahasa C ++, perilaku kompilator tidak ditentukan.



Sejumlah peringatan penganalisis juga ditemukan seperti ini:



  • V772 Memanggil operator 'hapus' untuk pointer kosong akan menyebabkan perilaku tidak terdefinisi. HEAP.CPP 284
  • V772 Memanggil operator 'hapus' untuk pointer kosong akan menyebabkan perilaku tidak terdefinisi. INIT.CPP 728
  • V772 Memanggil operator 'hapus' untuk pointer kosong akan menyebabkan perilaku tidak terdefinisi. MIXFILE.CPP 134
  • V772 Memanggil operator 'hapus' untuk pointer kosong akan menyebabkan perilaku tidak terdefinisi. MIXFILE.CPP 391
  • V772 Calling a 'delete' operator for a void pointer will cause undefined behavior. MSGBOX.CPP 423
  • V772 Calling a 'delete' operator for a void pointer will cause undefined behavior. SOUNDDLG.CPP 407
  • V772 Calling a 'delete' operator for a void pointer will cause undefined behavior. BUFFER.CPP 126
  • V772 Calling a 'delete' operator for a void pointer will cause undefined behavior. BUFF.CPP 162
  • V772 Calling a 'delete' operator for a void pointer will cause undefined behavior. BUFF.CPP 212
  • V772 Calling a 'delete' operator for a void pointer will cause undefined behavior. BFIOFILE.CPP 330
  • V772 Calling a 'delete' operator for a void pointer will cause undefined behavior. EVENT.CPP 934
  • V772 Calling a 'delete' operator for a void pointer will cause undefined behavior. HEAP.CPP 318
  • V772 Calling a 'delete' operator for a void pointer will cause undefined behavior. INIT.CPP 3851
  • V772 Calling a 'delete' operator for a void pointer will cause undefined behavior. MIXFILE.CPP 130
  • V772 Calling a 'delete' operator for a void pointer will cause undefined behavior. MIXFILE.CPP 430
  • V772 Calling a 'delete' operator for a void pointer will cause undefined behavior. MIXFILE.CPP 447
  • V772 Calling a 'delete' operator for a void pointer will cause undefined behavior. MIXFILE.CPP 481
  • V772 Calling a 'delete' operator for a void pointer will cause undefined behavior. MSGBOX.CPP 461
  • V772 Calling a 'delete' operator for a void pointer will cause undefined behavior. QUEUE.CPP 2982
  • V772 Calling a 'delete' operator for a void pointer will cause undefined behavior. QUEUE.CPP 3167
  • V772 Memanggil operator 'hapus' untuk pointer kosong akan menyebabkan perilaku tidak terdefinisi. SOUNDDLG.CPP 406


V773 Fungsi itu keluar tanpa melepaskan penunjuk 'progresspalette'. Kebocoran memori mungkin terjadi. MAPSEL.CPP 258



void Map_Selection(void)
{
  ....
  unsigned char *grey2palette    = new unsigned char[768];
  unsigned char *progresspalette = new unsigned char[768];
  ....
  scenario = Scenario + ((house == HOUSE_GOOD) ? 0 : 14);
  if (house == HOUSE_GOOD) {
    lastscenario = (Scenario == 14);
    if (Scenario == 15) return;
  } else {
    lastscenario = (Scenario == 12);
    if (Scenario == 13) return;
  }
  ....
}


"Jika kamu tidak membebaskan memori sama sekali, maka aku pasti tidak akan salah dalam memilih operator!" - mungkin pikir programmer itu.



gambar2.png


Tapi kemudian terjadi kebocoran memori, yang juga merupakan kesalahan. Di suatu tempat di akhir fungsi, memori dibebaskan, tetapi sebelum itu ada banyak tempat di mana keluar bersyarat dari fungsi terjadi, dan memori oleh pointer grey2palette dan progresspalett tidak dibebaskan.



aneka



V570 Variabel 'hdr-> MagicNumber' ditetapkan ke variabel itu sendiri. COMBUF.CPP 806



struct CommHdr {
  unsigned short MagicNumber;
  unsigned char Code;
  unsigned long PacketID;
} *hdr;

void CommBufferClass::Mono_Debug_Print(int refresh)
{
  ....
  hdr = (CommHdr *)SendQueue[i].Buffer;
  hdr->MagicNumber = hdr->MagicNumber;
  hdr->Code = hdr->Code;
  ....
}


Dua bidang struktur CommHdr diinisialisasi dengan nilainya sendiri. Menurut pendapat saya, operasi tidak ada gunanya, tetapi dilakukan berkali-kali:



  • V570 Variabel 'hdr-> Code' ditugaskan untuk dirinya sendiri. COMBUF.CPP 807
  • V570 Variabel 'hdr-> MagicNumber' ditetapkan ke variabel itu sendiri. COMBUF.CPP 931
  • V570 Variabel 'hdr-> Code' ditugaskan untuk dirinya sendiri. COMBUF.CPP 932
  • V570 Variabel 'hdr-> MagicNumber' ditetapkan ke variabel itu sendiri. COMBUF.CPP 987
  • V570 Variabel 'hdr-> Code' ditugaskan untuk dirinya sendiri. COMBUF.CPP 988
  • V570 Variabel 'obj' ditetapkan ke dirinya sendiri. PETA.CPP 1132
  • V570 Variabel 'hdr-> MagicNumber' ditetapkan ke variabel itu sendiri. COMBUF.CPP 910
  • V570 Variabel 'hdr-> Code' ditugaskan untuk dirinya sendiri. COMBUF.CPP 911
  • V570 Variabel 'hdr-> MagicNumber' ditetapkan ke variabel itu sendiri. COMBUF.CPP 1040
  • V570 Variabel 'hdr-> Code' ditugaskan untuk dirinya sendiri. COMBUF.CPP 1041
  • V570 Variabel 'hdr-> MagicNumber' ditetapkan ke variabel itu sendiri. COMBUF.CPP 1104
  • V570 Variabel 'hdr-> Code' ditugaskan untuk dirinya sendiri. COMBUF.CPP 1105
  • V570 Variabel 'obj' ditetapkan ke dirinya sendiri. PETA. CPP 1279


V591 Fungsi non-void harus mengembalikan nilai. HEAP.H 123



int FixedHeapClass::Free(void * pointer);

template<class T>
class TFixedHeapClass : public FixedHeapClass
{
  ....
  virtual int Free(T * pointer) {FixedHeapClass::Free(pointer);};
};


Fungsi Gratis kelas TFixedHeapClass ada operator pernyataan kembali . Hal yang paling menarik adalah fungsi yang disebut FixedHeapClass :: Free memiliki nilai kembalian bertipe int . Kemungkinan besar, programmer hanya lupa menulis pernyataan return dan sekarang fungsi tersebut mengembalikan nilai yang tidak dapat dipahami.



V672 Mungkin tidak perlu membuat variabel 'kerusakan' baru di sini. Salah satu argumen fungsi memiliki nama yang sama dan argumen ini adalah referensi. Periksa baris: 1219, 1278. BUILDING.CPP 1278



ResultType BuildingClass::Take_Damage(int & damage, ....)
{
  ....
  if (tech && tech->IsActive && ....) {
    int damage = 500;
    tech->Take_Damage(damage, 0, WARHEAD_AP, source, forced);
  }
  ....
}


Parameter kerusakan diteruskan dengan referensi. Oleh karena itu, perubahan nilai variabel ini diharapkan terjadi di badan fungsi. Namun di satu tempat, developer mendeklarasikan variabel dengan nama yang sama. Karena itu, nilai 500 akan disimpan dalam kerusakan variabel lokal, bukan parameter fungsi. Mungkin perilaku yang berbeda dimaksudkan.



Tempat lain seperti itu:



  • V672 Mungkin tidak perlu membuat variabel 'kerusakan' baru di sini. Salah satu argumen fungsi memiliki nama yang sama dan argumen ini adalah referensi. Periksa jalur: 4031, 4068. TECHNO.CPP 4068


V762 Ada kemungkinan fungsi virtual diganti secara tidak benar. Lihat argumen pertama fungsi 'Occupy_List' dalam kelas turunan 'BulletClass' dan kelas dasar 'ObjectClass'. BULLET.H 90



class ObjectClass : public AbstractClass
{
  ....
  virtual short const * Occupy_List(bool placement=false) const; // <=
  virtual short const * Overlap_List(void) const;
  ....
};

class BulletClass : public ObjectClass,
                    public FlyClass,
                    public FuseClass
{
  ....
  virtual short const * Occupy_List(void) const;                 // <=
  virtual short const * Overlap_List(void) const {return Occupy_List();};
  ....
};


Penganalisis telah mendeteksi potensi kesalahan saat menimpa fungsi virtual Occupy_List . Ini dapat menyebabkan fungsi yang salah dipanggil saat runtime.



Beberapa tempat yang lebih mencurigakan:



  • V762 Ada kemungkinan fungsi virtual diganti secara tidak benar. Lihat kualifikasi fungsi 'Ok_To_Move' di kelas turunan 'TurretClass' dan kelas dasar 'DriveClass'. TURRET.H 76
  • V762 Ada kemungkinan fungsi virtual diganti secara tidak benar. Lihat argumen keempat dari fungsi 'Help_Text' di kelas turunan 'HelpClass' dan kelas dasar 'DisplayClass'. HELP.H 55
  • V762 Ada kemungkinan fungsi virtual diganti secara tidak benar. Lihat argumen pertama dari fungsi 'Draw_It' di kelas turunan 'MapEditClass' dan kelas dasar 'HelpClass'. MAPEDIT.H 187
  • V762 It is possible a virtual function was overridden incorrectly. See first argument of function 'Occupy_List' in derived class 'AnimClass' and base class 'ObjectClass'. ANIM.H 80
  • V762 It is possible a virtual function was overridden incorrectly. See first argument of function 'Overlap_List' in derived class 'BulletClass' and base class 'ObjectClass'. BULLET.H 102
  • V762 It is possible a virtual function was overridden incorrectly. See qualifiers of function 'Remap_Table' in derived class 'BuildingClass' and base class 'TechnoClass'. BUILDING.H 281
  • V762 It is possible a virtual function was overridden incorrectly. See fourth argument of function 'Help_Text' in derived class 'HelpClass' and base class 'DisplayClass'. HELP.H 58
  • V762 Ada kemungkinan fungsi virtual diganti secara tidak benar. Lihat argumen pertama dari fungsi 'Overlap_List' di kelas turunan 'AnimClass' dan kelas dasar 'ObjectClass'. HEWAN.H 90


V763 Parameter 'coord' selalu ditulis ulang dalam badan fungsi sebelum digunakan. TAMPILAN CPP 4031



void DisplayClass::Set_Tactical_Position(COORDINATE coord)
{
  int xx = 0;
  int yy = 0;

  Confine_Rect(&xx, &yy, TacLeptonWidth, TacLeptonHeight,
    Cell_To_Lepton(MapCellWidth) + GlyphXClientSidebarWidthInLeptons,
    Cell_To_Lepton(MapCellHeight));

  coord = XY_Coord(xx + Cell_To_Lepton(MapCellX), yy + Cell_To_Lepton(....));

  if (ScenarioInit) {
    TacticalCoord = coord;
  }
  DesiredTacticalCoord = coord;
  IsToRedraw = true;
  Flag_To_Redraw(false);
}


Parameter coord segera diganti di badan fungsi. Nilai lama tidak digunakan. Sangat mencurigakan jika suatu fungsi memiliki argumen dan tidak bergantung padanya. Dan kemudian ada beberapa koordinat.



Dan tempat ini layak untuk dicoba:



  • V763 Parameter 'coord' selalu ditulis ulang dalam badan fungsi sebelum digunakan. TAMPILAN CPP 4251


V507 Pointer ke array lokal 'localpalette' disimpan di luar lingkup array ini. Penunjuk seperti itu akan menjadi tidak valid. MAPSEL.CPP 757



extern "C" unsigned char *InterpolationPalette;

void Map_Selection(void)
{
  unsigned char localpalette[768];
  ....
  InterpolationPalette = localpalette;
  ....
}


Ada banyak variabel global dalam kode permainan. Ini mungkin pendekatan umum untuk pengkodean pada masa itu. Tapi sekarang dianggap buruk dan bahkan berbahaya.



Localpalette array lokal disimpan di penunjuk InterpolationPalette, yang akan menjadi tidak valid setelah keluar dari fungsi.



Beberapa tempat yang lebih berbahaya:



  • V507 Pointer ke array lokal 'localpalette' disimpan di luar lingkup array ini. Penunjuk seperti itu akan menjadi tidak valid. MAPSEL.CPP 769
  • V507 Pointer ke 'buffer' larik lokal disimpan di luar lingkup larik ini. Penunjuk seperti itu akan menjadi tidak valid. WINDOWS.CPP 458


Kesimpulan



Seperti yang saya tulis di laporan pertama, semoga proyek-proyek baru Electronic Arts memiliki kualitas yang lebih baik. Secara umum, pengembang game secara aktif membeli PVS-Studio. Sekarang budget game sudah cukup besar, jadi tidak ada yang membutuhkan biaya tambahan untuk memperbaiki bug dalam produksi. Dan memperbaiki kesalahan pada tahap awal pengkodean praktis tidak membutuhkan waktu dan sumber daya lainnya.



Kami mengundang Anda untuk mengunduh dan mencoba PVS-Studio di semua proyek di situs web kami .





Jika Anda ingin berbagi artikel ini dengan pembaca berbahasa Inggris, silakan gunakan tautan terjemahan: Svyatoslav Razmyslov. The Code of the Command & Conquer Game: Bug dari tahun 90-an. Jilid dua .



All Articles