Bot pour Starcraft dans Rust, C et toute autre langue

StarCraft: Brood War . Combien cela signifie pour moi. Et pour beaucoup d'entre vous. Tellement que je doutais de donner un lien vers le wiki.


Une fois, Halt m'a frappé dans un e - mail personnel et m'a proposé d'apprendre Rust . Comme toute personne normale, nous avons décidé de commencer par bonjour le monde écrire une bibliothÚque dynamique pour Windows qui pourrait se charger dans l'espace d'adressage d'un jeu StarCraft et gérer les unités.


L'article dĂ©crira le processus de recherche de solutions, en utilisant des technologies, des techniques qui vous permettront d'apprendre de nouvelles choses dans le langage Rust et son Ă©cosystĂšme ou d'ĂȘtre inspirĂ© pour implĂ©menter un bot dans votre langage prĂ©fĂ©rĂ©, que ce soit C, C ++, ruby, python, etc.


Cet article mĂ©rite certainement d'ĂȘtre lu sous l'hymne de la CorĂ©e du Sud:


Starcraft OST

Bwapi


Ce jeu a dĂ©jĂ  20 ans. Et il est toujours populaire , les championnats rassemblent des salles entiĂšres de personnes aux États-Unis, mĂȘme en 2017 , oĂč la bataille des grands maĂźtres Jaedong vs Bisu a eu lieu. En plus des joueurs en direct, les voitures sans Ăąme participent Ă©galement aux batailles! Et cela est possible grĂące Ă  BWAPI . Liens plus utiles.


Depuis plus d'une dĂ©cennie, il existe une communautĂ© de dĂ©veloppeurs de bots autour de ce jeu. Les amateurs Ă©crivent des bots et participent Ă  divers championnats. Beaucoup d'entre eux Ă©tudient l'IA et l'apprentissage automatique. BWAPI est utilisĂ© par les universitĂ©s pour Ă©duquer leurs Ă©tudiants. Il existe mĂȘme une chaĂźne Twitch qui diffuse des jeux.


Il y a quelques années, une équipe de fans a inversé les internes de Starcraft et a écrit une API C ++ qui vous permet d'écrire des bots, de vous intégrer au processus de jeu et de dominer les petites personnes pathétiques.


Comme cela arrive souvent avant pour construire une maison, il faut se procurer du minerai, forger des outils ... écrire un bot, vous devez implémenter l'API. Qu'est-ce que Rust peut offrir?


Ffi


L'interaction avec d'autres langues de Rust est assez simple. Il y a FFI pour cela. Permettez-moi de vous fournir un bref extrait de la documentation .


Supposons que nous ayons une bibliothĂšque snappy qui a un fichier d'en - tĂȘte snappy-ch Ă  partir duquel nous copierons les dĂ©clarations de fonction.


Créez un projet en utilisant le fret .


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

Cargo a créé une structure de fichiers standard pour le projet.


Dans Cargo.toml spécifiez la dépendance à libc :


 [dependencies] libc = "0.2" 

src/main.rs fichier src/main.rs ressemblera Ă  ceci:


 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); } 

Nous collectons et gérons:


 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 

Vous ne pouvez appeler que cargo run , qui avant le lancement appelle cargo build . Ou créez un projet et appelez directement le binaire:


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

Le code se compile Ă  condition que la bibliothĂšque snappy soit installĂ©e (pour Ubuntu, le paquet libsnappy-dev doit ĂȘtre installĂ©).


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

Comme vous pouvez le voir, notre binaire est lié à la bibliothÚque partagée libsnappy. Et l'appel snappy_max_compressed_length est un appel de fonction de cette bibliothÚque.


rouille-bindgen


Ce serait bien si nous pouvions générer automatiquement des FFI. Heureusement, il existe un tel utilitaire dans l'arsenal de rastomanov appelé rust-bindgen . Elle est capable de générer des liaisons FFI vers des bibliothÚques C (et certaines C ++).


Installation:


 $ cargo install bindgen 

À quoi ressemble l'utilisation de rust-bindgen ? Nous prenons les fichiers d'en-tĂȘte C / C ++, dĂ©finissons l'utilitaire bindgen sur eux et nous obtenons le code Rust gĂ©nĂ©rĂ© avec les dĂ©finitions des structures et des fonctions de la structure. Voici Ă  quoi ressemble la gĂ©nĂ©ration FFI pour Snappy:


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

