Ein Bot für Starcraft in Rust, C oder einer anderen Sprache


StarCraft: Brutkrieg . Dieses Spiel bedeutet mir so viel! Und für viele von Ihnen, denke ich. So sehr, dass ich mich frage, ob ich überhaupt einen Link zu seiner Seite auf Wikipedia geben soll oder nicht.


Einmal schickte Halt mir eine PM und bot an, Rust zu lernen. Wie alle gewöhnlichen Menschen haben wir uns entschlossen, damit zu beginnen Hallo Welt Schreiben einer dynamischen Bibliothek für Windows, die in den Adressraum von StarCraft geladen und Einheiten verwaltet werden können.


Der folgende Artikel beschreibt den Prozess des Findens von Lösungen und des Einsatzes von Technologien und Techniken, mit denen Sie neue Dinge über Rust und sein Ökosystem lernen können. Sie können sich auch dazu inspirieren lassen, einen Bot in Ihrer Lieblingssprache zu implementieren, sei es C, C ++, Ruby, Python usw.


Es lohnt sich auf jeden Fall, die Hymne Südkoreas zu hören, während Sie diesen Artikel lesen:


Starcraft OST

Bwapi


Dieses Spiel ist fast 20 Jahre alt. Und es ist immer noch beliebt ; Meisterschaften zogen in den USA sogar 2017 viele Menschen an, wo der Kampf der Großmeister Jaedong gegen Bisu stattfand. Neben menschlichen Spielern nehmen auch seelenlose Maschinen an SC-Schlachten teil! Und das ist wegen BWAPI möglich . Weitere nützliche Links .


Seit über einem Jahrzehnt gibt es eine Community von Bot-Entwicklern rund um dieses Spiel. Enthusiasten erstellen Bots und nehmen an verschiedenen Meisterschaften teil. Viele von ihnen studieren KI und maschinelles Lernen. BWAPI wird von Universitäten verwendet, um ihre Studenten auszubilden. Es gibt sogar einen zuckenden Kanal, der solche Spiele sendet.


Daher hat ein Fan-Team vor einigen Jahren das Starcraft-Backend umgekehrt und eine API in C ++ entwickelt, mit der Sie Bots erstellen, Injektionen in den Spielprozess vornehmen und elende Menschen dominieren können.


Wie so oft vorher Beim Bau eines Hauses ist es notwendig, Erz abzubauen und Werkzeuge zu schmieden Um einen Bot zu erstellen, müssen Sie eine API implementieren. Was hat Rust zu bieten?


Ffi


Es ist ganz einfach, mit anderen Sprachen von Rust zu arbeiten. Dafür gibt es einen FFI . Lassen Sie mich einen kurzen Auszug aus der Dokumentation geben .


Stellen Sie sich vor, wir haben eine bissige Bibliothek mit einer Header-Datei snappy-ch , die Funktionsdeklarationen enthält.


Lassen Sie uns ein Projekt mit Fracht erstellen.


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

Cargo hat eine Standarddateistruktur für das Projekt erstellt.


In Cargo.toml wir die Abhängigkeit von libc an :


 [dependencies] libc = "0.2" 

Die Datei src/main.rs sieht folgendermaßen aus:


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

Lassen Sie uns das Projekt erstellen und ausführen:


 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 

Sie können nur den cargo build aufrufen, der den cargo build vor dem Lauf aufruft. Eine andere Möglichkeit besteht darin, das Projekt zu erstellen und die Binärdatei direkt aufzurufen:


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

Wenn die Snappy-Bibliothek installiert ist, wird der Code kompiliert (für Ubuntu sollten Sie das libsnappy-dev-Paket installieren).


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

Wie Sie sehen können, ist unsere Binärdatei mit der gemeinsam genutzten Bibliothek libsnappy verknüpft. Ein Aufruf von snappy_max_compressed_length in unserem Code ist ein Funktionsaufruf aus dieser Bibliothek.


Rostbinder


Es wäre schön, wenn wir unseren FFI automatisch generieren könnten. Glücklicherweise gibt es in der Toolbox eines Rust-Süchtigen ein Dienstprogramm namens rust-bindgen . Es ist in der Lage, FFI-Bindungen an C-Bibliotheken (und einige C ++) zu generieren.


Installation:


 $ cargo install bindgen 

Wie sieht Rostbinden aus? Wir nehmen die C / C ++ - Headerdateien, zeigen mit dem Dienstprogramm bindgen auf sie und die Ausgabe, die wir erhalten, wird Rust-Code mit den richtigen Deklarationen generiert, damit wir C-Strukturen und -Funktionen verwenden können. bindgen generiert bindgen für bissig:


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

