适用于Rust,C或任何其他语言的Starcraft机器人


《星际争霸:巢穴之战》 。 这个游戏对我来说意义非凡! 我想对你们中的许多人来说。 如此之多,我想知道是否应该在Wikipedia上提供其页面的链接。


一次霍尔特给我发了PM并提议学习Rust 。 像任何普通人一样,我们决定从 你好世界 为Windows编写一个动态库,该库可以加载到StarCraft的地址空间中并管理单元。


下面的文章将描述寻找解决方案以及使用允许您学习Rust及其生态系统新事物的技术的过程。 可能还会启发您使用自己喜欢的语言(无论是C,C ++,Ruby,Python等)来实现机器人。


阅读本文时,绝对值得听听韩国的赞美诗:


星际争霸OST

布瓦皮


这场比赛已经有20年历史了。 而且仍然很受欢迎 ; 甚至在2017年 ,冠军争夺战在美国也引起了人们的注意, 那里大师级的Jaedong与Bisu的战斗发生了。 除了人类玩家之外, 毫无灵魂的机器还参加了SC战斗! 由于BWAPI ,这是可能的。 更有用的链接


十多年来,围绕这个游戏的机器人开发者社区已经出现。 热心者创造机器人并参加各种锦标赛。 他们中的许多人都学习AI和机器学习。 大学使用BWAPI训练学生。 甚至还有一个抽搐频道播放此类比赛。


因此,几年前,一个粉丝团队扭转了星际争霸的后端,并开发了C ++ API,该API可让您创建机器人,在游戏过程中进行注入并控制可悲的人类。


经常发生 盖房子,有必要开采矿石,锻造工具 创建一个机器人,您需要实现一个API。 Rust必须提供什么?


联邦调查局


使用Rust中的其他语言非常简单。 为此有一个FFI 。 让我从文档中摘录一下


想象一下,我们有一个快照库,其中包含一个头文件snappy-ch ,其中包含函数声明。


让我们使用cargo创建一个项目。


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

Cargo为该项目创建了标准文件结构。


Cargo.toml我们指定对libc的依赖关系:


 [dependencies] libc = "0.2" 

src/main.rs文件将如下所示:


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

让我们构建并运行项目:


 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 

您只能叫cargo run ,这要在运行前叫cargo build 。 另一种选择是构建项目并直接调用二进制文件:


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

如果已安装snappy库,则将编译代码(对于Ubuntu,应安装libsnappy-dev软件包)。


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

如您所见,我们的二进制文件链接到libsnappy共享库。 在我们的代码中对snappy_max_compressed_length的调用是从该库进行的函数调用。


锈结合


如果我们能够自动生成FFI,那将是很好的。 幸运的是,Rust瘾君子的工具箱中有一个名为rust-bindgen的实用程序。 它能够生成与C(和某些C ++)库的FFI绑定。


安装方式:


 $ cargo install bindgen 

锈菌结合素看起来像什么? 我们使用C / C ++头文件,将bindgen实用程序指向它们,然后将获得的输出生成带有适当声明的Rust代码,以使我们使用C结构和函数。 这是bindgenbindgen生成的:


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

事实证明,bindgen无法应付BWAPI标头,生成大量不可用的代码(由于虚拟成员函数,公共API中的std ::字符串等)。 事实是BWAPI用C ++编写。 即使在C ++项目中,C ++通常也很难使用。 库组装完成后,最好使用相同的链接器(相同版本)链接它,头文件应使用相同的编译器(相同版本)进行解析。 所有这些因素都可能影响结果。 例如, 重整 ,仍然无法在GNU GCC中没有错误地实现 。 这些因素非常重要,甚至gtest也无法克服。 并且在文档中说:您最好使用相同的编译器和相同的链接器将gtest构建为项目的一部分。


布瓦比克


C是软件工程的通用语。 如果rust-bindgen适用于C语言,为什么不为C实现BWAPI,然后使用其API? 好主意!


