W3P-Protokoll
Der komplette Vertrag zwischen Runtime und externen Programmen: acht Shared-Memory-Blöcke, Weltzustand lesen, Events lesen, Befehle erteilen, Quittungen, Spur-Rollen, Canvas sowie Oberfläche & Eingabe. Lies diese Seite, wenn du aus einer anderen Sprache als Python andockst.
Runtime und externe Programme tauschen Daten ausschließlich über Shared Memory aus. Das Folgende ist alles, was es dazu gibt.
- Die Referenzimplementierung ist in Python:
sdk/python/w3world.py(Lesen) undsdk/python/w3fast.py(Schreiben); Größe und Offsets jeder Struktur sind dort definiert und durch Tests festgenagelt; - Das Protokoll beschreibt nur Semantik und ist unabhängig von der Spielversion. Wechselt die Spielversion, passt sich die Runtime selbst an, das Protokoll bleibt gleich; neue Felder werden nur ans Blockende angehängt, alte Clients funktionieren weiter.
Die meisten brauchen diese Seite nicht – nimm einfach das Python-SDK. Du brauchst sie nur, wenn du direkt aus C++ / C# / Rust / Go oder einer anderen Sprache andocken willst oder wissen möchtest, was unter dem SDK passiert.
1. Acht Shared-Memory-Blöcke
<pid> ist die Prozess-ID des Spiels.
| Name | Richtung | Inhalt | Synchronisation |
|---|---|---|---|
Local\War3World_<pid> | Runtime → du | Weltzustand: Header + 16 Spieler + bis zu 1024 Einheiten + 256 Einheitendetails + 256 Gegenstände am Boden + Erweiterungsbereich + Produktionstabelle | seqlock |
Local\War3Trees_<pid> | Runtime → du | Bis zu 4096 zerstörbare Objekte (Bäume usw.), alle 2 Sekunden aktualisiert | seqlock |
Local\War3Events_<pid> | Runtime → du | Event-Ring, 8192 Einträge | jeder Eintrag trägt eine eigene Sequenznummer |
Local\War3Map_<pid> | Runtime → du | Karte: Geländezellen (128 pro Zelle, bis zu 256×256) + Grenzen des spielbaren Bereichs + Startpositionen; wird in den ersten Sekunden der Partie in Etappen berechnet | seqlock (ändert sich nach der Berechnung nicht mehr) |
Local\War3Fast_<pid> | beide Richtungen | Befehlsspuren: 16 Spuren × 16 Slots; jeder Slot enthält einen Befehl + Quittung; jede Spur hat eine Rolle | pro Slot ein Schreiber, ein Leser |
Local\War3Canvas_<pid> | du → Runtime | Canvas: Header 64 Bytes + 256 Elemente × 112 Bytes + 64 KB Text- / Punkt-Pool; wird erst nach einem canvas_enable angelegt | seqlock (du schreibst, die Runtime liest in jedem Frame) |
Local\War3Msgs_<pid> | Runtime → du | Ring der Bildschirmnachrichten: voller Text von Spielhinweisen, Chat und Systemnachrichten, 128 Einträge × 256 Bytes | jeder Eintrag trägt eine eigene Sequenznummer |
Local\War3Input_<pid> | beide Richtungen | Oberfläche & Eingabe: Die Runtime schreibt Mausposition, den Bodenpunkt unter dem Mauszeiger und das Element unter dem Mauszeiger zurück; du schreibst die Hotkey-Tabelle und die Maus-Schalter; die Runtime übernimmt die Eingabe erst nach einem input_enable | Hotkey-Tabelle per seqlock |
Mehrere Clients nutzen Canvas und Eingabe gleichzeitig: Beide Blöcke gibt es nur einmal; schreibt jeder für sich, überschreiben sie sich gegenseitig. Es gelten diese Konventionen, auch für deinen eigenen Client:
- Canvas: Den benannten Mutex
Local\War3CanvasMutex_<pid>halten und lesen – ändern – schreiben: nur die eigenen Elemente ersetzen, die der anderen unverändert lassen (Pool-Offsets neu anordnen); Elemente, deren Besitzerprozess beendet ist oder die keinen Besitzer haben, entfernen. Im Element istreserved[1]= Prozess-ID des Besitzers,reserved[2]= laufende Nummer im Prozess; Elementnummern vergibt der Zähler bei Offset 60 im Blockheader (ab0x10000). - Eingabe: Jeder Client trägt seine Hotkeys und Maus-Schalter in
Local\War3InputClients_<pid>ein (Header 16 Bytes + 16 Clients × 528 Bytes), aktualisiert unterLocal\War3InputMutex_<pid>seinen eigenen Eintrag und schreibt dann die lebenden Clients zusammengeführt in den Eingabeblock: Hotkeys werden nach „Tastencode + Modifikatortasten“ dedupliziert, Maus-Schalter vereinigt. Events gehen an alle Clients, jeder erkennt seine Hotkeys an „Tastencode + Modifikatortasten“. Solange im Register noch andere lebende Clients stehen, keininput_enable 0senden. - Runtime: Klickbare Elemente, deren Besitzerprozess beendet ist, fangen keine Klicks mehr ab; alle 2 Sekunden prüft die Runtime das Register, und haben sich alle eingetragenen Clients beendet, setzt sie Hotkey-Tabelle und Maus-Schalter im Eingabeblock auf null.
2. Weltzustand lesen (seqlock)
loop:
s1 = block.seq (Offset 8, int32)
if s1 ungerade: erneut (Runtime schreibt gerade)
kopiere Header + players + units[unitCount] + details[detailCount] + items[itemCount]
if block.seq != s1: erneut
- Header: Veröffentlichungszähler (steigt er nicht, ist die Veröffentlichung abgerissen), Spieluhr der Engine, eine Epoche, die pro Partie um 1 steigt, eigene Spielernummer, ob gerade eine Partie läuft, Spielgeschwindigkeit, Veröffentlichungsperiode, Mikrosekunden, die das Erfassen dieser Kopie im Spiel-Thread gekostet hat, Event-Sequenznummer, Zeiten pro Abschnitt. Clients können
requestedPeriodMsschreiben, um eine Veröffentlichungsperiode anzufordern (16 ~ 1000 ms). - Einheit (112 Bytes): Handle-Paar (Einheiten am Handle-Paar erkennen – Adressen werden wiederverwendet), Typ als Vier-Zeichen-Code, Besitzer, Flags, Koordinaten, TP / Mana (mit Maximum), aktuelle Order + Order-Ziel, Aufgabenziel (was sie tatsächlich angreift), Heldenstufe / EP / Fertigkeitspunkte, Detail-Index,
visibleTo(Bit p = Spieler p sieht sie gerade). - Details (288 Bytes, Helden > Spielereinheiten > Creeps, bis zu 256): 12 Fähigkeiten (Code / Stufe / Flags / verbleibende Abklingzeit in Sekunden), 8 Buff-Codes, 6 Inventarplätze.
- Erweiterungen am Blockende (nur angehängt, frühere Offsets verschieben sich nie, alte Clients funktionieren weiter): Erweiterungsbereich
EXT1(Tageszeit im Spiel, Tag-/Nacht-Geschwindigkeit, Anzahl der Einträge in der Produktionstabelle) und Produktionstabelleprods[128](Gebäude, die gerade trainieren / forschen / bauen / aufwerten, Warteschlange, Gesamtdauer, bereits vergangene Zeit, ob sie hängen). Nur verwenden, wenn die Magic passt.
3. Events lesen
head = ring.writeSeq (Offset 8)
for seq in (cursor, head]:
e = ring.events[(seq - 1) % 8192]
if e.seq > seq: einer verloren (zu langsam gelesen, überschrieben)
elif e.seq != seq: noch nicht fertig geschrieben, beim nächsten Mal lesen
else: e verarbeiten
Die Event-Struktur hat 64 Bytes: seq kind clockMs addr handleLo handleHi typeId owner a b x y value extra.
- Aus dem Vergleich zweier aufeinanderfolgender Veröffentlichungen abgeleitet (Genauigkeit = Veröffentlichungsperiode):
unit.appeared / unit.died / unit.removed / unit.damaged / order.changed / hero.levelup / owner.changed / item.appeared / item.removed / game.started; - Auf Engine-Ebene (die Runtime zeichnet sie sofort im Spiel-Thread auf, es gibt also einen für jeden einzelnen Treffer):
damage(Quelle, Schadenstyp, Angriffstyp, Position, tatsächlicher TP-Verlust, Schaden vor Rüstung),killed(Killer); - Aus dem Verfolgen der Produktionstabelle abgeleitet:
production.done(Vier-Zeichen-Code des Fertiggestellten, Kategorie, benötigte Spielsekunden; wird auch für Gegner ausgegeben); - Von der Runtime bei jeder Veröffentlichung nebenbei geprüft:
spell.cast(Abklingzeit einer Fähigkeit startet:aVier-Zeichen-Code der Fähigkeit,bStufe,valueAbklingzeit in Sekunden,x/yZielpunkt),player.left(aSpielernummer,bneuer Slot-Status),selection.changed(Auswahl des lokalen Spielers; die vollständige Liste steht im Erweiterungsbereich des Weltblocks),game.ended(Partie verlassen); - Bildschirmnachrichten:
message(a= Nachrichten-Sequenznummer; den vollen Text schlägst du im Shared MemoryLocal\War3Msgs_<pid>nach: 128 Einträge × 256 Bytes, mit Spielhinweisen, Chat und Systemnachrichten;b= Nummer des Nachrichtenfelds); - Oberfläche & Eingabe (nach
input_enable):ui.click(aID des Canvas-Elements,b1 linke / 2 rechte Maustaste),ui.hover,hotkey(aHotkey-ID,bvirtueller Tastencode),mouse.world(x/yBodenkoordinaten,value= 1 heißt verschluckt); Modifikatortasten stehen jeweils inextra.
4. Befehle erteilen
- Ein Client-Objekt belegt eine Spur:
Local\War3FastMutex_<pid>halten, eine freie Spur suchen (oder eine, deren Besitzerprozess tot ist), Rolle, Spielernummer und die eigene pid eintragen. Braucht derselbe Prozess zwei Rollen, öffnet er zwei Spuren; - Slots füllen: Flag für semantischen Befehl, Opcode,
args[11], DeadlinedeadlineMs; - Sind alle Slots geschrieben, als eingereicht markieren und
submitSeqder Spur um 1 erhöhen; - Auf das Event
Local\War3FastDone_<pid>_<lane>warten (oder pollen), Quittungen lesen, Slots zurückgeben.
Die Runtime führt die Befehle gebündelt im Event-Dispatch des Spiel-Threads aus: Ein Abarbeitungsdurchlauf hat ein Zeitbudget von 4 ms (echter hochauflösender Timer); was darüber hinausgeht, bleibt für den nächsten Dispatch liegen. Slots mit abgelaufener Deadline werden nie mehr ausgeführt – es kann also nicht passieren, dass alte Befehle nach dem Fortsetzen einer Pause noch einmal laufen.
args-Indizes: 0..2 Einheit (Adresse, Handle lo, Handle hi), 3 Order-ID oder Vier-Zeichen-Code, 4..6 Ziel, 7/8 x / y (Float-Bits), 9 extra (Spielernummer / Slotnummer / Schalter / Warteschlangenposition), 10 mode (0 ohne Ziel / 1 auf Punkt / 2 auf Ziel).
Opcodes
| Opcode | Name | Beschreibung |
|---|---|---|
| 1 | point | Order einer Einheit auf einen Punkt (Bewegen / Angriffsbewegung / Patrouillieren / Boden angreifen / Zauber auf Punkt). extra bit0 = anstellen (hinter die aktuelle Order einreihen) |
| 2 | target | Order einer Einheit auf ein Ziel (Rechtsklick-Angriff / Ernten / Reparieren / Zauber auf Ziel / Gegenstand aufheben); das Ziel muss sichtbar sein |
| 3 | immediate | Befehl ohne Ziel (Stopp / Position halten / Trainieren / Forschen / Aufwerten / Zauber ohne Ziel) |
| 4 | build | Arbeiter errichtet ein Gebäude (Koordinaten auf 32 ausgerichtet) |
| 5 | learn | Held erlernt eine Fähigkeit |
| 6 | use_item | Inventarplatz Nummer extra benutzen |
| 7 | revive | Held am Altar wiederbeleben |
| 8 | rally | Sammelpunkt (auf Punkt / auf Ziel) |
| 9 | buy | Ein Laden verkauft einem Helden in der Nähe einen Gegenstand |
| 10 | item_drop | Gegenstand abgeben: einem Verbündeten geben, an einen Laden verkaufen (code = empfangende Einheit) oder auf den Boden legen |
| 20 ~ 25 | Abfragen | q_tech Tech-Zähler, q_feasible Machbarkeit, q_visible Sichtbarkeit, q_mine_gold Restgold einer Mine, q_captain Computer-Captain, q_dead_heroes Liste toter Helden |
| 30 | pause | Pause / Fortsetzen |
| 40 ~ 50 | Kamera | Kamerazustand lesen, Feld setzen, auf einen Punkt blicken, folgen, zurücksetzen, drehen, Grenzen, Glättung, UI ein-/ausblenden, sauberes Bild, Nebel |
| 60 ~ 63 | HUD | Text des Quest-Buttons, Titel und Beschreibung des Quest-Panels, aktualisieren, lesen, ob das Panel geöffnet ist |
| 70 | jass | JASS-Native per Name aufrufen (1291 Stück): Name und String-Parameter stehen im Zusatzbereich des Slots, die übrigen Parameter laut Signatur in args; der Rückgabewert steht in value[0]. Nur für die Spur lokaler Tools; Aufrufe mit Funktionsparametern oder solche, die den Skript-Thread anhalten würden, werden grundsätzlich abgelehnt. Siehe JASS-Kanal |
| 71 / 72 | jass_handle_of / jass_unit_of | Einheit im Snapshot ↔ JASS-Handle umrechnen (das Handle-Paar im Snapshot ist kein JASS-Handle) |
| 73 | canvas_enable | Legt den Canvas-Shared-Memory an und installiert den Zeichen-Hook; jede Spur darf es senden (das Canvas zeichnet nur ins lokale Bild). Beim ersten Mal muss der Hook installiert werden – Timeout von mindestens 2 Sekunden vorsehen |
| 74 | input_enable | extra = 1 übernimmt die Eingabe des Spielfensters (Klicks / Hover auf Canvas-Elemente, Hotkeys, Klicks auf den Boden), 0 = zurückgeben. Eingabeblock Local\War3Input_<pid>: Header 128 Bytes + 32 Hotkeys × 16 Bytes; du schreibst die Hotkey-Tabelle und die Maus-Schalter, die Runtime schreibt Mausposition, den Bodenpunkt unter dem Mauszeiger und das Element unter dem Mauszeiger zurück. Jede Spur darf es senden (betrifft nur die lokale Eingabe). Siehe Oberfläche & Eingabe |
5. Quittungen
Eine Quittung hat 52 Bytes (+8 Bytes Zeitmessung): status, engineReturn, verdict (Grundcode der Ablehnung), orderBefore / orderAfter (Order der Einheit, im selben Frame zurückgelesen), value[8] (Abfrageergebnisse), execUs (wie viele Mikrosekunden dieser Befehl im Spiel-Thread lief), engineUs (davon in der Order-Funktion der Engine selbst).
Alle Statuscodes und Grundcodes findest du unter Quittungen und Grundcodes.
6. Spur-Rollen
| Rolle | Was sie darf |
|---|---|
dev | Lokale Tools: semantische Befehle (für Einheiten des lokalen Spielers) + JASS-Kanal |
player | Nur semantische Befehle, und nur für Einheiten des Spielers, dem die Spur gehört (fremde = not_owner) |
observer | Nur Abfragen, Kamera, Lesen des HUD-Panel-Zustands, Canvas und lokale Eingabe aktivieren; alles andere ist forbidden |
Zwei KIs gegeneinander = zwei player-Spuren in derselben Partie (player 0 / player 1).
Im lokalen Modus deklariert der Client seine Rolle selbst (eine Konvention, keine Sicherheitsgrenze). In der Arena legt der Schiedsrichter-Prozess die Spuren an und übergibt jedem Teilnehmer nur die player-Spur.
7. Im Live-Spiel verifizierte Semantik
- Rechtsklick (smart) auf einen Gegner = genau diesen angreifen (Order-Ziel und Aufgabenziel sind beide diese Einheit); ein roher Angriffsbefehl als Zielbefehl setzt nur die Angriffs-Order, ohne sich das Ziel zu merken – die Einheit greift dann etwas anderes in der Nähe an;
- Die Engine lässt keine Zielbefehle auf Einheiten zu, die du nicht siehst: Nach Einbruch der Nacht liegen entfernte Lager im Kriegsnebel, und jeder Rechtsklick wird abgelehnt (1001);
- Ein „angenommener“ Bau bedeutet nur, dass der Arbeiter den Befehl übernommen hat: Auch ein Punkt im Wald wird sofort angenommen, der Arbeiter scheitert erst, wenn er dort ankommt; offensichtlich belegte Punkte werden sofort abgelehnt;
- Ein Held lässt sich erst etwa 3 Spielsekunden nach seinem Tod wiederbeleben; auch bei zu wenig Nahrung wird abgelehnt (Helden kosten Nahrung);
- Gegenstände im Inventar zählen nicht als Gegenstände am Boden; beim Aufheben wird
item.removedausgegeben; - Während der Pause steht die Engine-Uhr still, Befehle lassen sich aber weiterhin erteilen;
- Wird das Spiel minimiert gestartet, steht die Simulation (die Uhr läuft nicht).