Un bot para Starcraft en Rust, C o cualquier otro idioma


StarCraft: Brood War . ¡Este juego significa mucho para mí! Y para muchos de ustedes, supongo. Tanto, que me pregunto si debería incluso dar un enlace a su página en Wikipedia o no.


Una vez que Halt me envió PM y se ofreció a aprender Rust . Como cualquier persona común, decidimos comenzar con hola mundo escribir una biblioteca dinámica para Windows que pueda cargarse en el espacio de direcciones de StarCraft y administrar unidades.


El siguiente artículo describirá el proceso de búsqueda de soluciones y el uso de tecnologías y técnicas que le permitirán aprender cosas nuevas sobre Rust y su ecosistema. También puede inspirarse para implementar un bot usando su lenguaje favorito, ya sea C, C ++, Ruby, Python, etc.


Definitivamente vale la pena escuchar el himno de Corea del Sur mientras lee este artículo:


Starcraft OST

Bwapi


Este juego tiene casi 20 años. Y sigue siendo popular ; Los campeonatos atrajeron multitudes de personas en los EE. UU. incluso en 2017, donde tuvo lugar la batalla de los grandes maestros Jaedong vs Bisu. ¡Además de los jugadores humanos, las máquinas sin alma también participan en batallas SC! Y esto es posible gracias a BWAPI . Más enlaces útiles.


Durante más de una década, ha habido una comunidad de desarrolladores de bots en torno a este juego. Los entusiastas crean bots y participan en varios campeonatos. Muchos de ellos estudian IA y aprendizaje automático. BWAPI es utilizado por las universidades para capacitar a sus estudiantes. Incluso hay un canal de contracción que transmite dichos partidos.


Entonces, un equipo de fanáticos revirtió el back-end de Starcraft hace varios años y desarrolló una API en C ++, que le permite crear bots, realizar inyecciones en el proceso del juego y dominar a los humanos miserables.


Como suele suceder, antes para construir una casa, es necesario extraer mineral, forjar herramientas para crear un bot, debes implementar una API. ¿Qué tiene que ofrecer Rust?


Ffi


Es bastante simple trabajar con otros idiomas de Rust. Hay un FFI para esto. Déjame darte un breve extracto de la documentación .


Imaginemos que tenemos una biblioteca rápida , que tiene un archivo de encabezado snappy-ch , que contiene declaraciones de funciones.


Creemos un proyecto 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 ha creado una estructura de archivos estándar para el proyecto.


En Cargo.toml especificamos dependencia a libc :


 [dependencies] libc = "0.2" 

El archivo src/main.rs se verá así:


 extern crate libc; // To import C types, in our case for size_t use libc::size_t; #[link(name = "snappy")] // Specify the name of the library for linking the function extern { // We write the declaration of the function which we want to import // in C the declaration looks like this: // 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); } 

Construyamos y ejecutemos el proyecto:


 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 

Solo puede llamar a cargo run , que llama a cargo build antes de la ejecución. Otra opción es construir el proyecto y llamar al binario directamente:


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

Si la biblioteca snappy está instalada, el código se compilará (para Ubuntu debe instalar el paquete libsnappy-dev).


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

Como puede ver, nuestro binario está vinculado a la biblioteca compartida libsnappy. Y una llamada a snappy_max_compressed_length en nuestro código es una llamada de función de esta biblioteca.


Rust-Bindgen


Sería bueno si pudiéramos generar automáticamente nuestro FFI. Afortunadamente, hay una utilidad llamada rust-bindgen en la caja de herramientas de un adicto a Rust. Es capaz de generar enlaces FFI a bibliotecas C (y algunas C ++).


Instalación:


 $ cargo install bindgen 

¿Cómo se ve el rust-bindgen ? Tomamos los archivos de encabezado C / C ++, les señalamos la utilidad bindgen , y la salida que obtenemos se genera código Rust con las declaraciones adecuadas para permitirnos usar estructuras y funciones C. bindgen es lo que genera bindgen para 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; } 

Resulta que bindgen no puede hacer frente a los encabezados BWAPI, generando toneladas de código no utilizable (debido a las funciones de miembros virtuales, std :: string en una API pública, etc.). El caso es que BWAPI está escrito en C ++. C ++ es generalmente difícil de usar incluso en proyectos de C ++. Una vez que la biblioteca está ensamblada, es mejor vincularla con el mismo vinculador (misma versión), los archivos de encabezado deben analizarse con el mismo compilador (misma versión). Todos estos factores pueden afectar el resultado. Mangling, por ejemplo, que todavía no se puede implementar sin errores en GNU GCC. Estos factores son tan importantes que incluso gtest no pudo superarlo. Y en la documentación dice: será mejor que construyas gtest como parte del proyecto con el mismo compilador y el mismo vinculador.


Bwapi-c


C es la lengua franca de la ingeniería de software. Si rust-bindgen funciona bien para el lenguaje C, ¿por qué no implementar BWAPI para C y luego usar su API? Buena idea!