Il s'est avĂ©rĂ© que bindgen passe devant les en-tĂȘtes BWAPI, gĂ©nĂ©rant des tonnes de feuilles de code non utilisables (en raison des fonctions membres virtuelles, std :: string dans l'API publique, etc.). Le fait est que BWAPI est Ă©crit en C ++. C ++ est gĂ©nĂ©ralement difficile Ă  utiliser mĂȘme Ă  partir de projets C ++. Une fois qu'une bibliothĂšque compilĂ©e est prĂ©fĂ©rable de lier avec le mĂȘme Ă©diteur de liens (versions identiques), les fichiers d'en-tĂȘte sont mieux analysĂ©s avec le mĂȘme compilateur (versions identiques). Parce que de nombreux facteurs peuvent affecter le rĂ©sultat. Par exemple, le mangling , que GNU GCC ne peut toujours pas implĂ©menter sans erreurs . Ces facteurs sont si importants qu'ils ne pouvaient pas ĂȘtre surmontĂ©s mĂȘme dans gtest , et la documentation indiquait qu'il serait prĂ©fĂ©rable pour vous de construire gtest dans le cadre du projet avec le mĂȘme compilateur et le mĂȘme Ă©diteur de liens.


Bwapi-c


C est une programmation en lingua franca. Si rust-bindgen fonctionne bien pour le langage C, pourquoi ne pas implémenter le BWAPI pour C puis utiliser son API? Bonne idée!


Oui, c'est une bonne idée jusqu'à ce que vous examiniez les tripes de BWAPI et que vous voyiez le nombre de classes et de méthodes que vous devez implémenter = (en particulier toutes ces dispositions de structures en mémoire, assembleurs, correctifs de mémoire et autres horreurs pour lesquelles nous n'avons pas le temps. Il est nécessaire d'utiliser au maximum la solution existante.


Mais nous devons en quelque sorte gérer le mangling, le code C ++, l'héritage et les fonctions membres virtuelles.


En C ++, il existe deux outils puissants que nous utiliserons pour résoudre notre problÚme, ce sont les pointeurs opaques et les extern "C" .


extern "C" {} permet au code C ++ de se déguiser en C. Cela vous permet de générer des noms de fonction purs sans altération.


Les pointeurs opaques nous permettent d'effacer un type et de crĂ©er un pointeur vers "un certain type" dont nous ne fournissons pas l'implĂ©mentation. Comme il ne s'agit que d'une dĂ©claration d'un certain type, et non de son implĂ©mentation, il est impossible d'utiliser ce type par valeur, il ne peut ĂȘtre utilisĂ© que par pointeur.


Disons que nous avons un tel code C ++:


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

Nous pouvons le transformer en un en-tĂȘte C comme ceci:


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

Et la partie C ++, qui sera le lien entre l'en-tĂȘte C et l'implĂ©mentation C ++:


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

Toutes les mĂ©thodes de classe ne devaient pas ĂȘtre gĂ©rĂ©es de cette façon. Il existe des classes dans BWAPI, opĂ©rations sur lesquelles vous pouvez vous implĂ©menter en utilisant les valeurs de champ de ces structures, par exemple typedef struct Position { int x; int y; } Position; typedef struct Position { int x; int y; } Position; et des mĂ©thodes comme Position::get_distance .


Il y avait ceux qui devaient ĂȘtre jugĂ©s d'une maniĂšre spĂ©ciale. Par exemple, un AIModule doit ĂȘtre un pointeur vers une classe C ++ avec un ensemble spĂ©cifique de fonctions membres virtuelles. Cependant, voici le titre et l' implĂ©mentation .


Ainsi, aprÚs plusieurs mois de dur labeur, 554 méthodes et une douzaine de classes, la bibliothÚque multiplateforme BWAPI-C est née, qui vous permet d'écrire des bots en C. Le sous-produit était une compilation croisée et la possibilité d'implémenter l'API dans n'importe quel autre langage prenant en charge FFI et la convention d'appel cdecl.


Si vous écrivez une bibliothÚque, veuillez écrire l'API C.


La caractéristique la plus importante de BWAPI-C est la plus large capacité d'intégration avec d'autres langues. Python , Ruby , Rust , PHP , Java et bien d'autres savent travailler avec C, donc vous pouvez également écrire un bot dessus, si vous travaillez un peu avec un fichier et implémentez vos wrappers.


Écrire un bot en C


Cette partie décrit les principes généraux de la conception des modules Starcraft.


Il existe 2 types de bots: module et client. Nous allons voir un exemple d'écriture d'un module.


Un module est une bibliothĂšque tĂ©lĂ©chargeable, le principe gĂ©nĂ©ral du chargement peut ĂȘtre vu ici . Le module devrait exporter 2 fonctions: newAIModule et gameInit .


Avec gameInit tout est simple, cette fonction est appelée pour passer un pointeur sur le jeu en cours. Ce pointeur est trÚs important, car dans la nature de BWAPI vit une variable statique globale qui est utilisée dans certaines parties du code. gameInit :


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

newAIModule peu plus compliqué. Il doit renvoyer un pointeur vers une classe C ++ qui a une table de méthode virtuelle avec les noms onXXXXX qui sont appelés sur certains événements de jeu. Définissez la structure du module:


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

Le premier champ doit ĂȘtre un pointeur vers un tableau de mĂ©thodes (magie, tout). Ainsi, la fonction newAIModule :


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

createAIModuleWrapper est une autre magie qui transforme un pointeur C en pointeur vers une classe C ++ avec virtual les méthodes fonctions des membres.


module_vtable est une variable statique sur la table des méthodes, les valeurs des méthodes sont remplies de pointeurs vers des fonctions globales:


 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) {} 

