Bot para Starcraft en Rust, C y cualquier otro idioma

StarCraft: Brood War . Cuánto significa eso para mí. Y para muchos de ustedes. Tantos que dudé si dar un enlace a la wiki.


Una vez, Halt me llamó en un correo electrónico personal y se ofreció a aprender Rust . Como cualquier persona normal, decidimos comenzar con hola mundo escribir una biblioteca dinámica para Windows que pueda cargarse en el espacio de direcciones de un juego de StarCraft y administrar unidades.


El artículo describirá el proceso de encontrar soluciones, utilizando tecnologías, técnicas que le permitirán aprender cosas nuevas en el lenguaje Rust y su ecosistema o inspirarse para implementar un bot en su idioma favorito, ya sea C, C ++, ruby, python, etc.


Vale la pena leer este artículo bajo el himno de Corea del Sur:


Starcraft OST

Bwapi


Este juego ya tiene 20 años. Y todavía es popular , los campeonatos reúnen salas enteras 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 en vivo, los coches sin alma también participan en batallas! 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 escriben bots y participan en varios campeonatos. Muchos de ellos estudian IA y aprendizaje automático. BWAPI es utilizado por las universidades para educar a sus estudiantes. Incluso hay un canal de contracción que transmite juegos.


Entonces, un equipo de fanáticos hace unos años revirtió las partes internas de Starcraft y escribió una API C ++ que le permite escribir bots, integrarse en el proceso del juego y dominar a las personas patéticas.


Como suele suceder antes para construir una casa, necesitas obtener mineral, forjar herramientas ... escribe un bot, necesitas implementar la API. ¿Qué puede ofrecer Rust?


Ffi


Interactuar con otros idiomas de Rust es bastante simple. Hay FFI para esto. Permítanme proporcionar un breve extracto de la documentación .


Supongamos que tenemos una biblioteca snappy que tiene un archivo de encabezado snappy-ch desde el cual copiaremos las declaraciones de funciones.


Crea 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 creó una estructura de archivos estándar para el proyecto.


En Cargo.toml especifique la dependencia de libc :


 [dependencies] libc = "0.2" 

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


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

Recopilamos y ejecutamos:


 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 antes del lanzamiento llama a cargo build . O cree un proyecto y llame al binario directamente:


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

El código se compila con la condición de que la biblioteca rápida esté instalada (para Ubuntu, el paquete libsnappy-dev debe estar instalado).


 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 la llamada snappy_max_compressed_length es una llamada de función de esta biblioteca.


Rust-Bindgen


Sería bueno si pudiéramos generar automáticamente FFI. Afortunadamente, hay una utilidad en el arsenal de rastomanov llamada rust-bindgen . Es capaz de generar enlaces FFI a bibliotecas C (y algunas C ++).


Instalación:


 $ cargo install bindgen 

¿Cómo se ve el uso de Rust-Bindgen ? Tomamos los archivos de encabezado C / C ++, establecemos la utilidad bindgen en ellos y obtenemos el código Rust generado con las definiciones de las estructuras y funciones de la estructura. Así es como se ve la generación FFI 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; } 

