Bot untuk Starcraft di Rust, C dan bahasa lainnya

StarCraft: Brood War . Betapa itu berarti bagi saya. Dan bagi banyak dari Anda. Begitu banyak sehingga saya ragu apakah akan memberikan tautan ke wiki.


Suatu kali, Halt mengetuk saya dalam email pribadi dan menawarkan untuk belajar Rust . Seperti orang normal lainnya, kami memutuskan untuk memulai halo dunia menulis pustaka dinamis untuk Windows yang dapat memuat ke ruang alamat permainan StarCraft dan mengelola unit.


Artikel ini akan menjelaskan proses menemukan solusi, menggunakan teknologi, teknik yang memungkinkan Anda mempelajari hal-hal baru dalam bahasa Rust dan ekosistemnya atau terinspirasi untuk mengimplementasikan bot dalam bahasa favorit Anda, apakah itu C, C ++, ruby, python, dll.


Artikel ini tentu layak dibaca di bawah lagu Korea Selatan:


OST Starcraft

Bwapi


Game ini sudah berusia 20 tahun. Dan masih populer , kejuaraan mengumpulkan seluruh aula orang di Amerika Serikat bahkan pada 2017 , di mana pertempuran grandmaster Jaedong vs Bisu terjadi. Selain pemain live, mobil tanpa jiwa juga ambil bagian dalam pertempuran! Dan ini dimungkinkan berkat BWAPI . Tautan yang lebih bermanfaat.


Selama lebih dari satu dekade, ada komunitas pengembang bot di sekitar game ini. Penggemar menulis bot dan berpartisipasi dalam berbagai kejuaraan. Banyak dari mereka mempelajari AI dan pembelajaran mesin. BWAPI digunakan oleh universitas untuk mendidik siswa mereka. Bahkan ada saluran kedutan yang menyiarkan game.


Jadi, tim penggemar beberapa tahun yang lalu membalikkan internal Starcraft dan menulis C ++ API yang memungkinkan Anda untuk menulis bot, mengintegrasikan ke dalam proses permainan dan mendominasi orang-orang kecil yang menyedihkan.


Seperti yang sering terjadi sebelumnya untuk membangun rumah, Anda perlu mendapatkan bijih, menempa alat ... menulis bot, Anda perlu menerapkan API. Apa yang bisa ditawarkan Rust?


Ffi


Berinteraksi dengan bahasa lain dari Rust cukup sederhana. Ada FFI untuk ini. Biarkan saya memberikan kutipan singkat dari dokumentasi .


Misalkan kita memiliki perpustakaan tajam yang memiliki file header tajam-ch dari mana kita akan menyalin deklarasi fungsi.


Buat proyek menggunakan kargo .


$ cargo new --bin snappy Created binary (application) `snappy` project $ cd snappy snappy$ tree . β”œβ”€β”€ Cargo.toml └── src └── main.rs 1 directory, 2 files 

Cargo menciptakan struktur file standar untuk proyek tersebut.


Di Cargo.toml tentukan ketergantungan pada libc :


 [dependencies] libc = "0.2" 

src/main.rs akan terlihat seperti ini:


 extern crate libc; //   C ,     size_t use libc::size_t; #[link(name = "snappy")] //       extern { //    ,    //  C  : // size_t snappy_max_compressed_length(size_t source_length); fn snappy_max_compressed_length(source_length: size_t) -> size_t; } fn main() { let x = unsafe { snappy_max_compressed_length(100) }; println!("max compressed length of a 100 byte buffer: {}", x); } 

Kami mengumpulkan dan menjalankan:


 snappy$ cargo build ... snappy$ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.02s Running `target/debug/snappy` max compressed length of a 100 byte buffer: 148 

Anda hanya dapat memanggil cargo run , yang sebelum peluncuran memanggil cargo build . Atau bangun proyek dan panggil biner secara langsung:


 snappy$ ./target/debug/snappy max compressed length of a 100 byte buffer: 148 

Kode mengkompilasi dengan syarat bahwa perpustakaan snappy diinstal (untuk Ubuntu, paket libsnappy-dev harus diinstal).


 snappy$ ldd target/debug/snappy ... libsnappy.so.1 => /usr/lib/x86_64-linux-gnu/libsnappy.so.1 (0x00007f8de07cf000) 

Seperti yang Anda lihat, biner kami ditautkan ke pustaka bersama libsnappy. Dan panggilan snappy_max_compressed_length adalah panggilan fungsi dari perpustakaan ini.