Par le nom des fonctions et leurs signatures, il est clair dans quelles conditions et avec quels arguments elles sont appelées. Par exemple, j'ai rendu toutes les fonctions vides sauf


 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); } 

Cette fonction est appelée au démarrage du jeu. Un pointeur vers le module actuel est passé en argument. BWAPIC_getGame renvoie un pointeur global sur le jeu que nous avons défini en appelant BWAPIC_setGame . Nous allons donc montrer un exemple de compilation croisée et de fonctionnement de module:


 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 ... ... ... 

Nous poussons des boutons et commençons le jeu. Vous pouvez en savoir plus sur le lancement sur le site Web de BWAPI et dans BWAPI-C .


Le résultat du module:


image


Un exemple un peu plus complexe d'un module qui montre le travail avec les itérateurs, le contrÎle d'unité, la recherche de minéraux et les statistiques se trouve dans bwapi-c / example / Dll.c.


bwapi-sys


Dans l'écosystÚme Rasta, il est habituel d'appeler les packages d' une certaine maniÚre qui sont liés aux bibliothÚques natives. Tout paquet foo-sys a deux fonctions importantes:


  • Lien vers la bibliothĂšque libfoo native
  • Fournit des dĂ©clarations de fonction Ă  partir de la bibliothĂšque libfoo. Mais seules les dĂ©clarations, les abstractions de haut niveau dans les caisses * -sys ne sont pas fournies.

Pour que le package * -sys puisse réussir la liaison, ils y intÚgrent une recherche de bibliothÚque native et / ou un assemblage de bibliothÚque à partir des sources.


Pour que le package * -sys fournisse des déclarations, il faut soit les écrire à la main, soit les générer à l'aide de bindgen. Encore une fois bindgen. Tentative numéro deux =)


La génération de liants avec bwapi-c devient obscÚne:


 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 

OĂč BWAPI.h est le fichier avec les inclusions de tous les en-tĂȘtes d'en-tĂȘte de BWAPI-C.


Par exemple, pour des fonctions déjà connues, bindgen a généré les déclarations suivantes:


 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; } 

Il existe 2 stratégies: stocker le code généré dans le référentiel et générer du code à la volée lors de l'assemblage. Ces deux approches ont leurs avantages et leurs inconvénients .


Bienvenue bwapi-sys , un autre petit pas vers notre objectif.


Rappelez-vous, j'ai parlĂ© de multiplateforme? Nlinker a rejoint le projet et a mis en Ɠuvre une stratĂ©gie dĂ©licate. Si la cible est Windows, tĂ©lĂ©chargez le BWAPIC dĂ©jĂ  assemblĂ© depuis le github. Et pour les cibles restantes, nous collectons BWAPI-C Ă  partir des sources d'OpenBW (je vous le dirai un peu plus tard).


bwapi-rs


Maintenant que nous avons les liants, nous pouvons décrire des abstractions de haut niveau. Nous avons 2 types avec lesquels travailler: des valeurs pures et des pointeurs opaques.


Avec des valeurs pures, tout est plus simple. Prenons l'exemple de la couleur. Nous devons faciliter l'utilisation du code Rust afin de pouvoir utiliser les couleurs de maniĂšre pratique et naturelle:


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