Es stellt sich heraus, dass bindgen nicht mit BWAPI-Headern umgehen kann und Tonnen von nicht verwendbarem Code generiert (aufgrund von Funktionen für virtuelle Mitglieder, std :: string in einer öffentlichen API usw.). Die Sache ist, dass BWAPI in C ++ geschrieben ist. C ++ ist im Allgemeinen selbst in C ++ - Projekten schwierig zu verwenden. Sobald die Bibliothek zusammengestellt ist, ist es besser, sie mit demselben Linker (derselben Version) zu verknüpfen. Header-Dateien sollten mit demselben Compiler (derselben Version) analysiert werden. All diese Faktoren können das Ergebnis beeinflussen. Mangeln zum Beispiel, das immer noch nicht ohne Fehler in GNU GCC implementiert werden kann . Diese Faktoren sind so bedeutend, dass selbst gtest sie nicht überwinden konnte. In der Dokumentation heißt es: Sie sollten gtest besser als Teil des Projekts mit demselben Compiler und demselben Linker erstellen.


Bwapi-c


C ist die Verkehrssprache der Softwareentwicklung. Wenn rust-bindgen für die C-Sprache gut funktioniert, warum nicht BWAPI für C implementieren und dann dessen API verwenden? Gute Idee!


Ja, es ist eine gute Idee, bis Sie sich die Innenseiten von BWAPI ansehen und die Anzahl der Klassen und Methoden sehen, die Sie implementieren sollten. Besonders all diese Speicherlayouts, ASM-Codes, Speicher-Patches und andere "Horrors", für die wir keine Zeit haben. Es ist notwendig, die vorhandene Lösung vollständig zu nutzen.


Aber wir müssen die Funktionen Mangling, C ++ - Code, Vererbung und virtuelle Mitglieder irgendwie übertreffen.


In C ++ gibt es zwei leistungsstarke Tools, mit denen wir unser Problem lösen können: undurchsichtige Zeiger und extern "C" .


extern "C" {} ermöglicht es C ++ - Code, sich unter C zu "maskieren". Es ermöglicht das Generieren von Funktionsnamen ohne Mangeln.


Undurchsichtige Zeiger ermöglichen es uns, den Typ zu löschen und einen Zeiger auf "einen Typ" zu erstellen, ohne dessen Implementierung bereitzustellen. Da dies nur eine Deklaration eines Typs ist, ist es unmöglich, diesen Typ als Wert zu verwenden. Sie können ihn nur als Zeiger verwenden.


Stellen wir uns vor, wir haben diesen C ++ - Code:


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

Wir können daraus einen C-Header machen:


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

Und hier ist der C ++ - Teil, der die Verbindung zwischen dem C-Header und der C ++ - Implementierung darstellt:


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

Nicht alle Klassenmethoden mussten auf diese Weise verarbeitet werden. In BWAPI gibt es Klassen, die Sie mithilfe der Felder dieser Strukturen selbst implementieren können, z. B. typedef struct Position { int x; int y; } Position; typedef struct Position { int x; int y; } Position; und Methoden wie Position::get_distance .


Es gab Klassen, die ich auf besondere Weise behandeln musste. Beispielsweise sollte AIModule ein Zeiger auf eine C ++ - Klasse mit einem bestimmten Satz virtueller Elementfunktionen sein. Trotzdem ist hier Header und Implementierung .


Nach mehreren Monaten harter Arbeit, 554 Methoden und einem Dutzend Klassen wurde die plattformübergreifende Bibliothek BWAPI-C geboren, mit der Sie Bots in C erstellen können. Ein Nebenprodukt war die Möglichkeit der Kreuzkompilierung und die Möglichkeit, die API in einer anderen Sprache zu implementieren, die FFI und die cdecl-Aufrufkonvention unterstützt.


Wenn Sie eine Bibliothek schreiben, schreiben Sie bitte deren API in C.


Das wichtigste Merkmal von BWAPI-C ist die größtmögliche Integration in andere Programmiersprachen. Python , Ruby , Rust , PHP , Java und viele andere können mit C arbeiten. Wenn Sie also die Knicke ausbügeln und eigene Wrapper implementieren, können Sie mit deren Hilfe auch einen Bot schreiben.


Einen Bot in C schreiben


Dieser Teil beschreibt die allgemeinen Prinzipien der internen Organisation von Starcraft-Modulen.