karat-bindgen


Alangkah baiknya jika kita dapat secara otomatis menghasilkan FFI. Untungnya, ada utilitas seperti itu di gudang rastomanov yang disebut rust-bindgen . Dia mampu menghasilkan binding FFI ke pustaka C (dan beberapa C ++).


Instalasi:


 $ cargo install bindgen 

Seperti apa tampilan karat-bindgen ? Kami mengambil file header C / C ++, mengatur utilitas bindgen pada mereka, dan kami mendapatkan kode Rust yang dihasilkan dengan definisi struktur struktur dan fungsi. Seperti apa bentuk generasi FFI untuk tajam:


 $ bindgen /usr/include/snappy-ch | grep -C 1 snappy_max_compressed_length extern "C" { pub fn snappy_max_compressed_length(source_length: usize) -> usize; } 

Ternyata bindgen melewati di depan header BWAPI, menghasilkan ton lembar kode yang tidak dapat digunakan (karena fungsi anggota virtual, std :: string di API publik, dll.). Masalahnya adalah bahwa BWAPI ditulis dalam C ++. C ++ umumnya sulit digunakan bahkan dari proyek C ++. Setelah pustaka yang dikompilasi lebih baik untuk ditautkan dengan tautan yang sama (versi identik), file header lebih baik untuk diurai dengan kompiler yang sama (versi identik). Karena ada banyak faktor yang dapat mempengaruhi hasil. Sebagai contoh, mangling , yang GNU GCC masih tidak dapat menerapkan tanpa kesalahan . Faktor-faktor ini sangat signifikan sehingga tidak dapat diatasi bahkan di gtest , dan dokumentasi menunjukkan bahwa akan lebih baik bagi Anda untuk membangun gtest sebagai bagian dari proyek dengan kompiler yang sama dan penghubung yang sama.


Bwapi-c


C adalah lingua franca programming. Jika rust-bindgen bekerja dengan baik untuk bahasa C, mengapa tidak mengimplementasikan BWAPI untuk C dan kemudian menggunakan API-nya? Ide bagus!