Donc, pour une utilisation pratique, il sera nécessaire de définir un idiomatique pour l'énumération du langage Rust avec des constantes de C ++ et de définir des méthodes de conversion dans bwapi_sys :: Color en utilisant le trait std :: convert :: From :


 // 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, ... 

Bien que pour plus de commodité, vous pouvez utiliser la caisse enum-primitive-derive .


Avec des pointeurs opaques, ce n'est pas plus difficile. Pour ce faire, utilisez le modĂšle Newtype :


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

Autrement dit, Player est une certaine structure avec un champ privé - un pointeur opaque brut de C. Et voici comment vous pouvez décrire la méthode Player :: color:


 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 } } 

Nous pouvons maintenant écrire notre premier bot en rouille!


Écrire un bot en rouille


Comme preuve de concept, le bot ressemblera à un pays célÚbre: toutes ses fonctionnalités seront d'embaucher des travailleurs et de collecter des minéraux.


Corée du Nord


Corée du sud


Commençons par les newAIModule gameInit et newAIModule :


 #[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] remplit la mĂȘme fonction que l' extern "C" en C ++. Dans wrap_handler , toutes sortes de magie se produisent avec la substitution de la table de fonction virtuelle et se dĂ©guisent en classe C ++.


La description de la structure du module est encore plus simple et plus belle qu'en C:


 struct ExampleAIModule { name: String, } 

Ajoutez quelques méthodes pour rendre les statistiques et donner des ordres:


 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); } _ => {} }; } } } 

Pour que le type ExampleAIModule devienne un véritable module, vous devez lui apprendre à répondre aux événements onXXXX , pour lesquels vous devez implémenter le type EventHandler, qui est un analogue de la table virtuelle AIModule_vtable de 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) {} } 

Construire et démarrer le module est aussi simple que pour 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 ... ... ... 

Et la vidéo de travail:



Un peu sur la compilation croisée


Bref, Rust est magnifique! En deux clics, vous pouvez mettre en place de nombreuses chaßnes d'outils pour différentes plateformes. Plus précisément, la chaßne d'outils i686-pc-windows-gnu est définie par la commande:


 rustup target add i686-pc-windows-gnu 

Vous pouvez également spécifier le kofig pour la cargaison à la racine du projet .cargo/config :


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

Et c'est tout ce que vous devez faire pour compiler votre projet Rust Ă  partir de Linux sur Windows.


Openbw


Ces gars sont allés encore plus loin. Ils ont décidé d'écrire une version open-source du jeu SC: BW! Et ils s'en sortent plutÎt bien. L'un de leurs objectifs était d'implémenter des images HD, mais SC: Remastered les a devancés = (Pour le moment, vous pouvez utiliser leur API pour écrire des bots (oui, également en C ++). Mais la fonctionnalité la plus étonnante est la possibilité d'afficher les rediffusions directement dans le navigateur .


Conclusion


Un problĂšme non rĂ©solu est restĂ© pendant la mise en Ɠuvre: nous ne contrĂŽlons pas l'unicitĂ© des liens, et l'existence simultanĂ©e de &mut et & lorsque vous changez un objet entraĂźnera un comportement indĂ©fini. Trouble. Halt a essayĂ© de mettre en Ɠuvre des liaisons idiomatiques, mais son fusible s'est lĂ©gĂšrement estompĂ©. De plus, pour rĂ©soudre ce problĂšme, vous devrez pelleter qualitativement l'API C ++ et dĂ©finir correctement les qualificateurs const .


J'ai vraiment aimĂ© travailler sur ce projet, j'ai regardĂ© les rediffusions et plongĂ© profondĂ©ment dans l'atmosphĂšre. Ce jeu a laissĂ© un hĂ©ritage Ă  ëŻżì–Ž ëŻżì–Ž 않을 정도 읞. Aucun jeu n'est populaire auprĂšs de SC: BW, et son impact sur ëŒ€í•œëŻŒê”­ 정ìč˜ ì—êȌ Ă©tait impensable. Les pro-gamers en CorĂ©e 아마도 sont aussi populaires que dram ìŁŒì—° 배우 ë“€ les drames corĂ©ens diffusĂ©s aux heures de grande Ă©coute. 또한, 한ꔭ 에서 프로 êČŒìŽëšž 띌멎 ꔰ대 의 íŠč별한 ìœĄê”° 에 입대 할 수 있닀.


Vive StarCraft!


Les références


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


All Articles