Es gibt zwei Arten von Bots: Modul und Client. Schauen wir uns ein Beispiel für das Schreiben eines Moduls an.


Das Modul ist eine dynamische Bibliothek. Das allgemeine Prinzip des Ladens dynamischer Bibliotheken kann hier eingesehen werden . Das Modul sollte zwei Funktionen exportieren: newAIModule und gameInit .


gameInit ist einfach. Diese Funktion wird aufgerufen, um einen Zeiger auf ein aktuelles Spiel zu übergeben. Dieser Zeiger ist sehr wichtig, da es in der Wildnis von BWAPI eine globale statische Variable gibt, die in einigen Abschnitten des Codes verwendet wird. Beschreiben gameInit :


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

newAIModule ist etwas komplizierter. Es sollte den Zeiger auf eine C ++ - Klasse zurückgeben, die eine virtuelle Methodentabelle mit Namen wie onXXXXX enthält, die bei bestimmten Spielereignissen aufgerufen werden. Deklarieren wir die Struktur des Moduls:


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

Das erste Feld sollte ein Zeiger auf die Methodentabelle sein (es ist eine Art Magie). Hier ist die newAIModule Funktion:


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

createAIModuleWrapper ist ein weiterer Zaubertrick, der den C-Zeiger in den Zeiger auf die C ++ - Klasse verwandelt virtuelle Methoden Mitgliedsfunktionen.


module_vtable ist eine statische Variable in der Methodentabelle. Methodenwerte werden mit Zeigern auf globale Funktionen gefüllt:


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

Wenn Sie sich den Namen der Funktionen und ihre Signaturen ansehen, ist klar, unter welchen Bedingungen und mit welchen Argumenten sie aufgerufen werden müssen. Zum Beispiel habe ich alle Funktionen außer leer gemacht


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

Diese Funktion wird aufgerufen, wenn das Spiel ausgeführt wird. Das Argument ist ein Zeiger auf das aktuelle Modul. BWAPIC_getGame gibt einen globalen Zeiger auf das Spiel zurück, den wir mit einem Aufruf von BWAPIC_setGame . Lassen Sie uns ein funktionierendes Beispiel für das Cross-Kompilieren eines Moduls zeigen:


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

Drücke die Knöpfe und starte das Spiel. Weitere Informationen zum Kompilieren und Ausführen finden Sie auf der BWAPI- Website und in BWAPI-C .


Das Ergebnis des Moduls:


Bild


Ein etwas komplizierteres Beispiel für ein Modul, das zeigt, wie man mit Iteratoren, Einheitenverwaltung, Mineralsuche und Statistikausgabe arbeitet, finden Sie in bwapi-c / example / Dll.c.


bwapi-sys


Im Rust-Ökosystem gibt es eine bestimmte Möglichkeit , Pakete zu benennen, die mit nativen Bibliotheken verknüpft sind. Jedes Paket foo-sys führt zwei wichtige Funktionen aus:


  • Links zur nativen Bibliothek libfoo;
  • stellt Deklarationen für die Funktionen aus der libfoo-Bibliothek bereit. Aber nur Erklärungen! Übergeordnete Abstraktionen werden in * -sys-Kisten nicht bereitgestellt.

Damit das * -sys-Paket erfolgreich verknüpft werden kann, müssen Sie die Fracht anweisen, nach der nativen Bibliothek zu suchen und / oder die Bibliothek aus den Quellen zu erstellen .


Damit das * -sys-Paket Deklarationen bereitstellt, müssen Sie diese entweder selbst schreiben oder mit bindgen generieren. Wieder bindgen. Versuch Nummer zwei =)


Die Erzeugung von Bindungen ist super einfach:


 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 ist eine Datei mit allen C-Headern von BWAPI-C.


Beispielsweise hat bindgen bereits solche Deklarationen für die obigen Funktionen generiert:


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

Es gibt zwei Strategien: Speichern des generierten Codes im Repository und Generieren des Codes im laufenden Betrieb während des Builds. Beide Ansätze haben ihre Vor- und Nachteile .


Freut mich, Sie kennenzulernen bwapi-sys ; Noch ein kleiner Schritt zu unserem Ziel.


Erinnerst du dich daran, dass ich früher über plattformübergreifend gesprochen habe? nlinker schloss sich dem Projekt an und implementierte eine listige Strategie. Wenn der Zielhost Windows ist, laden Sie das bereits zusammengestellte BWAPIC von GitHub herunter. Und für die verbleibenden Ziele sammeln wir BWAPI-C aus den Quellen für OpenBW (ich werde es Ihnen etwas später erzählen).


