Bot para Starcraft em Rust, C e qualquer outro idioma

StarCraft: Guerra da Ninhada . Quanto isso significa para mim. E para muitos de vocês. Tantos que eu duvidava em dar um link para o wiki.


Uma vez, Halt me bateu em um e- mail pessoal e se ofereceu para aprender Rust . Como qualquer pessoa normal, decidimos começar com olá mundo escrevendo uma biblioteca dinâmica para Windows que possa ser carregada no espaço de endereço de um jogo StarCraft e gerenciar unidades.


O artigo descreverá o processo de encontrar soluções, usando tecnologias, técnicas que permitirão que você aprenda coisas novas na linguagem Rust e em seu ecossistema ou seja inspirado a implementar um bot em sua linguagem favorita, seja C, C ++, ruby, python, etc.


Vale a pena ler este artigo sob o hino da Coréia do Sul:


Starcraft OST

Bwapi


Este jogo já tem 20 anos. E ainda é popular , os campeonatos reúnem salas inteiras de pessoas nos EUA, mesmo em 2017 , onde ocorreu a batalha dos mestres Jaedong vs Bisu. Além de jogadores ao vivo, carros sem alma também participam de batalhas! E isso é possível graças ao BWAPI . Links mais úteis.


Por mais de uma década, houve uma comunidade de desenvolvedores de bot em torno deste jogo. Entusiastas escrevem bots e participam de vários campeonatos. Muitos deles estudam IA e aprendizado de máquina. O BWAPI é usado pelas universidades para educar seus alunos. Existe até um canal de contração que transmite jogos.


Assim, há alguns anos, uma equipe de fãs reverteu os componentes internos do Starcraft e criou uma API C ++ que permite escrever bots, integrar-se ao processo do jogo e dominar pessoas patéticas.


Como costuma acontecer antes para construir uma casa, você precisa obter minério, forjar ferramentas ... Para escrever um bot, você precisa implementar a API. O que a Rust pode oferecer?


Ffi


Interagir com outros idiomas do Rust é bastante simples. Existe FFI para isso. Deixe-me fornecer um breve trecho da documentação .


Suponha que tenhamos uma biblioteca snappy que possui um arquivo de cabeçalho snappy-ch a partir do qual copiaremos as declarações de função.


Crie um projeto usando carga .


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

Cargo criou uma estrutura de arquivos padrão para o projeto.


Em Cargo.toml especifique a dependência da libc :


 [dependencies] libc = "0.2" 

src/main.rs arquivo src/main.rs terá a seguinte aparência:


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

Coletamos e executamos:


 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 

Você só pode chamar a cargo run , que antes do lançamento chama a cargo build . Ou crie um projeto e chame o binário diretamente:


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

O código é compilado desde que a biblioteca snappy esteja instalada (para Ubuntu, o pacote libsnappy-dev deve estar instalado).


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

Como você pode ver, nosso binário está vinculado à biblioteca compartilhada libsnappy. E a chamada snappy_max_compressed_length é uma chamada de função desta biblioteca.


ferrugem-bindgen


Seria bom se pudéssemos gerar automaticamente o FFI. Felizmente, existe uma utilidade no arsenal de rastomanov chamada ferrugem-bindgen . Ela é capaz de gerar ligações FFI para bibliotecas C (e algumas C ++).


Instalação:


 $ cargo install bindgen 

Como é o uso do rust-bindgen ? Pegamos os arquivos de cabeçalho C / C ++, configuramos o utilitário bindgen neles e obtemos o código Rust gerado com as definições das estruturas e funções da estrutura. Aqui está a aparência da geração FFI para o 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; } 

Acontece que o bindgen passa na frente dos cabeçalhos BWAPI, gerando toneladas de folhas de código não utilizáveis ​​(devido a funções de membro virtuais, std :: string na API pública, etc.). A questão é que o BWAPI é escrito em C ++. C ++ é geralmente difícil de usar, mesmo em projetos C ++. Quando uma biblioteca compilada é melhor vincular ao mesmo vinculador (versões idênticas), os arquivos de cabeçalho são melhores para analisar com o mesmo compilador (versões idênticas). Porque existem muitos fatores que podem afetar o resultado. Por exemplo, manipulação , que o GNU GCC ainda não pode implementar sem erros . Esses fatores são tão significativos que não puderam ser superados nem no gtest , e a documentação indicou que seria melhor criar o gtest como parte do projeto com o mesmo compilador e o mesmo vinculador.


Bwapi-c


C é a programação da lingua franca. Se rust-bindgen funciona bem para a linguagem C, por que não implementar o BWAPI for C e depois usar sua API? Boa ideia!


