
StarCraft: Guerra da Ninhada . Este jogo significa muito para mim! E para muitos de vocês, eu acho. Tanto que me pergunto se devo dar um link para sua página na Wikipedia ou não.
Certa vez, Halt me enviou um PM e se ofereceu para aprender Rust . Como qualquer pessoa comum, decidimos começar com olá mundo escrever uma biblioteca dinâmica para Windows que possa ser carregada no espaço de endereço do StarCraft e gerenciar unidades.
O artigo a seguir descreve o processo de encontrar soluções e usar tecnologias e técnicas que permitirão que você aprenda coisas novas sobre o Rust e seu ecossistema. Você também pode se inspirar para implementar um bot usando sua linguagem favorita, seja C, C ++, Ruby, Python, etc.
Definitivamente, vale a pena ouvir o hino da Coréia do Sul enquanto lê este artigo:
Bwapi
Este jogo tem quase 20 anos. E ainda é popular ; Os campeonatos atraíram multidões de pessoas nos EUA, mesmo em 2017, onde ocorreu a batalha dos grandes mestres Jaedong vs Bisu. Além de jogadores humanos, máquinas sem alma também participam de batalhas de SC! E isso é possível por causa do BWAPI . Links mais úteis.
Por mais de uma década, houve uma comunidade de desenvolvedores de bot em torno deste jogo. Entusiastas criam bots e participam de vários campeonatos. Muitos deles estudam IA e aprendizado de máquina. O BWAPI é usado pelas universidades para treinar seus alunos. Existe até um canal de transmissão transmitindo essas correspondências.
Assim, uma equipe de fãs reverteu o back-end de Starcraft há vários anos e desenvolveu uma API em C ++, que permite criar bots, fazer injeções no processo do jogo e dominar humanos miseráveis.
Como costuma acontecer, antes construir uma casa, é necessário extrair minério, forjar ferramentas Ao criar um bot, você precisa implementar uma API. O que Rust tem a oferecer?
Ffi
É bastante simples trabalhar com outros idiomas do Rust. Existe um FFI para isso. Deixe-me dar um pequeno trecho da documentação .
Imagine que temos uma biblioteca snappy , que possui um arquivo de cabeçalho snappy-ch , que contém declarações de função.
Vamos criar 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
A Cargo criou uma estrutura de arquivos padrão para o projeto.
No Cargo.toml
, especificamos a dependência do libc :
[dependencies] libc = "0.2"
O arquivo src/main.rs
terá a seguinte aparência:
extern crate libc;
Vamos construir e executar o projeto:
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ê pode chamar apenas a cargo run
, que chama a cargo build
antes da execução. Outra opção é construir o projeto e chamar o binário diretamente:
snappy$ ./target/debug/snappy max compressed length of a 100 byte buffer: 148
Se a biblioteca snappy estiver instalada, o código será compilado (para o Ubuntu, você deve instalar o pacote libsnappy-dev).
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 uma chamada para snappy_max_compressed_length
em nosso código é uma chamada de função desta biblioteca.
ferrugem-bindgen
Seria bom se pudéssemos gerar automaticamente nosso FFI. Felizmente, existe um utilitário chamado rust-bindgen na caixa de ferramentas de um viciado em Rust. É capaz de gerar ligações FFI para bibliotecas C (e algumas C ++).
Instalação:
$ cargo install bindgen
Como é a ferrugem-bindgen ? Pegamos os arquivos de cabeçalho C / C ++, apontamos o utilitário bindgen para eles, e a saída que obtemos é gerada pelo código Rust com as declarações apropriadas para nos permitir usar estruturas e funções C. Aqui está o que o bindgen
gera 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 não pode lidar com cabeçalhos BWAPI, gerando toneladas de código não utilizável (por causa de funções membro virtuais, std :: string em uma API pública, etc.). A questão é que o BWAPI é escrito em C ++. C ++ é geralmente difícil de usar, mesmo em projetos C ++. Depois que a biblioteca é montada, é melhor vinculá-la ao mesmo vinculador (mesma versão), os arquivos de cabeçalho devem ser analisados com o mesmo compilador (mesma versão). Todos esses fatores podem afetar o resultado. Mangling, por exemplo, que ainda não pode ser implementado sem erros no GNU GCC. Esses fatores são tão significativos que nem o gtest conseguiu superá-lo. E na documentação diz: é melhor você criar o gtest como parte do projeto pelo mesmo compilador e pelo mesmo vinculador.
Bwapi-c
C é a língua franca da engenharia de software. Se rust-bindgen funciona bem para a linguagem C, por que não implementar o BWAPI for C e usar sua API? Boa ideia!
Sim, é uma boa ideia até você examinar o interior da BWAPI e ver o número de classes e métodos que você deve implementar. Especialmente todos esses layouts de memória, códigos ASM, patches de memória e outros "horrores" para os quais não temos tempo. É necessário usar a solução existente completamente.
Mas precisamos de alguma forma superar as funções desconcertantes, de código C ++, herança e membro virtual.
Em C ++, existem duas ferramentas poderosas que usaremos para resolver nosso problema: ponteiros opacos e extern "C"
.
extern "C" {}
permite que o código C ++ "oculte" a si mesmo em C. Torna possível gerar nomes de funções sem confundir.
Ponteiros opacos nos permitem limpar o tipo e criar um ponteiro para "algum tipo" sem fornecer sua implementação. Como essa é apenas uma declaração de algum tipo, é impossível usar esse tipo por valor, você pode usá-lo apenas por ponteiro.
Vamos imaginar que temos esse código C ++:
namespace cpp { struct Foo { int bar; virtual int get_bar() { return this->bar; } }; }
Podemos transformá-lo em um cabeçalho C:
extern "C" { typedef struct Foo_ Foo; // Opaque pointer to Foo // call cpp::Foo::get_bar int Foo_get_bar(Foo* self); }
E aqui está a parte do C ++ que será o link entre o cabeçalho C e a implementação do C ++:
int Foo_get_bar(Foo* self) {
Nem todos os métodos das classes tiveram que ser processados dessa maneira. No BWAPI, existem classes que você pode implementar usando os campos 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 aulas que eu tinha que tratar de uma maneira especial. Por exemplo, AIModule deve ser um ponteiro para uma classe C ++ com um conjunto específico de funções-membro virtuais. No entanto, aqui está o cabeçalho 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 criar bots em C. Um subproduto era a possibilidade de compilação cruzada e a capacidade de implementar a API em qualquer outro idioma que suporte FFI e a convenção de chamada cdecl.
Se você estiver escrevendo uma biblioteca, escreva sua API em C.
O recurso mais importante do BWAPI-C é a integração mais ampla possível com outras linguagens de programação. Python
, Ruby
, Rust
, PHP
, Java
e muitos outros são capazes de trabalhar com C; portanto, se você resolver os problemas e implementar os próprios wrappers, também poderá escrever um bot com a ajuda deles.
Escrevendo um bot em C
Esta parte descreve os princípios gerais da organização interna dos módulos Starcraft.
Existem 2 tipos de bots: módulo e cliente. Vejamos um exemplo de escrita de um módulo.
O módulo é uma biblioteca dinâmica. O princípio geral de carregamento de bibliotecas dinâmicas pode ser visto aqui . O módulo deve exportar 2 funções: newAIModule
e gameInit
.
gameInit
é fácil. Esta função é chamada para passar um ponteiro para um jogo atual. Esse ponteiro é muito importante, porque existe uma variável estática global nos caracteres curinga da BWAPI, que é usada em algumas seções do código. Vamos descrever gameInit
:
DLLEXPORT void gameInit(void* game) { BWAPIC_setGame(game); }
newAIModule
é um pouco mais complicado. Ele deve retornar o ponteiro para uma classe C ++, que possui uma tabela de métodos virtuais com nomes como onXXXXX, chamados em determinados eventos do jogo. Vamos declarar a estrutura do módulo:
typedef struct ExampleAIModule { const AIModule_vtable* vtable_; const char* name; } ExampleAIModule;
O primeiro campo deve ser um ponteiro para a tabela de métodos (é um tipo de mágica). Aqui está 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
é outro truque de mágica que transforma o ponteiro C no ponteiro da classe C ++ com métodos virtuais 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) {}
Se você observar o nome das funções e suas assinaturas, fica claro em que condições e com quais argumentos eles precisam ser 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 é executado. O argumento é um ponteiro para o módulo atual. BWAPIC_getGame
retorna um ponteiro global para o jogo, que definimos usando uma chamada para BWAPIC_setGame
. Então, vamos mostrar um exemplo prático de compilação cruzada de um 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 ... ... ...
Aperte os botões e execute o jogo. Mais informações sobre compilação e execução podem ser encontradas no site da BWAPI e em BWAPI-C .
O resultado do módulo:

Você pode encontrar um exemplo um pouco mais complicado de um módulo que mostra como trabalhar com iteradores, gerenciamento de unidades, pesquisa mineral e saída de estatísticas em bwapi-c / example / Dll.c.
bwapi-sys
No ecossistema Rust, existe uma certa maneira de nomear pacotes vinculados a bibliotecas nativas. Qualquer pacote foo-sys executa duas funções importantes:
- links com a biblioteca nativa libfoo;
- fornece declarações para as funções da biblioteca libfoo. Mas apenas declarações! Abstrações de alto nível não são fornecidas em caixas * -sys.
Para que o pacote * -sys seja capaz de vincular com êxito, você deve solicitar ao cargo que procure a biblioteca nativa e / ou construa a biblioteca a partir das fontes.
Para que o pacote * -sys forneça declarações, você precisa escrevê-las por conta própria ou gerá-las usando o bindgen. Novamente bindgen. Tentativa número dois =)
A geração de ligações é super fácil:
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
BWAPI.h
é um arquivo com todos os cabeçalhos C do BWAPI-C.
Por exemplo, o bindgen já gerou essas declarações para as funções acima:
extern "C" {
Existem duas estratégias: armazenar o código gerado no repositório e gerar o código rapidamente durante a compilação. Ambas as abordagens têm suas vantagens e desvantagens .
Prazer em conhecê-lo bwapi-sys ; mais um pequeno passo para o nosso objetivo.
Você se lembra que antes eu estava falando sobre multiplataforma? O nlinker entrou no projeto e implementou uma estratégia astuta. Se o host de 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 sobre isso mais adiante).
bwapi-rs
Agora temos as ligações e podemos definir abstrações de alto nível. Temos dois tipos para trabalhar: valores puros e ponteiros opacos.
Tudo é simples, com valores puros. Vamos tomar as cores como exemplo. Precisamos facilitar o uso do código Rust para usar as cores de maneira conveniente e natural:
game.draw_line(CoordinateType::Screen, (10, 20), (30, 40), Color::Red); ^^^
Portanto, para uso conveniente, seria necessário definir a enumeração com constantes C ++, mas também idiomática para Rust, e definir métodos de conversão em bwapi_sys :: Color usando std :: convert :: From:
Para sua conveniência, você pode usar a caixa enum-primitive- derivate.
Também é fácil usar ponteiros opacos. Vamos usar o padrão Newtype :
pub struct Player(*mut sys::Player);
Isso significa que o Player é um tipo de estrutura com um campo privado - um ponteiro opaco bruto de C. E aqui está a maneira como você pode definir o Player :: color:
impl Player {
Agora podemos escrever nosso primeiro bot no Rust!
Criando um bot no Rust
Como prova de conceito, o bot será semelhante a um país conhecido: toda a tarefa é contratar trabalhadores e coletar minerais.


Vamos começar com as funções 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
toda a mágica ocorre, com a substituição da tabela de funções virtuais e a classe C ++ "mascarada".
As definições da estrutura do módulo são ainda mais simples e sofisticadas do que em C:
struct ExampleAIModule { name: String, }
Vamos adicionar alguns métodos para renderizar as 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)) {
Para transformar o tipo ExampleAIModule em um módulo real, é necessário fazê-lo responder aos eventos onXXXX . Para fazer isso, você precisa implementar o tipo EventHandler, que é análogo da 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) {} }
Criar e executar o módulo é tão simples quanto em 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:
Openbw
Esses caras foram ainda mais longe. Eles decidiram escrever uma versão de código aberto do SC: BW! E eles são bons nisso. Um de seus objetivos era implementar imagens em HD, mas SC: Remastered estava à frente deles = (neste momento, você pode usar sua API para escrever bots (sim, também em C ++), mas o recurso mais surpreendente é a capacidade de visualizar reproduz diretamente no seu navegador .
Conclusão
Houve um problema não resolvido com a implementação: não controlamos as referências como únicas; portanto, a existência de &mut
e &
na mesma região resultará em um comportamento indefinido quando o objeto for modificado. Um tipo de problema. Halt tentou implementar ligações idiomáticas, mas não conseguiu encontrar uma solução. Além disso, se você quiser concluir esta tarefa, precisará "escavar" com cuidado a API C ++ e colocar os qualificadores const
corretamente.
Eu realmente gostei de trabalhar neste projeto, assisti aos replays 종일 e profundamente imerso na atmosfera. Este jogo fez um abalo em um universo. Nenhum jogo pode ser popular pela popularidade de SC: BW, e seu impacto no 대한민국 정치 에게 era impensável. Os jogadores profissionais da Coréia do Sul são tão populares quanto os Dorams coreanos transmitidos no horário nobre. 또한, 한국 에서 게이머 라면 군대 의 특별한 에 에 입대 할 수.
Viva o StarCraft!
Ligações
Muito obrigado a Steve Klabnik por me ajudar com a revisão do artigo.