Protocole W3P
L'intégralité du contrat entre le runtime et les programmes externes : huit blocs de mémoire partagée, lecture de l'état du monde, lecture des événements, envoi de commandes, reçus, rôles des voies, canevas, interface et entrées. Lisez cette page si vous vous connectez depuis un autre langage que Python.
Le runtime et les programmes externes échangent des données uniquement via la mémoire partagée. Tout ce qui suit constitue l’ensemble du contrat.
- L’implémentation de référence est en Python :
sdk/python/w3world.py(lecture) etsdk/python/w3fast.py(écriture) ; la taille et les offsets de chaque structure y sont définis et verrouillés par des tests ; - Le protocole ne décrit que la sémantique et ne dépend pas de la version du jeu. Quand la version du jeu change, le runtime s’adapte lui-même et le protocole reste identique ; les nouveaux champs sont toujours ajoutés en fin de bloc, les anciens clients continuent donc de fonctionner.
La plupart des gens n’ont pas besoin de cette page : utilisez simplement le SDK Python. Elle ne vous sert que si vous voulez vous connecter directement en C++ / C# / Rust / Go ou dans un autre langage, ou si vous voulez savoir ce qui se passe sous le SDK.
1. Huit blocs de mémoire partagée
<pid> est l’identifiant du processus du jeu.
| Nom | Sens | Contenu | Synchronisation |
|---|---|---|---|
Local\War3World_<pid> | runtime → vous | État du monde : en-tête + 16 joueurs + jusqu’à 1024 unités + 256 fiches de détail d’unité + 256 objets au sol + zone d’extension + table de production | seqlock |
Local\War3Trees_<pid> | runtime → vous | Jusqu’à 4096 destructibles (arbres, etc.), rafraîchis toutes les 2 secondes | seqlock |
Local\War3Events_<pid> | runtime → vous | Anneau d’événements, 8192 entrées | chaque entrée porte son propre numéro de séquence |
Local\War3Map_<pid> | runtime → vous | Carte : cases de terrain (128 par case, jusqu’à 256×256) + limites de la zone jouable + points de départ ; calculée par lots pendant les premières secondes de la partie | seqlock (ne change plus une fois calculée) |
Local\War3Fast_<pid> | bidirectionnel | Voies de commande : 16 voies × 16 emplacements ; chaque emplacement contient une commande + son reçu ; chaque voie a un rôle | un seul écrivain et un seul lecteur par emplacement |
Local\War3Canvas_<pid> | vous → runtime | Canevas : en-tête de 64 octets + 256 éléments × 112 octets + réserve de 64 Ko pour le texte / les points ; créé seulement après un premier envoi de canvas_enable | seqlock (vous écrivez, le runtime lit à chaque frame) |
Local\War3Msgs_<pid> | runtime → vous | Anneau des messages à l’écran : texte intégral des indications du jeu, du chat et des messages système, 128 entrées × 256 octets | chaque entrée porte son propre numéro de séquence |
Local\War3Input_<pid> | bidirectionnel | Interface et entrées : le runtime y écrit la position de la souris, le point du sol sous le curseur et l’élément survolé ; vous y écrivez la table des raccourcis clavier et les interrupteurs de la souris ; le runtime ne prend en charge les entrées qu’après un premier envoi de input_enable | seqlock pour la table des raccourcis |
Plusieurs clients qui utilisent le canevas et les entrées en même temps : ces deux blocs n’existent qu’en un seul exemplaire, et si chacun écrit de son côté, ils s’écrasent mutuellement. Voici les conventions, à respecter aussi si vous écrivez votre propre client :
- Canevas : lecture - modification - écriture en tenant le mutex nommé
Local\War3CanvasMutex_<pid>; ne remplacez que vos propres éléments et laissez ceux des autres tels quels (en recalculant les offsets dans la réserve) ; supprimez ceux dont le processus propriétaire s’est terminé et ceux qui n’ont pas de propriétaire. Dans un élément,reserved[1]= pid du processus propriétaire,reserved[2]= numéro de séquence dans ce processus ; les numéros d’éléments sont attribués par le compteur situé à l’offset 60 de l’en-tête du bloc (à partir de0x10000). - Entrées : chaque client inscrit ses raccourcis et ses interrupteurs de souris dans
Local\War3InputClients_<pid>(en-tête de 16 octets + 16 clients × 528 octets) ; en tenantLocal\War3InputMutex_<pid>, il met à jour sa propre entrée, puis fusionne les clients encore vivants et écrit le résultat dans le bloc d’entrées : les raccourcis sont dédoublonnés par « code de touche + touches de modification », les interrupteurs de souris sont réunis (union). Les événements sont envoyés à tous les clients, et chacun reconnaît ses raccourcis par « code de touche + touches de modification ». Tant que la table d’inscription contient d’autres clients vivants, n’envoyez pasinput_enable 0. - Runtime : les éléments cliquables dont le processus propriétaire s’est terminé n’interceptent plus les clics ; toutes les 2 secondes, le runtime consulte la table d’inscription et, quand tous les clients inscrits se sont terminés, remet à zéro la table des raccourcis et les interrupteurs de souris du bloc d’entrées.
2. Lire l’état du monde (seqlock)
loop:
s1 = block.seq (offset 8, int32)
if s1 est impair : réessayer (le runtime est en train d'écrire)
copier en-tête + players + units[unitCount] + details[detailCount] + items[itemCount]
if block.seq != s1: réessayer
- En-tête : compteur de publications (s’il n’augmente plus, la publication est interrompue), horloge de jeu du moteur, epoch incrémenté de 1 à chaque partie, numéro du joueur local, partie en cours ou non, vitesse de jeu, période de publication, microsecondes passées à collecter cette copie sur le thread du jeu, numéro de séquence des événements, durées par étape. Les clients peuvent écrire
requestedPeriodMspour demander une période de publication (16 ~ 1000 ms). - Unité (112 octets) : paire de handles (identifiez les unités par leur paire de handles, les adresses sont réutilisées), code à quatre caractères du type, propriétaire, drapeaux, coordonnées, PV / mana (avec leurs maximums), ordre en cours + cible de l’ordre, cible de tâche (ce qu’elle attaque réellement), niveau / expérience / points de compétence du héros, index de détail,
visibleTo(bit p = le joueur p la voit en ce moment). - Détails (288 octets ; héros > unités des joueurs > creeps, 256 au maximum) : 12 capacités (code / niveau / drapeaux / secondes de recharge restantes), 8 codes de buff, 6 emplacements d’inventaire.
- Extensions en fin de bloc (ajout uniquement, les offsets précédents ne bougent jamais, les anciens clients continuent de fonctionner) : zone d’extension
EXT1(heure du jour dans le jeu, vitesse du cycle jour/nuit, nombre d’entrées de la table de production) et table de productionprods[128](bâtiments en train d’entraîner / rechercher / construire / améliorer, file d’attente, durée totale, temps écoulé, bloqué ou non). Ne les utilisez que si le magic correspond.
3. Lire les événements
head = ring.writeSeq (offset 8)
for seq in (cursor, head]:
e = ring.events[(seq - 1) % 8192]
if e.seq > seq: une entrée perdue (écrasée parce que la lecture est trop lente)
elif e.seq != seq: pas encore entièrement écrite, relire la prochaine fois
else: traiter e
La structure d’un événement fait 64 octets : seq kind clockMs addr handleLo handleHi typeId owner a b x y value extra.
- Obtenus en comparant deux publications consécutives (précision = période de publication) :
unit.appeared / unit.died / unit.removed / unit.damaged / order.changed / hero.levelup / owner.changed / item.appeared / item.removed / game.started; - Niveau moteur (le runtime les enregistre sur le thread du jeu au moment où ils se produisent, il y en a donc un pour chaque coup) :
damage(source, type de dégâts, type d’attaque, position, PV réellement perdus, dégâts avant armure),killed(tueur) ; - Obtenus en suivant la table de production :
production.done(code à quatre caractères de ce qui est terminé, catégorie, secondes de jeu nécessaires ; émis aussi pour les adversaires) ; - Vérifiés par le runtime à chaque publication :
spell.cast(début de la recharge du sort :acode à quatre caractères du sort,bniveau,valuerecharge en secondes,x/ypoint d’incantation),player.left(anuméro du joueur,bnouvel état de l’emplacement),selection.changed(sélection du joueur local ; la liste complète est dans la zone d’extension du bloc monde),game.ended(on quitte la partie) ; - Messages à l’écran :
message(a= numéro du message, dont le texte intégral se lit dans la mémoire partagéeLocal\War3Msgs_<pid>: 128 entrées × 256 octets, indications du jeu, chat et messages système compris ;b= numéro du cadre de messages) ; - Interface et entrées (une fois
input_enableactivé) :ui.click(aid de l’élément du canevas,b1 clic gauche / 2 clic droit),ui.hover,hotkey(aid du raccourci,bcode de touche virtuelle),mouse.world(x/ycoordonnées au sol,value= 1 si le clic a été absorbé) ; les touches de modification sont toutes dansextra.
4. Envoyer des commandes
- Un objet client occupe une voie : prenez
Local\War3FastMutex_<pid>, trouvez une voie libre (ou dont le processus propriétaire est mort), puis écrivez le rôle, le numéro de joueur et votre propre pid. Si un même processus a besoin de deux rôles, ouvrez deux voies ; - Remplissez les emplacements : drapeau de commande sémantique, opcode,
args[11], échéancedeadlineMs; - Une fois tous les emplacements écrits, marquez-les comme soumis et incrémentez de 1 le
submitSeqde la voie ; - Attendez l’événement
Local\War3FastDone_<pid>_<lane>(ou interrogez en boucle), lisez les reçus, puis rendez les emplacements.
Le runtime exécute les commandes par lots dans la distribution d’événements du thread du jeu : chaque vidage dispose d’un budget de 4 ms (minuterie haute résolution réelle) ; ce qui dépasse est reporté à la distribution suivante. Un emplacement dont l’échéance est passée n’est jamais exécuté : vous ne verrez donc jamais « d’anciennes commandes réexécutées après la reprise d’une pause ».
Index de args : 0..2 unité (adresse, handle lo, handle hi), 3 identifiant d’ordre ou code à quatre caractères, 4..6 cible, 7/8 x / y (bits du float), 9 extra (numéro de joueur / numéro d’emplacement / interrupteur / mise en file), 10 mode (0 sans cible / 1 sur un point / 2 sur une cible).
Opcodes
| Opcode | Nom | Description |
|---|---|---|
| 1 | point | Ordre sur un point pour une unité (déplacement / attaque-déplacement / patrouille / attaque au sol / sort ciblé sur un point). extra bit0 = mise en file (insérée après l’ordre en cours) |
| 2 | target | Ordre sur une cible pour une unité (attaque au clic droit / récolte / réparation / sort ciblé sur une unité / ramassage d’objet) ; la cible doit être visible |
| 3 | immediate | Commande sans cible (arrêt / tenir la position / entraînement / recherche / amélioration / sort sans cible) |
| 4 | build | Un ouvrier construit un bâtiment (coordonnées alignées sur 32) |
| 5 | learn | Un héros apprend une compétence |
| 6 | use_item | Utilise l’emplacement d’inventaire extra |
| 7 | revive | Ressuscite un héros à l’autel |
| 8 | rally | Point de ralliement (sur un point / sur une cible) |
| 9 | buy | Une boutique vend un objet à un héros à proximité |
| 10 | item_drop | L’objet quitte l’inventaire : donné à un allié, vendu à une boutique (code = unité qui le reçoit), ou lâché au sol |
| 20 ~ 25 | Requêtes | q_tech décompte des technologies, q_feasible faisabilité, q_visible visibilité, q_mine_gold or restant dans une mine, q_captain capitaine de l’ordinateur, q_dead_heroes liste des héros morts |
| 30 | pause | Pause / reprise |
| 40 ~ 50 | Caméra | Lire l’état de la caméra, définir un champ, regarder un point, suivre, réinitialiser, pivoter, limites, lissage, affichage de l’interface, image épurée, brouillard |
| 60 ~ 63 | HUD | Texte du bouton de quêtes, titre et description du panneau de quêtes, rafraîchissement, lire si le panneau est ouvert |
| 70 | jass | Appelle une native JASS par son nom (1291 natives) : le nom et les paramètres chaîne vont dans la zone annexe de l’emplacement, les autres paramètres dans args selon la signature ; la valeur de retour est dans value[0]. Réservé aux voies des outils locaux ; tout appel avec un paramètre de type fonction ou susceptible de suspendre le thread de script est refusé. Voir Canal JASS |
| 71 / 72 | jass_handle_of / jass_unit_of | Conversion unité de l’instantané ↔ handle JASS (la paire de handles de l’instantané n’est pas un handle JASS) |
| 73 | canvas_enable | Crée la mémoire partagée du canevas et installe le hook de dessin ; n’importe quelle voie peut l’envoyer (le canevas ne dessine que sur l’écran local). Le premier appel doit installer le hook : prévoyez un délai d’au moins 2 secondes |
| 74 | input_enable | extra = 1 prend en charge les entrées de la fenêtre du jeu (clic / survol des éléments du canevas, raccourcis clavier, clics au sol), 0 = les rend au jeu. Bloc d’entrées Local\War3Input_<pid> : en-tête de 128 octets + 32 raccourcis × 16 octets ; vous écrivez la table des raccourcis et les interrupteurs de la souris, le runtime y écrit la position de la souris, le point du sol sous le curseur et l’élément survolé. N’importe quelle voie peut l’envoyer (seules les entrées locales sont concernées). Voir Interface et entrées |
5. Reçus
Un reçu fait 52 octets (+8 octets de chronométrage) : status, engineReturn, verdict (code de motif du rejet), orderBefore / orderAfter (l’ordre de l’unité relu dans la même frame), value[8] (résultats des requêtes), execUs (microsecondes d’exécution de cette commande sur le thread du jeu), engineUs (la part passée dans la fonction d’ordre du moteur elle-même).
Tous les codes d’état et codes de motif sont listés dans Reçus et codes de motif.
6. Rôles des voies
| Rôle | Ce qu’il peut faire |
|---|---|
dev | Outils locaux : commandes sémantiques (sur les unités du joueur local) + canal JASS |
player | Uniquement des commandes sémantiques, et uniquement sur les unités du joueur auquel appartient la voie (celles des autres = not_owner) |
observer | Uniquement les requêtes, la caméra, la lecture de l’état des panneaux du HUD, l’activation du canevas et des entrées locales ; tout le reste = forbidden |
Deux IA qui s’affrontent = deux voies player dans la même partie (player 0 / player 1).
En mode local, le rôle est déclaré par le client lui-même (c’est une convention, pas une frontière de sécurité). Sur l’Arène, c’est le processus arbitre qui crée les voies et ne remet à chaque participant que la voie player.
7. Sémantique vérifiée en parties réelles
- Clic droit (smart) sur un ennemi = attaquer celui-ci (la cible de l’ordre et la cible de tâche sont toutes deux cette unité) ; un ordre d’attaque brut envoyé comme commande sur cible ne fait que passer à l’ordre d’attaque sans mémoriser la cible, et l’unité part attaquer autre chose à proximité ;
- Le moteur refuse les commandes sur cible visant des unités que vous ne voyez pas : à la tombée de la nuit, les camps éloignés passent dans le brouillard de guerre et tous les clics droits sont rejetés (1001) ;
- Une construction « acceptée » signifie seulement que l’ouvrier a pris l’ordre : un emplacement en pleine forêt est lui aussi accepté sur le moment, et l’ouvrier n’échoue qu’une fois sur place ; un emplacement manifestement occupé est rejeté immédiatement ;
- Un héros ne peut être ressuscité qu’environ 3 secondes de jeu après sa mort ; la résurrection est aussi rejetée si la nourriture manque (les héros consomment de la nourriture) ;
- Les objets dans un inventaire ne comptent pas comme objets au sol ; en ramasser un émet
item.removed; - Pendant une pause, l’horloge du moteur s’arrête, mais les commandes peuvent toujours être envoyées ;
- Une partie lancée en fenêtre réduite a sa simulation arrêtée (l’horloge n’avance pas).