Sim, é uma boa ideia até que você analise a essência da BWAPI e veja o número de classes e métodos que você precisa implementar = (Especialmente todos esses layouts de estruturas na memória, assemblers, patches de memória e outros horrores para os quais não temos tempo. É necessário usar a solução existente ao máximo.


Mas devemos, de alguma forma, lidar com funções desconcertantes, de código C ++, herança e membro virtual.


Em C ++, existem duas ferramentas poderosas que usaremos para resolver nosso problema: são ponteiros opacos e extern "C" .


extern "C" {} permite que o código C ++ se disfarce como C. Isso permite gerar nomes de funções puros sem confundir.


Ponteiros opacos nos permitem apagar um tipo e criar um ponteiro para "algum tipo" cuja implementação não fornecemos. Como esta é apenas uma declaração de algum tipo, e não sua implementação, é impossível usar esse tipo por valor, ele pode ser usado apenas por ponteiro.


Digamos que temos esse código C ++:


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

Podemos transformá-lo em um cabeçalho C como este:


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

E a parte C ++, que será o link entre o cabeçalho C e a implementação C ++:


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

Nem todos os métodos de classe tiveram que ser tratados dessa maneira. Existem classes no BWAPI, operações nas quais você pode se implementar usando os valores de campo dessas estruturas, por exemplo, typedef struct Position { int x; int y; } Position; typedef struct Position { int x; int y; } Position; e métodos como Position::get_distance .


Havia quem tivesse que ser julgado de uma maneira especial. Por exemplo, um AIModule deve ser um ponteiro para uma classe C ++ com um conjunto específico de funções-membro virtuais. No entanto, aqui está o título e a implementação .


Assim, após vários meses de trabalho duro, 554 métodos e uma dúzia de classes, nasceu a biblioteca de plataforma cruzada BWAPI-C , que permite escrever bots em C. O subproduto foi a compilação cruzada e a capacidade de implementar a API em qualquer outro idioma que suporte a FFI e a convenção de chamada cdecl.


Se você estiver escrevendo uma biblioteca, escreva a API C.


O recurso mais importante do BWAPI-C é a mais ampla capacidade de integração com outros idiomas. Python , Ruby , Rust , PHP , Java e muitos outros sabem trabalhar com C, portanto, você também pode escrever um bot neles, se trabalhar um pouco com um arquivo e implementar seus wrappers.


Escrevendo um bot em C


Esta parte descreve os princípios gerais do design dos módulos Starcraft.


Existem 2 tipos de bots: módulo e cliente. Veremos um exemplo de escrita de um módulo.


Um módulo é uma biblioteca para download; o princípio geral de carregamento pode ser visto aqui . O módulo deve exportar 2 funções: newAIModule e gameInit .


Com gameInit tudo é simples, essa função é chamada para passar um ponteiro para o jogo atual. Esse ponteiro é muito importante, porque na natureza do BWAPI vive uma variável estática global usada em algumas partes do código. Vamos descrever gameInit :


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

newAIModule pouco mais complicado. Ele deve retornar um ponteiro para uma classe C ++ que possui uma tabela de método virtual com os nomes onXXXXX chamados em determinados eventos do jogo. Defina a estrutura do módulo:


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

O primeiro campo deve ser um ponteiro para uma tabela de métodos (mágica, todas as coisas). Portanto, a função newAIModule :


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

createAIModuleWrapper é outra mágica que transforma um ponteiro C em um ponteiro para uma classe C ++ com virtual métodos funções de membro.


module_vtable é uma variável estática na tabela de métodos, os valores dos métodos são preenchidos com ponteiros para funções globais:


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

Pelo nome das funções e suas assinaturas, fica claro em que condições e com quais argumentos eles são chamados. Por exemplo, deixei todas as funções vazias, exceto


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

Esta função é chamada quando o jogo começa. Um ponteiro para o módulo atual é passado como argumento. BWAPIC_getGame retorna um ponteiro global para o jogo que definimos chamando BWAPIC_setGame . Então, mostraremos um exemplo de compilação cruzada e operação de módulo:


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

Apertamos botões e começamos o jogo. Você pode ler mais sobre o lançamento no site da BWAPI e em BWAPI-C .


O resultado do módulo:


imagem


Um exemplo um pouco mais complexo de um módulo que mostra o trabalho com iteradores, controle de unidade, pesquisa mineral e estatísticas pode ser encontrado em bwapi-c / example / Dll.c.


bwapi-sys


No ecossistema Rasta, é habitual chamar pacotes de uma certa maneira que vinculam a bibliotecas nativas. Qualquer pacote foo-sys tem duas funções importantes:


  • Link para a biblioteca nativa libfoo
  • Fornece declarações de função da biblioteca libfoo. Mas apenas declarações, abstrações de alto nível em caixas * -sys não são fornecidas.

Para que o pacote * -sys possa ser vinculado com êxito, eles integram uma pesquisa de biblioteca nativa e / ou um conjunto de bibliotecas das fontes nele.


Para que o pacote * -sys forneça declarações, é necessário escrevê-las manualmente ou gerar usando bindgen. Novamente bindgen. Tentativa número dois =)