bwapi-rs


Jetzt haben wir die Bindungen und können Abstraktionen auf hoher Ebene definieren. Wir haben zwei Arten, mit denen wir arbeiten können: reine Werte und undurchsichtige Zeiger.


Alles ist einfach mit reinen Werten. Nehmen wir als Beispiel Farben. Wir müssen die Verwendung von Rust-Code vereinfachen, um Farben auf bequeme und natürliche Weise verwenden zu können:


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

Für eine bequeme Verwendung wäre es daher erforderlich, die Aufzählung mit C ++ - Konstanten, aber auch idiomatisch für Rust, zu definieren und Methoden für die Konvertierung in bwapi_sys :: Color mit std :: convert :: From zu definieren:


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

Für Ihre Bequemlichkeit können Sie die Enum-Primitive-Derive- Kiste verwenden.


Es ist auch einfach, undurchsichtige Zeiger zu verwenden. Verwenden wir das Newtype- Muster:


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

Dies bedeutet, dass Player eine Art Struktur mit einem privaten Feld ist - ein roher undurchsichtiger Zeiger von C. Und so können Sie Player :: color definieren:


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

Jetzt können wir unseren ersten Bot in Rust schreiben!


Erstellen eines Bots in Rust


Als Proof of Concept wird der Bot einem bekannten Land ähneln: Die ganze Aufgabe besteht darin, Arbeiter einzustellen und Mineralien zu sammeln.


Nordkorea


Südkorea


Beginnen wir mit den erforderlichen Funktionen gameInit und 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] führt die gleiche Funktion wie extern "C" in C ++ aus. Innerhalb von wrap_handler gesamte Magie auf, wobei die virtuelle Funktionstabelle ersetzt und die C ++ - Klasse "maskiert" wird.


Die Definitionen der Modulstruktur sind noch einfacher und ausgefallener als in C:


 struct ExampleAIModule { name: String, } 

Fügen wir einige Methoden zum Rendern der Statistiken und zum Erteilen von Befehlen hinzu:


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

Um den ExampleAIModule-Typ in ein echtes Modul umzuwandeln, müssen Sie ihn auf onXXXX- Ereignisse reagieren lassen . Dazu müssen Sie den EventHandler-Typ implementieren, der analog zur virtuellen Tabelle AIModule_vtable von C ist:


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

Das Erstellen und Ausführen des Moduls ist so einfach wie bei 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 ... ... ... 

Und das Video der Arbeit:



Openbw


Diese Jungs gingen noch weiter. Sie beschlossen, eine Open-Source-Version von SC: BW zu schreiben! Und sie sind gut darin. Eines ihrer Ziele war die Implementierung von HD-Bildern, aber SC: Remastered war ihnen voraus = (In diesem Moment können Sie ihre API zum Schreiben von Bots verwenden (ja, auch in C ++), aber das Erstaunlichste ist die Fähigkeit zum Anzeigen Wiedergabe direkt in Ihrem Browser .


Fazit


Bei der Implementierung gab es ein ungelöstes Problem: Wir steuern die Referenzen nicht als eindeutig, sodass das Vorhandensein von &mut und & für dieselbe Region zu einem undefinierten Verhalten führt, wenn das Objekt geändert wird. Eine Art Ärger. Halt versuchte , idiomatische Bindungen zu implementieren, fand jedoch keine Lösung. Wenn Sie diese Aufgabe ausführen möchten, müssen Sie die C ++ - API sorgfältig "hochschaufeln" und die const Qualifizierer korrekt const .


Ich habe es wirklich genossen, an diesem Projekt zu arbeiten, ich habe mir die Wiederholungen angesehen deeply 종일 und tief in die Atmosphäre eingetaucht. Dieses Spiel hat ein Universum in Mitleidenschaft gezogen. Kein Spiel kann aufgrund seiner Beliebtheit bei SC: BW 비교할 수 없다 sein, und seine Auswirkungen auf 대한민국 정치 에게 waren undenkbar. Pro-Gamer in Korea sind ebenso beliebt wie koreanische Dorams, die zur Hauptsendezeit senden. 또한, 한국 에서 프로 게이머 라면 군대 군대 특별한 육군 에 에 입대 할 수 수.


Es lebe StarCraft!






Vielen Dank an Steve Klabnik, der mir bei der Durchsicht des Artikels geholfen hat.

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


All Articles