Ya, itu ide yang bagus sampai Anda melihat isi BWAPI dan melihat jumlah kelas dan metode yang perlu Anda implementasikan = (Terutama semua tata letak struktur ini dalam memori, assembler, patch memori, dan kengerian lainnya yang kami tidak punya waktu. Perlu untuk menggunakan solusi yang ada secara maksimal.


Tetapi kita harus berurusan dengan fungsi mangling, kode C ++, inheritance, dan anggota virtual.


Dalam C ++, ada dua alat kuat yang akan kita gunakan untuk menyelesaikan masalah kita, ini adalah pointer buram dan extern "C" .


extern "C" {} memungkinkan kode C ++ untuk menyamarkan dirinya sebagai C. Ini memungkinkan Anda untuk menghasilkan nama fungsi murni tanpa mangling.


Pointer buram memberi kita kemampuan untuk menghapus suatu tipe dan membuat pointer ke "beberapa tipe" yang implementasinya tidak kami sediakan. Karena ini hanya deklarasi dari beberapa jenis, dan bukan implementasinya, tidak mungkin untuk menggunakan jenis ini berdasarkan nilai, ini hanya dapat digunakan oleh pointer.


Katakanlah kita memiliki kode C ++ seperti itu:


 namespace cpp { struct Foo { int bar; virtual int get_bar() { return this->bar; } }; } // namespace cpp 

Kita bisa mengubahnya menjadi header C seperti ini:


 extern "C" { typedef struct Foo_ Foo; //    Foo //  cpp::Foo::get_bar int Foo_get_bar(Foo* self); } 

Dan bagian C ++, yang akan menjadi tautan antara header C dan implementasi C ++:


 int Foo_get_bar(Foo* self) { //      cpp::Foo    ::get_bar return reinterpret_cast<cpp::Foo*>(self)->get_bar(); } 

Tidak semua metode kelas harus ditangani dengan cara ini. Ada kelas di BWAPI, operasi di mana Anda dapat mengimplementasikan diri Anda sendiri menggunakan nilai bidang struktur ini, misalnya typedef struct Position { int x; int y; } Position; typedef struct Position { int x; int y; } Position; dan metode seperti Position::get_distance .


Ada yang harus diadili dengan cara khusus. Misalnya, AIModule harus menjadi pointer ke kelas C ++ dengan serangkaian fungsi anggota virtual tertentu. Namun, inilah judul dan implementasinya .


Jadi, setelah beberapa bulan kerja keras, 554 metode dan selusin kelas, perpustakaan lintas platform BWAPI-C lahir, yang memungkinkan Anda untuk menulis bot dalam C. Produk sampingannya adalah kompilasi silang dan kemampuan untuk mengimplementasikan API dalam bahasa lain yang mendukung FFI dan konvensi pemanggilan cdecl.


Jika Anda menulis perpustakaan, silakan tulis C API.


Fitur terpenting dari BWAPI-C adalah kemampuan terluas untuk berintegrasi dengan bahasa lain. Python , Ruby , Rust , PHP , Java dan banyak lainnya tahu cara bekerja dengan C, oleh karena itu Anda juga dapat menulis bot pada mereka, jika Anda bekerja sedikit dengan sebuah file dan mengimplementasikan pembungkus Anda.


Menulis bot di C


Bagian ini menjelaskan prinsip-prinsip umum desain modul Starcraft.


Ada 2 jenis bot: modul dan klien. Kita akan melihat contoh penulisan modul.


Modul adalah pustaka yang dapat diunduh, prinsip umum pemuatan dapat dilihat di sini . Modul harus mengekspor 2 fungsi: newAIModule dan gameInit .


Dengan gameInit semuanya sederhana, fungsi ini dipanggil untuk meneruskan pointer ke game saat ini. Pointer ini sangat penting, karena di belantara BWAPI terdapat variabel statis global yang digunakan di beberapa bagian kode. Mari kita gambarkan gameInit :


 DLLEXPORT void gameInit(void* game) { BWAPIC_setGame(game); } 

newAIModule sedikit lebih rumit. Seharusnya mengembalikan pointer ke kelas C ++ yang memiliki tabel metode virtual dengan nama onXXXXX yang dipanggil pada acara game tertentu. Tentukan struktur modul:


 typedef struct ExampleAIModule { const AIModule_vtable* vtable_; const char* name; } ExampleAIModule; 

Bidang pertama harus menjadi penunjuk ke tabel metode (sihir, semua hal). Jadi, fungsi newAIModule :


 DLLEXPORT void* newAIModule() { ExampleAIModule* const module = (ExampleAIModule*) malloc( sizeof(ExampleAIModule) ); module->name = "ExampleAIModule"; module->vtable_ = &module_vtable; return createAIModuleWrapper( (AIModule*) module ); } 

createAIModuleWrapper adalah keajaiban lain yang mengubah pointer C menjadi pointer ke kelas C ++ dengan virtual metode fungsi anggota.


module_vtable adalah variabel statis pada tabel metode, nilai-nilai metode diisi dengan pointer ke fungsi global:


 static AIModule_vtable module_vtable = { onStart, onEnd, onFrame, onSendText, onReceiveText, onPlayerLeft, onNukeDetect, onUnitDiscover, onUnitEvade, onUnitShow, onUnitHide, onUnitCreate, onUnitDestroy, onUnitMorph, onUnitRenegade, onSaveGame, onUnitComplete }; void onEnd(AIModule* module, bool isWinner) { } void onFrame(AIModule* module) {} void onSendText(AIModule* module, const char* text) {} void onReceiveText(AIModule* module, Player* player, const char* text) {} void onPlayerLeft(AIModule* module, Player* player) {} void onNukeDetect(AIModule* module, Position target) {} void onUnitDiscover(AIModule* module, Unit* unit) {} void onUnitEvade(AIModule* module, Unit* unit) {} void onUnitShow(AIModule* module, Unit* unit) {} void onUnitHide(AIModule* module, Unit* unit) {} void onUnitCreate(AIModule* module, Unit* unit) {} void onUnitDestroy(AIModule* module, Unit* unit) {} void onUnitMorph(AIModule* module, Unit* unit) {} void onUnitRenegade(AIModule* module, Unit* unit) {} void onSaveGame(AIModule* module, const char* gameName) {} void onUnitComplete(AIModule* module, Unit* unit) {} 

Dengan nama fungsi dan tanda tangannya, jelas di bawah kondisi apa dan dengan argumen apa mereka dipanggil. Sebagai contoh, saya membuat semua fungsi kosong kecuali


 void onStart(AIModule* module) { ExampleAIModule* self = (ExampleAIModule*) module; Game* game = BWAPIC_getGame(); Game_sendText(game, "Hello from bwapi-c!"); Game_sendText(game, "My name is %s", self->name); } 

Fungsi ini dipanggil saat game dimulai. Penunjuk ke modul saat ini dilewatkan sebagai argumen. BWAPIC_getGame mengembalikan pointer global ke game yang kami atur dengan memanggil BWAPIC_setGame . Jadi, kami akan menunjukkan contoh operasi lintas-kompilasi dan modul:


 bwapi-c/example$ tree . β”œβ”€β”€ BWAPIC.dll └── Dll.c 0 directories, 2 files bwapi-c/example$ i686-w64-mingw32-gcc -mabi=ms -shared -o Dll.dll Dll.c -I../include -L. -lBWAPIC bwapi-c/example$ cp Dll.dll ~/Starcraft/bwapi-data/ bwapi-c/example$ cd ~/Starcraft/bwapi-data/ Starcraft$ wine bwheadless.exe -e StarCraft.exe -l bwapi-data/BWAPI.dll --headful ... ... ... 

Kami menekan tombol dan memulai permainan. Anda dapat membaca lebih lanjut tentang peluncuran di situs web BWAPI dan di BWAPI-C .


Hasil modul:


gambar


Contoh modul yang sedikit lebih rumit yang menunjukkan bekerja dengan iterator, kontrol unit, pencarian mineral, dan statistik dapat ditemukan di bwapi-c / example / Dll.c.


bwapi-sys


Dalam ekosistem Rasta, sudah biasa memanggil paket dengan cara tertentu yang terhubung ke perpustakaan asli. Paket foo-sys memiliki dua fungsi penting:


  • Tautan ke pustaka libfoo asli
  • Menyediakan deklarasi fungsi dari libfoo library. Tapi hanya deklarasi, abstraksi tingkat tinggi di * -sys peti tidak disediakan.

Agar paket * -sys berhasil dihubungkan, mereka mengintegrasikan pencarian perpustakaan asli dan / atau perakitan perpustakaan dari sumber ke dalamnya.


Agar paket * -sys memberikan deklarasi, seseorang harus menulisnya secara manual atau menghasilkan menggunakan bindgen. Lagi bindgen. Percobaan nomor dua =)


Membuat binder dengan bwapi-c menjadi cabul:


 bindgen BWAPI.h -o lib.rs \ --opaque-type ".+_" \ --blacklist-type "std.*|__.+|.+_$|Game_v(Send|Print|Draw).*|va_list|.+_t$" \ --no-layout-tests \ --no-derive-debug \ --raw-line "#![allow(improper_ctypes, non_snake_case)]" \ -- -I../submodules/bwapi-c/include sed -i -r -- 's/.+\s+(.+)_;/pub struct \1;/' lib.rs 

Di mana BWAPI.h adalah file dengan penyertaan semua header dari BWAPI-C.


Misalnya, untuk fungsi yang sudah diketahui, bindgen menghasilkan deklarasi berikut:


 extern "C" { /// BWAPIC_setGame must be called from gameInit to initialize BWAPI::BroodwarPtr pub fn BWAPIC_setGame(game: *mut Game); } extern "C" { pub fn BWAPIC_getGame() -> *mut Game; } 

Ada 2 strategi: menyimpan kode yang dihasilkan dalam repositori dan membuat kode dengan cepat selama perakitan. Kedua pendekatan ini memiliki kelebihan dan kekurangan .


Selamat datang bwapi-sys , langkah kecil menuju tujuan kami.


Ingat, saya berbicara tentang cross-platform? Nlinker bergabung dengan proyek dan menerapkan strategi yang rumit. Jika targetnya adalah Windows, unduh BWAPIC yang sudah dirakit dari github. Dan untuk target yang tersisa, kami mengumpulkan BWAPI-C dari sumber untuk OpenBW (saya akan memberi tahu Anda nanti).


bwapi-rs


Sekarang kita memiliki pengikat, kita dapat menggambarkan abstraksi tingkat tinggi. Kami memiliki 2 jenis untuk dikerjakan: nilai murni dan pointer buram.


Dengan nilai murni, semuanya lebih sederhana. Ambil warna sebagai contoh. Kita perlu membuatnya nyaman untuk menggunakan kode Rust sehingga kita dapat menggunakan warna dengan cara yang nyaman dan alami:


 game.draw_line(CoordinateType::Screen, (10, 20), (30, 40), Color::Red); ^^^ 

Jadi untuk penggunaan yang mudah, perlu untuk mendefinisikan idiomatik untuk enumerasi bahasa Rust dengan konstanta dari C ++ dan mendefinisikan metode konversi di bwapi_sys :: Warna menggunakan std :: convert :: From trait:


 // FFI version #[repr(C)] #[derive(Copy, Clone)] pub struct Color { pub color: ::std::os::raw::c_int, } // Idiomatic version #[derive(PartialEq, PartialOrd, Copy, Clone)] pub enum Color { Black = 0, Brown = 19, ... 

Meskipun untuk kenyamanan, Anda dapat menggunakan peti enum-primitif-turunan .


Dengan pointer buram, itu tidak sulit. Untuk melakukan ini, gunakan pola Newtype :


 pub struct Player(*mut sys::Player); 

Yaitu, Player adalah struktur tertentu dengan bidang pribadi - penunjuk buram mentah dari C. Dan inilah cara Anda dapat menggambarkan metode Pemain :: warna:


 impl Player { //    Player::getColor  bwapi-sys //extern "C" { // pub fn Player_getColor(self_: *mut Player) -> Color; //} pub fn color(&self) -> Color { // bwapi_sys::Player_getColor -    BWAPI-C // self.0 -   let color = unsafe { bwapi_sys::Player_getColor(self.0) }; color.into() //  bwapi_sys::Color -> Color } } 

Sekarang kita bisa menulis bot pertama kita di Rust!


Menulis bot di Rust


Sebagai bukti konsep, bot akan terlihat seperti satu negara terkenal: semua fungsinya adalah untuk mempekerjakan pekerja dan mengumpulkan mineral.


Korea utara


Korea selatan


Mari kita mulai dengan fungsi gameInit dan gameInit diperlukan:


 #[no_mangle] pub unsafe extern "C" fn gameInit(game: *mut void) { bwapi_sys::BWAPIC_setGame(game as *mut bwapi_sys::Game); } #[no_mangle] pub unsafe extern "C" fn newAIModule() -> *mut void { let module = ExampleAIModule { name: String::from("ExampleAIModule") }; let result = wrap_handler(Box::new(module)); result } 

#[no_mangle] melakukan fungsi yang sama dengan extern "C" di C ++. Di dalam wrap_handler , segala macam keajaiban terjadi dengan penggantian tabel fungsi virtual dan menyamar sebagai kelas C ++.


Deskripsi struktur modul bahkan lebih sederhana dan lebih indah daripada di C:


 struct ExampleAIModule { name: String, } 

Tambahkan beberapa metode untuk merender statistik dan memberikan pesanan:


 impl ExampleAIModule { fn draw_stat(&mut self) { let game = Game::get(); let message = format!("Frame {}", game.frame_count()); game.draw_text(CoordinateType::Screen, (10, 10), &message); } fn give_orders(&mut self) { let player = Game::get().self_player(); for unit in player.units() { match unit.get_type() { UnitType::Terran_SCV | UnitType::Zerg_Drone | UnitType::Protoss_Probe => { if !unit.is_idle() { continue; } if unit.is_carrying_gas() || unit.is_carrying_minerals() { unit.return_cargo(false); continue; } if let Some(mineral) = Game::get() .minerals() .min_by_key(|m| unit.distance_to(m)) { // WE REQUIRE MORE MINERALS unit.right_click(&mineral, false); } } UnitType::Terran_Command_Center => { unit.train(UnitType::Terran_SCV); } UnitType::Protoss_Nexus => { unit.train(UnitType::Protoss_Probe); } UnitType::Zerg_Hatchery | UnitType::Zerg_Lair | UnitType::Zerg_Hive => { unit.train(UnitType::Zerg_Drone); } _ => {} }; } } } 

Agar tipe ExampleAIModule berubah menjadi modul nyata, Anda perlu mengajarkannya untuk merespons peristiwa onXXXX , di mana Anda perlu mengimplementasikan tipe EventHandler, yang merupakan analog dari tabel virtual AIModule_vtable dari C:


 impl EventHandler for ExampleAIModule { fn on_start(&mut self) { Game::get().send_text(&format!("Hello from Rust! My name is {}", self.name)); } fn on_end(&mut self, _is_winner: bool) {} fn on_frame(&mut self) { self.draw_stat(); self.give_orders(); } fn on_send_text(&mut self, _text: &str) {} fn on_receive_text(&mut self, _player: &mut Player, _text: &str) {} fn on_player_left(&mut self, _player: &mut Player) {} fn on_nuke_detect(&mut self, _target: Position) {} fn on_unit_discover(&mut self, _unit: &mut Unit) {} fn on_unit_evade(&mut self, _unit: &mut Unit) {} fn on_unit_show(&mut self, _unit: &mut Unit) {} fn on_unit_hide(&mut self, _unit: &mut Unit) {} fn on_unit_create(&mut self, _unit: &mut Unit) {} fn on_unit_destroy(&mut self, _unit: &mut Unit) {} fn on_unit_morph(&mut self, _unit: &mut Unit) {} fn on_unit_renegade(&mut self, _unit: &mut Unit) {} fn on_save_game(&mut self, _game_name: &str) {} fn on_unit_complete(&mut self, _unit: &mut Unit) {} } 

Membangun dan memulai modul semudah untuk C:


 bwapi-rs$ cargo build --example dll --target=i686-pc-windows-gnu bwapi-rs$ cp ./target/i686-pc-windows-gnu/debug/examples/dll.dll ~/Starcraft/bwapi-data/Dll.dll bwapi-rs$ cd ~/Starcraft/bwapi-data/ Starcraft$ wine bwheadless.exe -e StarCraft.exe -l bwapi-data/BWAPI.dll --headful ... ... ... 

Dan video kerjanya:



Sedikit tentang kompilasi silang


Singkatnya, Karat itu indah! Dalam dua klik, Anda dapat menaruh banyak toolchain untuk platform yang berbeda. Secara khusus, toolchain i686-pc-windows-gnu diatur oleh perintah:


 rustup target add i686-pc-windows-gnu 

Anda juga dapat menentukan kofig untuk kargo di root dari proyek .cargo/config :


 [target.i686-pc-windows-gnu] linker = "i686-w64-mingw32-gcc" ar = "i686-w64-mingw32-ar" runner = "wine" 

Dan itu semua yang perlu Anda lakukan untuk mengkompilasi proyek Rust Anda dari Linux pada Windows.


Openbw


Orang-orang ini bahkan melangkah lebih jauh. Mereka memutuskan untuk menulis versi open-source dari game SC: BW! Dan mereka melakukannya dengan cukup baik. Salah satu tujuan mereka adalah mengimplementasikan gambar-gambar HD, tetapi SC: Remastered lebih dulu daripada mereka = (Saat ini, Anda dapat menggunakan API mereka untuk menulis bot (ya, juga di C ++). Tetapi fitur yang paling menakjubkan adalah kemampuan untuk melihat replay langsung di browser .


Kesimpulan


Masalah yang belum terselesaikan tetap ada selama implementasi: kami tidak mengontrol keunikan tautan, dan keberadaan &mut simultan &mut & ketika mengubah suatu objek akan menyebabkan perilaku yang tidak terdefinisi. Kesulitan. Menghentikan mencoba menerapkan ikatan idiomatik, tetapi sekeringnya sedikit memudar. Selain itu, untuk mengatasi masalah ini, Anda harus menyekop C ++ API secara kualitatif dan menetapkan kualifikasi const benar.


Saya benar-benar menikmati bekerja di proyek ini, saya 쒅일 쒅일 menyaksikan tayangan ulang dan terjun jauh ke atmosfer. Game ini meninggalkan λ―Ώμ–΄ λ―Ώμ–΄ μ•Šμ„ 정도 인 warisan. Tidak ada game yang benar-benar populer dengan SC: BW, dan dampaknya pada λŒ€ν•œλ―Όκ΅­ μ •μΉ˜ μ—κ²Œ tidak terpikirkan. Pro-gamer di Korea μ•„λ§ˆλ„ sama populernya dengan λ“œλΌλ§ˆ μ£Όμ—° 배우 λ“€ drama Korea di prime time. λ˜ν•œ, ν•œκ΅­ μ—μ„œ ν”„λ‘œ 게이머 라면 κ΅°λŒ€ 의 νŠΉλ³„ν•œ 윑ꡰ 에 μž…λŒ€ μž…λŒ€ ν•  수 μžˆλ‹€.


StarCraft live yang panjang!


Referensi


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


All Articles