Sí, es una buena idea hasta que analice el interior de BWAPI y vea la cantidad de clases y métodos que debe implementar. Especialmente todos estos diseños de memoria, códigos asm, parches de memoria y otros "horrores" para los que no tenemos tiempo. Es necesario utilizar la solución existente por completo.


Pero necesitamos de alguna manera superar las funciones de destrucción, código C ++, herencia y miembros virtuales.


En C ++, hay dos herramientas poderosas que usaremos para resolver nuestro problema: punteros opacos y extern "C" .


extern "C" {} permite que el código C ++ se "enmascare" a sí mismo bajo C. Permite generar nombres de funciones sin alterarlos.


Los punteros opacos nos permiten borrar el tipo y crear un puntero a "algún tipo" sin proporcionar su implementación. Dado que esto es solo una declaración de algún tipo, es imposible usar este tipo por valor, puede usarlo solo por puntero.


Imaginemos que tenemos este código C ++:


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

Podemos convertirlo en un encabezado C:


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

Y aquí está la parte de C ++ que será el enlace entre el encabezado de C y la implementación de C ++:


 int Foo_get_bar(Foo* self) { // cast the opaque pointer to the certain cpp::Foo and call the method ::get_bar return reinterpret_cast<cpp::Foo*>(self)->get_bar(); } 

No todos los métodos de clases tuvieron que ser procesados ​​de esta manera. En BWAPI, hay clases que puede implementar usted mismo usando los campos de estas estructuras, por ejemplo typedef struct Position { int x; int y; } Position; typedef struct Position { int x; int y; } Position; y métodos como Position::get_distance .


Hubo clases que tuve que tratar de una manera especial. Por ejemplo, AIModule debería ser un puntero a una clase C ++ con un conjunto específico de funciones de miembro virtual. Sin embargo, aquí está el encabezado y la implementación .


Entonces, después de varios meses de arduo trabajo, 554 métodos y una docena de clases, nació la biblioteca multiplataforma BWAPI-C , que le permite crear bots en C. Un subproducto fue la posibilidad de compilación cruzada y la capacidad de implementar la API en cualquier otro lenguaje que admita FFI y la convención de llamadas cdecl.


Si está escribiendo una biblioteca, escriba su API en C.


La característica más importante de BWAPI-C es la integración más amplia posible con otros lenguajes de programación. Python , Ruby , Rust , PHP , Java y muchos otros pueden trabajar con C, por lo que si soluciona los problemas e implementa propios envoltorios, también puede escribir un bot con su ayuda.


Escribir un bot en C


Esta parte describe los principios generales de la organización interna de los módulos de Starcraft.


Hay 2 tipos de bots: módulo y cliente. Veamos un ejemplo de escritura de un módulo.


El módulo es una biblioteca dinámica. El principio general de cargar bibliotecas dinámicas se puede ver aquí . El módulo debe exportar 2 funciones: newAIModule y gameInit .


gameInit es fácil. Esta función se llama para pasar un puntero a un juego actual. Este puntero es muy importante, porque hay una variable estática global en el comodín de BWAPI, que se usa en algunas secciones del código. gameInit :


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

newAIModule es un poco más complicado. Debería devolver el puntero a una clase C ++, que tiene una tabla de método virtual con nombres como onXXXXX que se llaman en ciertos eventos del juego. Declaremos la estructura del módulo:


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

El primer campo debe ser un puntero a la tabla de métodos (es una especie de magia). Aquí está la newAIModule :


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

createAIModuleWrapper es otro truco de magia que convierte el puntero C en el puntero a la clase C ++ con métodos virtuales funciones miembro


module_vtable es una variable estática en la tabla de métodos, los valores de los métodos se completan con punteros a funciones 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) {} 

Si observa el nombre de las funciones y sus firmas, queda claro en qué condiciones y con qué argumentos deben llamarse. Por ejemplo , hice todas las funciones vacías, excepto


 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 función se llama cuando se ejecuta el juego. El argumento es un puntero al módulo actual. BWAPIC_getGame devuelve un puntero global al juego, que configuramos mediante una llamada a BWAPIC_setGame . Entonces, vamos a mostrar un ejemplo funcional de compilación cruzada de un 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 ... ... ... 

Presiona los botones y ejecuta el juego. Puede encontrar más información sobre compilación y ejecución en el sitio web de BWAPI y en BWAPI-C .


El resultado del módulo:


imagen


Puede encontrar un ejemplo un poco más complicado de un módulo que muestra cómo trabajar con iteradores, administración de unidades, búsqueda de minerales y salida de estadísticas en bwapi-c / example / Dll.c.


bwapi-sys


En el ecosistema Rust hay una cierta forma de nombrar paquetes que enlazan con bibliotecas nativas. Cualquier paquete foo-sys ejecuta dos funciones importantes:


  • enlaces con la biblioteca nativa libfoo;
  • proporciona declaraciones a las funciones de la biblioteca libfoo. ¡Pero solo declaraciones! Las abstracciones de alto nivel no se proporcionan en cajas * -sys.