Resultó que bindgen pasa frente a los encabezados de BWAPI, generando toneladas de hojas de código no utilizables (debido a las funciones de miembro virtual, std :: string en la 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 es mejor vincular una biblioteca compilada con el mismo vinculador (versiones idénticas), es mejor analizar los archivos de encabezado con el mismo compilador (versiones idénticas). Porque hay muchos factores que pueden afectar el resultado. Por ejemplo, destrozar , que GNU GCC todavía no puede implementar sin errores . Estos factores son tan importantes que no pueden superarse ni siquiera en gtest , y la documentación indica que sería mejor para usted construir gtest como parte del proyecto con el mismo compilador y el mismo vinculador.


Bwapi-c


C es la programación de lingua franca. 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 las entrañas de BWAPI y vea la cantidad de clases y métodos que necesita implementar = (Especialmente todos estos diseños de estructuras en memoria, ensambladores, parches de memoria y otros horrores para los que no tenemos tiempo. Es necesario utilizar la solución existente al máximo.


Pero de alguna manera debemos ocuparnos de las funciones de manipulación, código C ++, herencia y miembros virtuales.


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


extern "C" {} permite que el código C ++ se disfrace como C. Esto le permite generar nombres de funciones puros sin alterarlos.


Los punteros opacos nos dan la capacidad de borrar un tipo y crear un puntero a "algún tipo" cuya implementación no proporcionamos. Dado que esto es solo una declaración de algún tipo, y no su implementación, es imposible usar este tipo por valor, solo puede usarse por puntero.


Digamos que tenemos ese código C ++:


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

Podemos convertirlo en un encabezado C como este:


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

Y 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) { //      cpp::Foo    ::get_bar return reinterpret_cast<cpp::Foo*>(self)->get_bar(); } 

No todos los métodos de clase tuvieron que ser manejados de esta manera. Hay clases en BWAPI, operaciones en las que puede implementarse utilizando los valores de campo 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 quienes tuvieron que ser juzgados de una manera especial. Por ejemplo, un AIModule debe ser un puntero a una clase C ++ con un conjunto específico de funciones miembro virtuales. Sin embargo, aquí está el título 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 escribir bots en C. El subproducto fue la compilación cruzada y la capacidad de implementar la API en cualquier otro idioma que admita FFI y la convención de llamadas cdecl.


Si está escribiendo una biblioteca, escriba la API de C.


La característica más importante de BWAPI-C es la capacidad más amplia para integrarse con otros idiomas. Python , Ruby , Rust , PHP , Java y muchos otros saben cómo trabajar con C, por lo tanto, también puede escribir un bot en ellos, si trabaja un poco con un archivo e implementa sus envoltorios.


Escribir un bot en C


Esta parte describe los principios generales del diseño de los módulos de Starcraft.


Hay 2 tipos de bots: módulo y cliente. Veremos un ejemplo de cómo escribir un módulo.


Un módulo es una biblioteca descargable; el principio general de carga se puede ver aquí . El módulo debe exportar 2 funciones: newAIModule y gameInit .


Con gameInit todo es simple, esta función se llama para pasar un puntero al juego actual. Este puntero es muy importante, porque en la naturaleza de BWAPI vive una variable estática global que se usa en algunas partes del código. gameInit :


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

newAIModule poco más complicado. Debería devolver un puntero a una clase C ++ que tenga una tabla de método virtual con los nombres en XXXXX que se invocan en ciertos eventos del juego. Defina la estructura del módulo:


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

El primer campo debe ser un puntero a una tabla de métodos (magia, todas las cosas). Entonces, la newAIModule función newAIModule :


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

createAIModuleWrapper es otra magia que convierte un puntero C en un puntero a una clase C ++ con virtual métodos 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) {} 

Por el nombre de las funciones y sus firmas queda claro en qué condiciones y con qué argumentos se llaman. 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 comienza el juego. Se pasa un puntero al módulo actual como argumento. BWAPIC_getGame devuelve un puntero global al juego que configuramos llamando a BWAPIC_setGame . Entonces, mostraremos un ejemplo de compilación cruzada y operación del 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 ... ... ... 

Tocamos botones y comenzamos el juego. Puede leer más sobre el lanzamiento en el sitio web de BWAPI y en BWAPI-C .


El resultado del módulo:


imagen


Puede encontrar un ejemplo un poco más complejo de un módulo que muestra el trabajo con iteradores, control de unidades, búsqueda de minerales y estadísticas en bwapi-c / example / Dll.c.


bwapi-sys


En el ecosistema Rasta, es habitual llamar a los paquetes de una manera determinada que se vinculan a las bibliotecas nativas. Cualquier paquete foo-sys tiene dos funciones importantes:


  • Enlace a la biblioteca nativa de libfoo
  • Proporciona declaraciones de funciones de la biblioteca libfoo. Pero solo no se proporcionan declaraciones, abstracciones de alto nivel en cajas * -sys.

Para que el paquete * -sys pueda vincularse con éxito, integran una búsqueda de biblioteca nativa y / o un ensamblaje de biblioteca de las fuentes en él.


Para que el paquete * -sys proporcione declaraciones, uno debe escribirlas a mano o generar usando bindgen. De nuevo bindgen. Intento número dos =)


Generar carpetas con bwapi-c se vuelve obsceno:


 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 

