
ستاركرافت: الحضنة الحرب . هذه اللعبة تعني الكثير بالنسبة لي! وبالنسبة للكثيرين منكم ، أعتقد ذلك. كثيرًا ، أتساءل ما إذا كان يجب عليّ أن أقدم رابطًا لصفحتها على ويكيبيديا أم لا.
مرة واحدة أرسلني هالت وعرضت أن أتعلم الصدأ . مثل أي شخص عادي ، قررنا أن نبدأ مرحبا العالم كتابة مكتبة حيوية لنظام التشغيل Windows يمكن تحميلها في مساحة عناوين StarCraft وإدارة الوحدات.
سوف تصف المقالة التالية عملية البحث عن حلول واستخدام التقنيات والتقنيات التي تتيح لك معرفة أشياء جديدة عن Rust ونظامها البيئي. قد تكون أيضًا مصدر إلهام لتطبيق الروبوت باستخدام لغتك المفضلة ، سواء كانت C أو C ++ أو Ruby أو Python ، إلخ.
بالتأكيد يستحق الاستماع إلى ترنيمة كوريا الجنوبية أثناء قراءة هذا المقال:
بوابي
هذه اللعبة عمرها 20 عامًا تقريبًا. وما زالت شعبية . استقطبت البطولات حشودًا من الناس في الولايات المتحدة الأمريكية حتى في عام 2017 حيث وقعت معركة أسياد جايدونج وبيسو. بالإضافة إلى لاعبين بشريين ، تشارك الآلات التي لا روح لها في معارك SC! وهذا ممكن بسبب BWAPI . المزيد من الروابط المفيدة.
لأكثر من عقد الآن ، كان هناك مجتمع من مطوري الروبوت حول هذه اللعبة. عشاق خلق الروبوتات والمشاركة في مختلف البطولات. الكثير منهم يدرسون الذكاء الاصطناعي والتعلم الآلي. يستخدم BWAPI من قبل الجامعات لتدريب طلابهم. حتى أن هناك قناة نشل تبث مثل هذه المباريات.
لذلك ، قام فريق من المعجبين بعكس اتجاه Starcraft الخلفي قبل عدة سنوات وقام بتطوير واجهة برمجة تطبيقات في C ++ ، والتي تتيح لك إنشاء روبوتات ، وجعل الحقن في عملية اللعبة والسيطرة على البشر البائسين.
كما يحدث في كثير من الأحيان ، من قبل بناء منزل ، فمن الضروري أن الألغام خام ، وصياغة الأدوات إنشاء الروبوت ، تحتاج إلى تطبيق API. ماذا روست لديها لهذا العرض؟
Ffi
الأمر بسيط للغاية للعمل مع لغات أخرى من Rust. هناك FFI لهذا الغرض. اسمحوا لي أن أقدم لكم مقتطفات قصيرة من الوثائق .
تخيل أن لدينا مكتبة سريعة ، تحتوي على ملف رأس snappy-ch ، والذي يحتوي على إعلانات الوظائف.
دعونا إنشاء مشروع باستخدام البضائع .
$ 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
ملف src/main.rs
كما يلي:
extern crate libc;
لنقم ببناء وتشغيل المشروع:
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
في الكود الخاص بنا هو استدعاء دالة من هذه المكتبة.
الصدأ bindgen
سيكون من الرائع أن نتمكن من إنشاء FFI الخاص بنا تلقائيًا. لحسن الحظ ، هناك أداة مساعدة تسمى rust-bindgen في صندوق أدوات مدمن Rust. وهو قادر على إنشاء روابط FFI إلى مكتبات C (وبعض C ++).
التثبيت:
$ cargo install bindgen
ما الذي يبدو عليه الصدأ ؟ نأخذ ملفات رأس C / C ++ ، ونشير الأداة المساعدة bindgen إليها ، ويتم إنشاء الإخراج الذي نحصل عليه كود Rust مع الإعلانات الصحيحة للسماح لنا باستخدام بنيات ووظائف C. إليك ما يولد bindgen
لـ bindgen
:
$ 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 ، مما يولد أطنانًا من التعليمات البرمجية غير القابلة للاستخدام (بسبب وظائف الأعضاء الافتراضية ، std :: string في واجهة برمجة تطبيقات عامة ، إلخ). الشيء هو أن BWAPI هو مكتوب في C ++. من الصعب استخدام C ++ بشكل عام حتى من مشاريع C ++. بمجرد تجميع المكتبة ، من الأفضل ربطها بالرابط نفسه (نفس الإصدار) ، يجب تحليل ملفات الرأس باستخدام نفس المحول البرمجي (نفس الإصدار). كل هذه العوامل قد تؤثر على النتيجة. مانجلينج على سبيل المثال ، والتي لا تزال لا يمكن تنفيذها دون أخطاء في جنو الخليج. هذه العوامل مهمة لدرجة أنه حتى gtest لم يستطع التغلب عليها. وفي الوثائق تقول: من الأفضل بناء gtest كجزء من المشروع بواسطة نفس المترجم ونفس الرابط.
بوابى
C هي اللغة المشتركة لهندسة البرمجيات. إذا كان rust-bindgen يعمل جيدًا للغة C ، فلماذا لا يتم تطبيق BWAPI لـ C ، ثم استخدام API الخاص به؟ فكرة جيدة!
نعم ، إنها فكرة جيدة إلى أن تنظر إلى الدواخل في BWAPI وترى عدد الفئات والطرق التي يجب أن تطبقها. لا سيما جميع هذه تخطيطات الذاكرة ، رموز asm ، تصحيح الذاكرة و "أهوال" أخرى ليس لدينا وقت ل. من الضروري استخدام الحل الحالي بالكامل.
لكننا بحاجة بطريقة ما للتغلب على وظائف mangling ، و C ++ ، والميراث ، والأعضاء الافتراضية.
في C ++ ، هناك أداتان قويتان سنستخدمهما لحل مشكلتنا: مؤشرات مبهمة و extern "C"
.
extern "C" {}
يسمح رمز C ++ "قناع" نفسه تحت C. يجعل من الممكن إنشاء أسماء الوظائف دون mangling.
تسمح لنا المؤشرات غير الشفافة بمسح الكتابة وإنشاء مؤشر إلى "نوع ما" دون توفير تطبيقه. نظرًا لأن هذا مجرد إعلان من نوع ما ، فمن المستحيل استخدام هذا النوع حسب القيمة ، يمكنك استخدامه فقط عن طريق المؤشر.
دعنا نتخيل أن لدينا رمز C ++:
namespace cpp { struct Foo { int bar; virtual int get_bar() { return this->bar; } }; }
يمكننا تحويلها إلى عنوان 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) {
ليس كل أساليب الطبقات كان لا بد من معالجتها بهذه الطريقة. في 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. كان المنتج الثانوي هو إمكانية التجميع التبادلي والقدرة على تنفيذ واجهة برمجة التطبيقات (API) بأي لغة أخرى تدعم FFI واتفاقية استدعاء cdecl.
إذا كنت تكتب مكتبة ، من فضلك ، اكتب واجهة برمجة التطبيقات في C.
أهم ميزة في BWAPI-C هي أوسع تكامل ممكن مع لغات البرمجة الأخرى. Python
، Ruby
، Rust
، PHP
، Java
والعديد من الآخرين قادرون على العمل مع سي ، لذلك إذا تمكنت من التغلب على العقبات وتنفيذ الأغلفة الخاصة ، يمكنك أيضًا كتابة روبوت بمساعدتهم.
كتابة روبوت في C
يصف هذا الجزء المبادئ العامة للتنظيم الداخلي لوحدات Starcraft.
هناك نوعان من السير: الوحدة والعميل. دعونا نلقي نظرة على مثال لكتابة وحدة نمطية.
الوحدة هي مكتبة ديناميكية. يمكن الاطلاع هنا على المبدأ العام لتحميل المكتبات الديناميكية. يجب أن الوحدة النمطية تصدير 2 وظائف: newAIModule
و gameInit
.
gameInit
سهل. تسمى هذه الوظيفة لتمرير مؤشر إلى لعبة الحالية. يعد هذا المؤشر مهمًا جدًا ، نظرًا لوجود متغير ثابت عام في براري BWAPI ، والذي يُستخدم في بعض أقسام الكود. دعنا نصف gameInit
:
DLLEXPORT void gameInit(void* game) { BWAPIC_setGame(game); }
newAIModule
قليلا أكثر تعقيدا. يجب أن تُرجع المؤشر إلى فئة C ++ ، والتي تحتوي على جدول طرق ظاهري به أسماء مثل على XXXXX والتي تسمى في بعض أحداث اللعبة. دعنا نعلن هيكل الوحدة:
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
. لذلك ، دعونا نعرض مثالًا عمليًا على تجميع وحدة نمطية:
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-sys
يوجد في نظام Rust النظام البيئي طريقة معينة لتسمية الحزم التي ترتبط بالمكتبات الأصلية. أي حزمة foo-sys تنفذ وظيفتين مهمتين:
- الروابط مع المكتبة المحلية libfoo ؛
- يوفر إعلانات للوظائف من مكتبة libfoo. لكن التصريحات فقط! لا يتم توفير التجريدات عالية المستوى في صناديق * -sys.
لكي تتمكن الحزمة * -sys من الارتباط بنجاح ، يجب عليك إخبار البضائع للبحث عن المكتبة الأصلية و / أو إنشاء المكتبة من المصادر.
لكي توفر الحزمة * -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
هو ملف يحتوي على كافة رؤوس C من BWAPI-C.
على سبيل المثال ، أنشأ bindgen بالفعل مثل هذه الإعلانات للوظائف المذكورة أعلاه:
extern "C" {
هناك استراتيجيتان: تخزين الكود الذي تم إنشاؤه في المستودع ، وإنشاء الكود على الطاير أثناء الإنشاء. كلا النهجين لهما مزايا وعيوب .
سعيد لمقابلتك bwapi-sys . خطوة واحدة أكثر قليلا لهدفنا.
هل تتذكر أنه في وقت سابق كنت أتحدث عن منصة مشتركة؟ انضم nlinker للمشروع ونفذ استراتيجية الماكرة. إذا كان المضيف الهدف هو Windows ، فقم بتنزيل BWAPIC المجمعة بالفعل من GitHub. وبالنسبة للأهداف المتبقية ، نقوم بتجميع BWAPI-C من مصادر OpenBW (سأخبرك بذلك لاحقًا).
bwapi-rs
الآن لدينا الارتباطات ويمكننا تحديد التجريدات عالية المستوى. لدينا نوعان للعمل مع: القيم الخالصة والمؤشرات غير الشفافة.
كل شيء بسيط مع القيم الخالصة. لنأخذ الألوان كمثال. نحتاج إلى تسهيل استخدامه من كود Rust لاستخدام الألوان بطريقة مريحة وطبيعية:
game.draw_line(CoordinateType::Screen, (10, 20), (30, 40), Color::Red); ^^^
لذلك ، من أجل الاستخدام المريح ، سيكون من الضروري تحديد التعداد باستخدام ثوابت C ++ ولكن أيضًا اصطلاحي لـ Rust ، وتحديد طرق تحويله في bwapi_sys :: Color باستخدام std :: convert :: From:
من أجل راحتك ، يمكنك استخدام صندوق التعداد-البدائي .
كما أنها سهلة الاستخدام مؤشرات مبهمة. لنستخدم نمط Newtype :
pub struct Player(*mut sys::Player);
هذا يعني أن "المشغل" عبارة عن نوع من الهياكل مع حقل خاص - مؤشر خام غير شفاف من C. وهنا يمكنك تعريف "المشغل :: اللون:
impl Player {
الآن يمكننا أن نكتب أول بوت لدينا في Rust!
خلق روبوت في الصدأ
كدليل على المفهوم ، سيكون الروبوت مشابهًا للعداد المعروف: المهمة بأكملها هي توظيف العمال وجمع المعادن.


لنبدأ بالوظائف المطلوبة 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
كل السحر داخل 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)) {
لتحويل نوع 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 ... ... ...
وفيديو العمل:
Openbw
ذهب هؤلاء الرجال إلى أبعد من ذلك. قرروا كتابة نسخة مفتوحة المصدر من SC: BW! وهم جيدون في ذلك. كان أحد أهدافهم هو تطبيق صور عالية الدقة ، ولكن SC: Remastered كانت أمامهم = (في هذه اللحظة ، يمكنك استخدام واجهة برمجة التطبيقات الخاصة بهم لكتابة الروبوتات (نعم ، أيضًا في C ++) ، ولكن الميزة الأكثر إثارة هي القدرة على عرض الاعادة مباشرة في متصفحك .
الخاتمة
كانت هناك مشكلة لم يتم حلها في التنفيذ: نحن لا نتحكم في المراجع لتكون فريدة من نوعها ، وبالتالي فإن وجود &mut
و &
في نفس المنطقة سيؤدي إلى سلوك غير محدد عند تعديل الكائن. نوع من المتاعب. حاول هالت تنفيذ الارتباطات الاصطلاحية ، لكنه لم ينجح في إيجاد حل. وأيضًا إذا كنت ترغب في إكمال هذه المهمة ، يجب عليك "تفريغ" واجهة برمجة تطبيقات C ++ بعناية ووضع تصفيات const
بشكل صحيح.
لقد استمتعت حقًا بالعمل في هذا المشروع ، وشاهدت عمليات إعادة التوزيع وانغمست بشدة في الجو. حققت هذه اللعبة ent دنت في الكون. لا يمكن أن تحظى أي لعبة بشعبية مع SC: BW ، وكان تأثيرها على un 정치 에게 غير وارد. يحظى اللاعبون المؤيدون للاعبين في كوريا بشعبية كبيرة مثل البث الدرامي الكوري في أوقات الذروة. 또한، 한국 에서 프로 게이머 라면 군대 의 특별한 육군 에 입대 입대 수 수 수.
تحيا ستار كرافت!
الروابط
شكرا جزيلا لستيف كلابنيك لمساعدتي في مراجعة المقال.