Para que el paquete * -sys pueda vincularse con éxito, debe indicarle a la carga que busque la biblioteca nativa y / o que construya la biblioteca a partir de las fuentes.


Para que el paquete * -sys proporcione declaraciones, debe escribirlas usted mismo o generarlas usando bindgen. De nuevo bindgen. Intento número dos =)


La generación de enlaces es súper 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 es un archivo con todos los encabezados C de BWAPI-C.


Por ejemplo, bindgen ya ha generado tales declaraciones para las funciones anteriores:


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

Hay dos estrategias: almacenar el código generado en el repositorio y generar el código sobre la marcha durante la compilación. Ambos enfoques tienen sus ventajas y desventajas .


Encantado de conocerte bwapi-sys ; Un pequeño paso más hacia nuestro objetivo.


¿Recuerdas que antes estaba hablando de multiplataforma? nlinker se unió al proyecto e implementó una estrategia astuta. Si el host de destino es Windows, descargue el BWAPIC ya ensamblado de GitHub. Y para los objetivos restantes, recopilamos BWAPI-C de las fuentes para OpenBW (te contaré un poco más adelante).


bwapi-rs


Ahora tenemos los enlaces y podemos definir abstracciones de alto nivel. Tenemos dos tipos con los que trabajar: valores puros y punteros opacos.


Todo es simple con valores puros. Tomemos los colores como ejemplo. Necesitamos facilitar el uso del código Rust para usar los colores de una manera conveniente y natural:


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

Entonces, para un uso conveniente, sería necesario definir la enumeración con constantes de C ++ pero también idiomática para Rust, y definir métodos para convertirla en bwapi_sys :: Color usando 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, ... 

Para su comodidad, puede usar la caja de derivación enum-primitive .


También es fácil de usar punteros opacos. Usemos el patrón Newtype :


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

Significa que Player es un tipo de estructura con un campo privado: un puntero opaco en bruto de C. Y esta es la forma en que puede definir Player :: color:


 impl Player { // so the method is declared Player::getColor in bwapi-sys //extern "C" { // pub fn Player_getColor(self_: *mut Player) -> Color; //} pub fn color(&self) -> Color { // bwapi_sys::Player_getColor - wrapper function from BWAPI-C // self.0 - opaque pointer let color = unsafe { bwapi_sys::Player_getColor(self.0) }; color.into() // cast bwapi_sys::Color -> Color } } 

¡Ahora podemos escribir nuestro primer bot en Rust!


Crear un bot en Rust


Como prueba de concepto, el bot será similar a un instrumento bien conocido: toda la tarea es contratar trabajadores y recolectar minerales.


Corea del norte


Corea del sur


Comencemos con las funciones requeridas gameInit y 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] realiza la misma función que la extern "C" en C ++. Dentro de wrap_handler produce toda la magia, con la sustitución de la tabla de funciones virtuales y la clase de C ++ "enmascarado".


Las definiciones de la estructura del módulo son aún más simples y elegantes que en C:


 struct ExampleAIModule { name: String, } 

Agreguemos un par de métodos para representar las estadísticas y dar órdenes:


 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 convertir el tipo ExampleAIModule en un módulo real, debe hacer que responda a los eventos onXXXX . Para hacerlo, debe implementar el tipo EventHandler, que es un análogo de la tabla 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 y ejecutar el módulo es tan simple como 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 ... ... ... 

Y el video de la obra:



Openbw


Estos muchachos fueron aún más lejos. Decidieron escribir una versión de código abierto de SC: BW! Y son buenos en eso. Uno de sus objetivos era implementar imágenes HD, pero SC: Remastered estaba por delante de ellos = (En este momento, puede usar su API para escribir bots (sí, también en C ++), pero la característica más sorprendente es la capacidad de ver repeticiones directamente en su navegador .


Conclusión


Hubo un problema sin resolver con la implementación: no controlamos que las referencias sean únicas, por lo que la existencia de &mut y & en la misma región dará como resultado un comportamiento indefinido cuando se modifique el objeto. Un poco problemático. Halt intentó implementar enlaces idiomáticos, pero no logró encontrar una solución. Además, si desea completar esta tarea, debe "remover" cuidadosamente la API de C ++ y colocar los calificadores const correctamente.


Realmente disfruté trabajando en este proyecto, vi las repeticiones 하루 종일 y me sumergí profundamente en la atmósfera. Este juego hizo mella en un universo. Ningún juego puede ser 비교할 수 없다 por popularidad con SC: BW, y su impacto en 대한민국 정치 에게 fue impensable. Los jugadores profesionales en Corea 아마도 son tan populares como los 드라마 주연 배우 들 que transmiten dorams coreanos en horario estelar. 또한, 한국 에서 프로 게이머 라면 군대 의 특별한 육군 에 입대 할 수 수.


¡Viva StarCraft!






Muchas gracias a Steve Klabnik por ayudarme con la revisión del artículo.

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


All Articles