Donde BWAPI.h es el archivo con las inclusiones de todos los encabezados de encabezado de BWAPI-C.


Por ejemplo, para funciones ya conocidas, bindgen generó las siguientes declaraciones:


 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 2 estrategias: almacenar el código generado en el repositorio y generar código sobre la marcha durante el ensamblaje. Ambos enfoques tienen sus ventajas y desventajas .


Bienvenido bwapi-sys , otro pequeño paso hacia nuestro objetivo.


¿Recuerdas que hablé de multiplataforma? Nlinker se unió al proyecto e implementó una estrategia difícil. Si el objetivo es Windows, descargue el BWAPIC ya ensamblado del github. Y para los objetivos restantes, recopilamos BWAPI-C de las fuentes para OpenBW (te contaré un poco más adelante).


bwapi-rs


Ahora que tenemos las carpetas, podemos describir las abstracciones de alto nivel. Tenemos 2 tipos con los que trabajar: valores puros y punteros opacos.


Con valores puros, todo es más simple. Toma el color como ejemplo. Necesitamos que sea conveniente usar el código Rust para que podamos 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á necesario definir una idiomática para la enumeración del lenguaje Rust con constantes de C ++ y definir métodos de conversión en bwapi_sys :: Color usando std :: convert :: From rasgo:


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

Aunque por conveniencia, puede usar la caja enum-primitive-derivar .


Con punteros opacos, no es más difícil. Para hacer esto, use el patrón Newtype :


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

Es decir, Player es una determinada estructura con un campo privado: un puntero opaco sin formato de C. Y así es como puede describir el 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 } } 

¡Ahora podemos escribir nuestro primer bot en Rust!


Escribiendo un bot en Rust


Como prueba de concepto, el bot se verá como un país famoso: toda su funcionalidad será contratar trabajadores y recolectar minerales.


Corea del norte


Corea del sur


Comencemos con las gameInit requeridas de 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 , todo tipo de magia ocurre con la sustitución de la tabla de funciones virtuales y se disfraza como una clase de C ++.


La descripción de la estructura del módulo es aún más simple y hermosa que en C:


 struct ExampleAIModule { name: String, } 

Agregue un par de métodos para representar 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 que el tipo ExampleAIModule se convierta en un módulo real, debe enseñarle a responder a los eventos onXXXX , para lo cual 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 e iniciar el módulo es tan fácil 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 trabajo:



Un poco sobre compilación cruzada


En resumen, Rust es hermoso! En dos clics, puede poner muchas cadenas de herramientas para diferentes plataformas. Específicamente, la cadena de herramientas i686-pc-windows-gnu se establece mediante el comando:


 rustup target add i686-pc-windows-gnu 

También puede especificar el kofig para carga en la raíz del proyecto .cargo/config :


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

Y eso es todo lo que necesita hacer para compilar su proyecto Rust desde Linux en Windows.


Openbw


Estos muchachos fueron aún más lejos. Decidieron escribir una versión de código abierto del juego SC: BW! Y les va bastante bien. Uno de sus objetivos era implementar imágenes HD, pero SC: Remastered se adelantó a ellas = (Por el momento, puedes 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 el navegador .


Conclusión


Un problema no resuelto permaneció durante la implementación: no controlamos la unicidad de los enlaces, y la existencia simultánea de &mut y & al cambiar un objeto conducirá a un comportamiento indefinido. Problemas Halt intentó implementar enlaces idiomáticos, pero su fusible se desvaneció ligeramente. Además, para resolver este problema, deberá nivelar cualitativamente la API de C ++ y establecer correctamente los calificadores const .


Realmente disfruté trabajando en este proyecto, 종일 종일 vi repeticiones y me sumergí profundamente en la atmósfera. Este juego dejó 믿어 믿어 않을 정도 인 un legado. Ningún juego es , 없다 popular entre SC: BW, y su impacto en 대한민국 정치 에게 fue impensable. Los jugadores profesionales en Corea 아마도 son tan populares como los dramas coreanos 드라마 주연 배우 들 transmitidos en horario estelar. 또한, 한국 에서 프로 게이머 라면 군대 의 특별한 육군 에 입대 할 수 있다.


¡Viva StarCraft!


Referencias


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


All Articles