StarCraft: Brood War. Как много это значит для меня. И для многих из вас. Настолько много, что я засомневался, давать ли ссылку на вики.
Как-то раз мне в личку постучался Halt и предложил выучить Rust. Как и любые нормальные люди, мы решили начать с hello world написания динамической библиотеки под Windows, которая могла бы загружаться в адресное пространство игры StarCraft и управлять юнитами.
В статье будет описан процесс поиска решений, использования технологий, приемов, которые позволят вам почерпнуть новое в языке Rust и его экосистеме или вдохновиться для реализации бота на своем любимом языке, будь то C, C++, ruby, python, e.t.c.
Эту статью стоит читать непременно под гимн Южной Кореи:
BWAPI
Этой игре уже 20 лет. И она до сих пор популярна, чемпионаты собирают целые залы людей в США даже в 2017 году, где состоялась битва грандмастеров Jaedong vs Bisu. Помимо живых игроков, в битвах участвуют и бездушные машины! И это возможно благодаря BWAPI. Больше полезных ссылок.
Уже более десяти лет вокруг этой игры существует сообщество разработчиков ботов. Энтузиасты пишут ботов и участвуют в различных чемпионатах. Многие из них изучают ИИ и машинное обучение. BWAPI используется университетами для обучения своих студентов. Существует даже твитч канал, который транслирует игры.
Итак, команда фанатов несколько лет назад отреверсила внутреннести Starcraft и написала на C++ API, которое позволяет писать ботов, внедряться в процесс игры и господствовать над жалкими людишками.
Как это часто бывает, прежде чем построить дом, надо добыть руду, выковать инструменты... написать бота, необходимо реализовать API. Что же может предложить со своей стороны Rust?
FFI
Взаимодействовать с другими языками из Rust довольно просто. Для этого существует FFI. Позвольте мне предоставить краткую выдержку из документации.
Пусть у нас есть библиотека snappy, у которой есть заголовочный файл snappy-c.h, из которого мы будем копировать объявления функций.
Создадим проект с помощью cargo.
$ cargo new --bin snappy
Created binary (application) `snappy` project
$ cd snappy
snappy$ tree
.
+-- Cargo.toml
L-- src
L-- main.rs
1 directory, 2 files
Cargo создал стандартную файловую структуру для проекта.
В Cargo.toml
укажем зависимость к libc:
[dependencies]
libc = "0.2"
src/main.rs
файл будет выглядеть так:
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);
}
Собираем и запускаем:
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
является вызовом функции из этой библиотеки.
rust-bindgen
Было бы хорошо, если бы мы могли в автоматическом режиме сгенерировать FFI. К счастью, в арсенале растоманов есть такая утилита под названием rust-bindgen. Она умеет генерировать FFI байндинги к C (и некоторым C++) библиотекам.
Установка:
$ cargo install bindgen
Как выглядит использование rust-bindgen? Мы берем заголовочные файлы C/C++, натравливаем на них утилиту bindgen, на выходе получаем сгенерированный Rust код с определениями сишных структур и функций. Вот как выглядит генерация FFI для snappy:
$ bindgen /usr/include/snappy-c.h | grep -C 1 snappy_max_compressed_length
extern "C" {
pub fn snappy_max_compressed_length(source_length: usize) -> usize;
}
Оказалось, что bindgen пасует перед заголовками BWAPI, генерируя тонны неюзабельных простынок кода (из-за виртуальных функций-членов, std::string в публичном API и т.д.). Все дело в том, что BWAPI написан на C++. C++ вообще сложно использовать даже из C++ проектов. Однажды собранную библиотеку лучше влинковывать тем же линковщиком (одинаковых версий), заголовочные файлы лучше парсить тем же компилятором (одинаковых версий). Потому что существует множество факторов, которые могут повлиять на исход. Например, манглинг, который в GNU GCC до сих пор не могут реализовать без ошибок. Эти факторы настолько значимы, что их не смогли побороть даже в gtest, а в документации указали, что лучше бы вам собирать gtest как часть проекта тем же компилятором и тем же линковщиком.
BWAPI-C
C — это лингва франка программирования. Если rust-bindgen хорошо работает для языка C, почему бы не реализовать BWAPI для C, а потом использовать его API? Хорошая идея!
Да, хорошая идея, пока ты не заглянул в кишки BWAPI и не увидел кол-во классов и методов, которые необходимо реализовать =( Особенно все эти расположения структур в памяти, ассемблеры, патчинг памяти и прочие ужасы, на которые у нас нет времени. Надо по максимуму заиспользовать уже существующее решение.
Но надо как-то бороться с манглингом, C++ кодом, наследованиями и виртуальными функциями-членами.
В 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; // Непрозрачный указатель на Foo
// объявляем cpp::Foo::get_bar
int Foo_get_bar(Foo* self);
}
И C++ часть, которая будет связующим звеном между C заголовком и C++ реализацией:
int Foo_get_bar(Foo* self) {
// кастуем непрозрачный указатель к конкретному cpp::Foo и вызываем метод ::get_bar
return reinterpret_cast<cpp::Foo*>(self)->get_bar();
}
Не все методы классов пришлось обрабатывать таким образом. В BWAPI есть классы, операции над которыми можно реализовать самому, используя значения полей этих структур, например typedef struct Position { int x; int y; } Position;
и методы вроде Position::get_distance
.
Были и те, над которыми пришлось постараться особенным образом. Например, AIModule должен быть указателем на C++ класс с определенным набором виртуальных функций-членов. Тем не менее, вот заголовок и реализация.
Итак, спустя несколько месяцев кропотливой работы, 554 метода и десяток классов, на свет родилась кроссплатформенная библиотека BWAPI-C, которая позволяет писать ботов на C. Побочным продуктом стала возможность кросскомпиляции и возможность реализовать API на любом другом языке, который поддерживает FFI и соглашение о вызове cdecl.
Если вы пишите библиотеку, пожалуйста, пишите API на C.
Самая главная фишка BWAPI-C — это широчайшая возможность интеграции с другими языками. Python
, Ruby
, Rust
, PHP
, Java
и многие многие другие умеют работать с C, следовательно на них тоже можно написать бота, если чуть-чуть поработать напильником и реализовать свои обертки.
Пишем бота на C
Эта часть описывает общие принципы устройства модулей Starcraft.
Существуют 2 типа ботов: модуль и клиент. Мы рассмотрим пример написания модуля.
Модуль — это загружаемая библиотека, общий принцип загрузки можно посмотреть здесь. Модуль должен экспортировать 2 функции: newAIModule
и gameInit
.
С 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
— это еще одна магия, которая превращает С указатель в указатель на С++ класс с виртуальными методами функциями-членами.
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
. Итак, покажем пример кросскомпиляции и работы модуля:
bwapi-c/example$ tree
.
+-- BWAPIC.dll
L-- 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-sys
В экосистеме Раста принято определенным образом называть пакеты, которые линкуются к нативным библиотекам. Любой пакет foo-sys занимается двумя важными функциями:
- Линкуется к нативной библиотеке libfoo
- Предоставляет объявления к функциям из библиотеки libfoo. Но только лишь объявления, высокоуровневые абстракции в *-sys крейтах не предоставляются.
Чтобы *-sys пакет умел успешно линковаться, в него встраивают поиск нативной библиотеки и/или сборку библиотеки из исходников.
Чтобы *-sys пакет предоставил объявления, надо или написать их руками, или сгенерировать с помощью bindgen. Снова bindgen. Попытка номер два =)
Генерация байндингов с помощью bwapi-c становится до неприличия простой:
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.
Например, для уже известных функций 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;
}
Существуют 2 стратегии: хранение сгенерированного кода в репозитории и генерация кода на лету при сборке. И тот и другой подход имеет свои преимущества и недостатки.
Приветствуем bwapi-sys, еще одну маленькую ступень к нашей цели.
Помните, я говорил про кроссплатформенность? К проекту присоединился nlinker и реализовал хитрую стратегию. Если целевой таргет — Windows, то скачиваем уже собранную BWAPIC из гитхаба. А для остальных таргетов собираем BWAPI-C из исходников для OpenBW (расскажу чуть позже).
bwapi-rs
Теперь, когда у нас есть байндинги, мы можем описывать высокоуровневые абстракции. У нас есть 2 типа, с которыми надо работать: чистые значения и непрозрачные указатели.
С чистыми значениями все проще. Возьмем за пример цвета. Нам надо добиться удобного использования из Rust кода, чтобы можно было использовать цвета удобным и естественным образом:
game.draw_line(CoordinateType::Screen, (10, 20), (30, 40), Color::Red);
^^^
Значит для удобного использования надо будет определить идиоматическое для языка Rust перечисление с константами из C++ и определить методы конвертирования в bwapi_sys::Color с помощью типажа 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,
...
Хотя для удобства можно воспользоваться крейтом enum-primitive-derive.
С непрозрачными указателями ничуть не сложнее. Для этого воспользуемся паттерном Newtype:
pub struct Player(*mut sys::Player);
То есть Player — это некая структура с приватным полем — сырым непрозрачным указателем из C. И вот как можно описать метод 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
}
}
Теперь мы можем написать своего первого бота на Rust!
Пишем бота на Rust
В качестве proof of concept бот будет похож на одну известную страну: весь его функционал будет заключаться в найме рабочих и сборе минералов.
Начнем с обязательных функций gameInit
и 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]
выполняет ту же функцию, что и extern "C"
в C++. Внутри 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, который является аналогом виртуальной таблицы AIModule_vtable из 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) {}
}
Сборка и запуск модуля так же просты, как и для 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
...
...
...
И видео работы:
Немного о кросскомпиляции
Если кратко, то в Rust она прекрасна! В два клика можно поставить множество тулчейнов для разных платформ. Конкретно тулчейн i686-pc-windows-gnu ставится командой:
rustup target add i686-pc-windows-gnu
Так же можно указать кофиг для cargo в корне проекта .cargo/config
:
[target.i686-pc-windows-gnu]
linker = "i686-w64-mingw32-gcc"
ar = "i686-w64-mingw32-ar"
runner = "wine"
И это все, что нужно сделать, чтобы скомпилировать проект Rust из Linux под Windows.
OpenBW
Эти ребята пошли еще дальше. Они решили написать open-source версию игры SC:BW! И у них неплохо получается. Одной из их целей была реализация HD картинки, но SC: Remastered их опередили =( На данный момент можно использовать их API для написания ботов (да, тоже на C++). Но самой умопомрачительной фичей является возможность просматривать реплеи прямо в браузере.
Заключение
При реализации осталась нерешенная проблема: мы не контролируем уникальность ссылок, а одновременное существование &mut
и &
при изменении объекта приведет к неопределенному поведению. Беда. Halt пытался реализовать идиоматичные байндинги, но его запал слегка угас. Также для решения этой задачи придется качественно перелопатить C++ API и правильно проставить const
квалификаторы.
Мне очень понравилось работать над этим проектом, я ???? смотрел реплеи и глубоко погрузился в атмосферу. Эта игра оставила ???? ?? ??? наследие. Ни одну игру нельзя ??? ? ?? по популярности с SC:BW, а ее влияние на ???? ???? оказалось немыслимым. Прогеймеры в Корее ??? так же популярны, как и ??? ?? ??? корейских дорам, транслирующихся в прайм-тайм. ??, ???? ??????? ??? ??? ??? ??? ? ??.
Да здравствует StarCraft!
Ссылки
Комментарии (19)
nlinker
21.07.2018 22:29+1Здoрово написано!
К сожалению да, авторы BWAPI написали его на C++ и это доставило массу головной боли всем, кто не на MSVC++ и Windows. Хотя их можно понять, когда они начинали (а это было аж в 2009м), решиться писать на C было сложно, поскольку сам проект был связан с реверс инжинирингом бинарников Starcraft, а он написан на C++. Вдобавок, они считали, что их разработка будет продуктивнее, если они будут делать на C++.
Но теперь, когда так много различных платформ, с которыми можно интегрироваться (Rust, Go, Node, JVM, Python, .Net) выбор C++ кажется архитектурным просчётом.
Но повторюсь, ребята проделали огромную работу, и они большие молодцы, пусть уж лучше законченная библиотека на C++, чем гипотетическая на C.0xf0a00
21.07.2018 22:56+2Но теперь, когда так много различных платформ, с которыми можно интегрироваться (Rust, Go, Node, JVM, Python, .Net) выбор C++ кажется архитектурным просчётом.
А вы считаете что лучше было бы на С?
xomachine
21.07.2018 22:48Отличная статья! Я бы даже сказал: о наболевшем, а именно о мучениях с ABI C++.
Однако, я не смог понять, как Вам удалось справиться с соглашениями о вызовах. Если речь идёт о 32-битном коде, то просто объявления статических функций и заполнения ими таблицы виртуальных методов недостаточно, поскольку виртуальные методы используют соглашение о вызовах
thiscall
, тогда как функции обычно используютstdcall
.
С 64-битным кодом всё несколько проще. Насколько я понял, там спасает ключ компилятора
-mabi=ms
. Тем не менее, мне очень интересно, что за магия происходит вwrap_handler
и как Rust понимает, что нужно использовать соглашения о вызовах MSVC вместо System V без-mabi=ms
.humbug Автор
22.07.2018 11:45+3Дело в том, что я избавился от виртуальных функций членов в публичном API с помощью opaque pointers. BWAPI-C скрывает C++ ABI внутри себя и правильно выбирает соглашение о вызовах благодаря нативному компилятору, а наружу торчит обычный C.
Например, для Windows BWAPI-C собирается cl.exe & link.exe, на выходе получаем 2 библиотеки — BWAPIC.lib и BWAPIC.dll (в CMake за это отвечает флаг CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS). GNU toolchain умеет работать с .lib, а динамическая линковка с .dll уже происходит во время запуска игры. Таким образом у нас нет головной боли с соглашением о вызовах.
Ну а с 64-битным кодом всё еще проще, его просто нет. Игра писалась 20 лет назад =)
nlinker
22.07.2018 20:06+1> Ну а с 64-битным кодом всё еще проще, его просто нет. Игра писалась 20 лет назад =)
Чуть подкорректирую: оригинальный Старкрафт, он да, 32-битный, но OpenBW может в x64.
Логику ботов вполне можно писать под OpenBW, а потом компилировать в Win32 таргет, запустить под оригинальным Старкрафтом, убрать шероховатости, если есть.
Это неплохой вариант, если кому-то некомфортно под виндой (например, мне :-))
humbug Автор
22.07.2018 11:50+3Чтобы понять, что происходит в
wrap_handler
надо понять, что происходит в сишной оберткеcreateAIModuleWrapper
: https://github.com/RnDome/bwapi-c/blob/master/src/AIModule.cpp. Я обернул C структуру в C++ класс, вернув сырой указатель. Чтобы создать Rust модуль, надо его обернуть в C, чтобы потом обернуть в C++, читайте https://github.com/RnDome/bwapi-rs/blob/master/src/aimodule.rs .
Если будут вопросы, я отвечу подробнее.
bro-dev0
22.07.2018 11:41Когда первый раз увидел это офигел, до сих пор самый сильный ИИ который я видел в играх. Но даже средний киберспортсмен в рамках 1 игры успевает понять как обыграть его.
some_name
22.07.2018 16:58А это вообще
легальнонасколько возможно — вторгаться в адресное пространство другой программы? Я как-то читал, что все программы исполняются в своей «песочнице», и менеджер памяти контроллирует, чтобы другой процесс не лез куда его не просят.Halt
22.07.2018 17:05+1Динамическая библиотека загружается в адресное пространство процесса, соответственно она имеет к нему доступ. То что вы пишете относится к виртуальной памяти и изоляции процессов.
Halt