A geração de ligantes com bwapi-c se torna obscena:


 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 

Onde BWAPI.h é o arquivo com as inclusões de todos os cabeçalhos de cabeçalho do BWAPI-C.


Por exemplo, para funções já conhecidas, o bindgen gerou as seguintes declarações:


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

Existem duas estratégias: armazenar o código gerado no repositório e gerar código rapidamente durante a montagem. Ambas as abordagens têm suas vantagens e desvantagens .


Bem - vindo bwapi-sys , outro pequeno passo em direção ao nosso objetivo.


Lembre-se, eu falei sobre plataforma cruzada? Nlinker se juntou ao projeto e implementou uma estratégia complicada. Se o destino for Windows, faça o download do BWAPIC já montado no github. E para os demais alvos, coletamos o BWAPI-C a partir das fontes do OpenBW (falarei um pouco mais adiante).


bwapi-rs


Agora que temos os ligantes, podemos descrever abstrações de alto nível. Temos dois tipos para trabalhar: valores puros e ponteiros opacos.


Com valores puros, tudo é mais simples. Tome a cor como exemplo. Precisamos facilitar o uso do código Rust para que possamos usar as cores de maneira conveniente e natural:


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

Portanto, para uso conveniente, será necessário definir uma enumeração idiomática para a linguagem Rust com constantes do C ++ e definir métodos de conversão em bwapi_sys :: Color usando a característica 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, ... 

Embora por conveniência, você pode usar a caixa enum-primitive-derivate .


Com ponteiros opacos, não é mais difícil. Para fazer isso, use o padrão Newtype :


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

Ou seja, o Player é uma certa estrutura com um campo privado - um ponteiro opaco bruto de C. E aqui está como você pode descrever o método 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 } } 

Agora podemos escrever nosso primeiro bot no Rust!


Escrevendo um bot no Rust


Como prova de conceito, o bot parecerá um país famoso: toda a sua funcionalidade será contratar trabalhadores e coletar minerais.


Coreia do norte


Coreia do sul


Vamos começar com as gameInit necessárias gameInit e 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] executa a mesma função que extern "C" em C ++. Dentro do wrap_handler , todo tipo de mágica acontece com a substituição da tabela de funções virtuais e se disfarça como uma classe C ++.


A descrição da estrutura do módulo é ainda mais simples e mais bonita do que em C:


 struct ExampleAIModule { name: String, } 

Adicione alguns métodos para renderizar estatísticas e dar ordens:


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

Para que o tipo ExampleAIModule se transforme em um módulo real, você precisa ensiná-lo a responder a eventos onXXXX , para os quais é necessário implementar o tipo EventHandler, que é análogo à tabela virtual 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) {} } 

Construir e iniciar o módulo é tão fácil quanto para 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 ... ... ... 

E o vídeo do trabalho:



Um pouco sobre compilação cruzada


Em suma, Rust é lindo! Em dois cliques, você pode colocar muitas cadeias de ferramentas para plataformas diferentes. Especificamente, a cadeia de ferramentas i686-pc-windows-gnu é definida pelo comando:


 rustup target add i686-pc-windows-gnu 

Você também pode especificar o kofig para carga na raiz do projeto .cargo/config :


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

E é tudo o que você precisa fazer para compilar seu projeto Rust do Linux no Windows.


Openbw


Esses caras foram ainda mais longe. Eles decidiram escrever uma versão de código aberto do jogo SC: BW! E eles se dão muito bem. Um de seus objetivos era implementar imagens em HD, mas o SC: Remastered se adiantou a elas = (no momento, você pode usar sua API para escrever bots (sim, também em C ++). Mas o recurso mais surpreendente é a capacidade de visualizar replays diretamente no navegador .


Conclusão


Um problema não resolvido permaneceu durante a implementação: não controlamos a exclusividade dos links, e a existência simultânea de &mut e & ao alterar um objeto levará a um comportamento indefinido. Problema. Halt tentou implementar ligações idiomáticas, mas seu fusível desapareceu um pouco. Além disso, para resolver esse problema, você precisará descartar qualitativamente a API C ++ e definir corretamente os qualificadores const .


Gostei muito de trabalhar nesse projeto, assisti replays e mergulhei profundamente na atmosfera. Este jogo deixou 믿어 믿어 않을 정도 인 um legado. Nenhum jogo é popular no SC: BW, e seu impacto no 에게 정치 에게 era impensável. Os jogadores profissionais da Coréia do Sul são tão populares quanto os dramas coreanos transmitidos no horário nobre. 또한, 한국 에서 게이머 라면 군대 의 특별한 에 에 입대 할 수.


Viva o StarCraft!


Referências


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


All Articles