是的,这是一个好主意,直​​到您深入研究BWAPI的内部并看到应该实现的类和方法的数量。 特别是所有这些我们没有时间进行的内存布局,asm代码,内存修补和其他“恐怖”。 有必要完全使用现有解决方案。


但是,我们需要以某种方式击败混乱,C ++代码,继承和虚拟成员函数。


在C ++中,有两个功能强大的工具extern "C"来解决问题: 不透明的指针extern "C"


extern "C" {}允许C ++代码在C语言下“屏蔽”自身。它可以生成函数名称而无需处理。


不透明的指针使我们可以清除类型并创建指向“某种类型”的指针,而无需提供其实现。 由于这只是某种类型的声明,因此无法通过值使用此类型,只能通过指针使用它。


假设我们有以下C ++代码:


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

我们可以将其转换为C标头:


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

这是C ++部分,它将成为C标头和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(); } 

并非所有的类方法都必须以这种方式处理。 在BWAPI中,可以使用这些结构的字段来实现自己的类,例如typedef struct Position { int x; int y; } Position; typedef struct Position { int x; int y; } Position; 以及Position::get_distance类的方法。


有些课程我必须以特殊的方式对待。 例如,AIModule应该是指向具有特定虚拟成员函数集的C ++类的指针。 不过,这是标题实现


因此,经过几个月的努力, 554种方法和十几个类,跨平台库BWAPI-C诞生了,它使您可以用C创建机器人。 副产品是交叉编译的可能性以及以支持FFI和cdecl调用约定的任何其他语言实现API的能力。


如果要编写库,请使用C编写其API。


BWAPI-C的最重要功能是与其他编程语言的尽可能广泛的集成。 PythonRubyRustPHPJava和许多其他功能都可以使用C,因此,如果您解决了一些难题并实现了自己的包装器,则还可以在它们的帮助下编写一个机器人。


用C编写机器人


本部分描述了Starcraft模块内部组织的一般原理。


机器人有2种类型:模块和客户端。 让我们看一个编写模块的例子。


该模块是一个动态库。 加载动态库的一般原理可以在这里查看。 该模块应导出2个函数: newAIModulegameInit


gameInit很容易。 调用此函数可将指针传递给当前游戏。 该指针非常重要,因为在代码的某些部分中使用了BWAPI的通配符,其中包含全局静态变量。 让我们描述gameInit


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

newAIModule有点复杂。 它应该返回指向C ++类的指针,该类具有一个虚拟方法表,其名称类似于onXXXXX ,在某些游戏事件中会被调用。 让我们声明模块的结构:


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

第一个字段应该是指向方法表的指针(这是一种魔术)。 这是newAIModule函数:


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

createAIModuleWrapper是另一个魔术,可以将C指针转换为指向C ++类的指针, 虚方法 成员函数。


module_vtable是方法表上的静态变量,方法值填充有指向全局函数的指针:


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

如果查看函数的名称及其签名,则很清楚在什么条件下以及需要使用哪些参数调用它们。 例如,我将所有功能都清空了,除了


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

游戏运行时调用此函数。 该参数是指向当前模块的指针。 BWAPIC_getGame返回指向游戏的全局指针,我们使用对BWAPIC_setGame的调用来设置该BWAPIC_setGame 。 因此,让我们展示一个交叉编译模块的工作示例:


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

按下按钮并运行游戏。 可以在BWAPI网站和BWAPI-C中找到有关编译和执行的更多信息。


模块的结果:


图片


您可以在bwapi-c / example / Dll.c中找到一个更复杂的模块示例,其中显示了如何使用迭代器,单元管理,矿物搜索和统计信息输出。


bwapi系统


在Rust生态系统中,有一种命名程序包的方式可以链接到本机库。 任何软件包foo-sys都执行两个重要功能:


  • 与本地库libfoo的链接;
  • 提供libfoo库中函数的声明。 但是只有声明! * -sys包装箱中未提供高级抽象。

为了使* -sys软件包能够成功链接,您必须告诉cargo 搜索本机库和/或从源代码构建库。


为了使* -sys包提供声明,您需要自己编写它们或使用bindgen生成它们。 再次bindgen。 尝试第二个=)


绑定的生成非常简单:


 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是具有BWAPI-C中所有C头的文件。


例如,bindgen已经为上面的函数生成了这样的声明:


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

有两种策略:将生成的代码存储在存储库中,以及在构建过程中即时生成代码。 两种方法都有其优点和缺点


很高兴认识您bwapi-sys ; 朝着我们的目标又迈出了一步。


您还记得我之前谈论过跨平台吗? nlinker加入了该项目并实施了一项狡猾的策略。 如果目标主机是Windows,则从GitHub下载已经组装好的BWAPIC。 对于其余的目标,我们从OpenBW的来源中收集BWAPI-C(稍后再介绍)。


布瓦皮尔


现在我们有了绑定,我们可以定义高级抽象了。 我们有两种类型可以使用:纯值和不透明指针。


一切都很简单,只有纯价值。 让我们以颜色为例。 我们需要使Rust代码易于使用,以便以方便自然的方式使用颜色:


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

因此,为了方便使用,有必要使用C ++ 常量定义枚举,还应使用Rust惯用的枚举,并定义使用std :: convert :: From在bwapi_sys :: Color中对其进行转换的方法:


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

为了您的方便,您可以使用枚举基元派生的板条箱。


使用不透明指针也很容易。 让我们使用Newtype模式:


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

这意味着Player是一种带有私有字段的结构-C语言的原始不透明指针。这是定义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 } } 

现在我们可以用Rust编写我们的第一个机器人了!


在Rust中创建一个机器人


作为概念验证,该机器人将类似于一个知名的countru:整个任务是雇用工人并收集矿物质。


北朝鲜


韩国


让我们从所需的功能gameInitnewAIModule


 #[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]与C ++中的extern "C"执行相同的功能。 在wrap_handler内部, wrap_handler所有魔术,其中包括虚拟函数表的替换和“掩盖” C ++类。


模块结构的定义比C语言更简单,更奇特:


 struct ExampleAIModule { name: String, } 

让我们添加一些方法来呈现统计信息和发出订单:


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

要将ExampleAIModule类型转换为实际模块,您需要使其对onXXXX事件做出响应。 为此,您需要实现EventHandler类型,该类型类似于C语言中的AIModule_vtable虚拟表:


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

生成和运行模块就像使用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 ... ... ... 

和作品的视频:



Openbw


这些家伙走得更远。 他们决定编写SC的开源版本:BW! 他们很擅长。 他们的目标之一是实现HD图像,但SC:Remastered领先于他们=(目前,您可以使用其API编写机器人程序(是的,在C ++中也是如此),但是最令人惊奇的功能是能够查看直接在浏览器中回放。


结论


该实现存在一个未解决的问题:我们不控制引用的唯一性,因此在修改对象时, &mut&到同一区域的存在将导致未定义的行为。 有点麻烦。 Halt 尝试实现惯用的绑定,但是他没有设法找到解决方案。 另外,如果您想完成此任务,则必须仔细“铲起” C ++ API并正确放置const限定符。


我真的很享受这个项目的工作,观看了重播종일,并沉浸在氛围中。 该游戏在宇宙中产生了凹痕。 在SC:BW中,没有一款游戏能像现在这样流行,它对대한민국정치的影响是不可想象的。 韩国的职业玩家与黄金时段的韩国多拉姆广播一样受欢迎。 또한,프로프이수수수수수수수수


星际争霸万岁!






非常感谢Steve Klabnik帮助我审阅了这篇文章。

Source: https://habr.com/ru/post/zh-CN436254/


All Articles