@asterobot:base
@asterobot:base est la bibliothèque standard dont se sert chaque paquet. D’abord, il fait tout ce que le client Dofus officiel fait de lui-même, pour qu’aucun autre paquet n’ait jamais à le faire. Il donne à un bot un personnage connecté, dans le monde, prêt à tout sur sa carte, et l’y maintient :
il identifie la connexion de jeu avec le ticket qu’Asterobot a obtenu à la connexion ;
il choisit un personnage et attend qu’il soit chargé ;
il entre dans le monde et suit le personnage de carte en carte ;
tant que le script tourne, il garde la connexion en vie comme le fait le client. Il répond à la preuve d’identité du serveur et à ses relances, et il envoie des pings.
Ensuite, il donne à chaque paquet ce qui vient après :
se déplacer : marcher, changer de carte, les portes et les zaaps, les voyages vers n’importe quelle carte, et les fonctions qui choisissent où aller ;
les plans : il joue les plans dessinés dans le Studio d’Asteroboard, en une ligne,
await run(plan)(voir Plans) ;l’état du personnage : niveau, kamas, vie, sacs, combats, objets, métiers et carte, sous forme de conditions qu’un plan ou un script pose (voir Conditions sur l’état du jeu) ;
le gameplay : récolter, combattre avec le module de combat d’Asterobot, se reposer et manger, et l’équipement, sous forme d’étapes qu’un plan joue ou qu’un script appelle (voir Récolter, Combattre, Se reposer et manger et L’équipement) ;
des profils de combat : 24 façons de combattre toutes prêtes, pour n’importe quelle classe, que chaque bot peut jouer (voir Les profils de combat).
D’autre gameplay, comme la banque et la vente, arrivera plus tard dans base, pour qu’un plan nomme @asterobot:base pour tout cela.
Il est pensé pour être la seule dépendance de chaque paquet. Un paquet importe base et oublie la connexion : base démarre tout seul, sur les deux sortes de bots, qu’il soit joué seul ou tiré par d’autres paquets.
C’est du JavaScript simple au-dessus des modules intégrés d’Asterobot, et il ne fait lui-même aucune cryptographie : la preuve d’identité est calculée dans Asterobot, en Go (voir La preuve d’identité). Tout ce qu’il envoie, et quand, vient de captures « homme du milieu » du vrai client, et base s’améliore à chaque nouvelle capture (voir Améliorer base à partir d’une nouvelle capture).
Sommaire
Démarrage rapide
L’installer
Depuis ce dépôt, dans la bibliothèque d’Asterobot :
asterobot library install --path ./base --name @asterobot:base --version 0.0.1
Réinstallez-le après chaque modification, sous la même version tant que vous travaillez dessus : chaque démarrage lit les fichiers installés à ce moment-là.
Le jouer seul
Choisissez @asterobot:base comme paquet d’un bot et cliquez sur Lancer. La Console du bot affiche alors base: playing <name> une fois le personnage sélectionné, et base: in the world, on map <id> une fois qu’il se trouve sur sa carte. Le bot reste dans le monde jusqu’à ce que vous l’arrêtiez.
L’utiliser depuis votre propre paquet
Déclarez-le dans le asterobot.json de votre paquet :
{
"dependencies": {
"@asterobot:base": { "version": "0.0.1" }
}
}
L’importer suffit. Attendez le monde là où votre code en a besoin :
import { connect } from "@asterobot:base";
import { botInfo } from "asterobot:bot";
export default async function behavior() {
// Pas attendu ici, pour que Lancer rende la main tout de suite.
void main();
}
async function main() {
const map = await connect();
botInfo("On map", map.mapId, "cell", map.cellId);
// ... votre script
}
connect() se résout avec la carte sur laquelle se trouve le personnage, tout de suite s’il y est déjà, et se rejette si le personnage ne peut pas y arriver (voir Lignes de console et erreurs). Elle renvoie la même promesse à chaque appel pendant une exécution du script, donc chaque paquet du bot peut l’appeler.
Comment base fonctionne dans tous les cas
Un paquet qui utilise base doit se comporter de la même façon sur un bot Full socket, où Asterobot est le client, et sur un bot MITM, où une personne joue avec le client officiel et où Asterobot relaie la session. Il doit aussi fonctionner que base soit le paquet joué par le bot, ou une dépendance plusieurs niveaux plus bas. Base gère lui-même ces quatre cas.
Les quatre cas
Bot Full socket | Bot MITM | |
|---|---|---|
Base joué seul | S’identifie, sélectionne, entre dans le monde, puis répond à tout ce que le serveur demande | N’envoie rien. Attend que le personnage de la personne soit sur une carte. |
Base en dépendance, à n’importe quelle profondeur | Pareil, dès que le script charge base | Pareil |
Base décide lui-même de la colonne qui s’applique, d’après session.shared : Asterobot le définit sur un bot MITM. Il n’y a rien à configurer, et un paquet qui utilise base est le même code dans les quatre cas.
Sur un bot Full socket, personne d’autre n’est là pour répondre au serveur, donc base fait tout ce que ferait le client officiel.
Sur un bot MITM, le client officiel répond déjà. Une seconde réponse de base mettrait deux réponses sur le réseau là où le serveur en attend une, donc base n’envoie absolument rien. Il suit quand même la session, si bien que
connect()se résout dès que le personnage de la personne se trouve sur une carte.
Il démarre tout seul
Le index.js de base appelle connect() à son niveau le plus haut, pendant le chargement du module.
Assez tôt. Les modules se chargent avant le code qui les importe, donc les gestionnaires de base sont en place avant que le code de tout autre paquet ne tourne, et avant que le serveur ne puisse parler. La première vérification du serveur arrive juste après qu’il a accepté le ticket.
À n’importe quelle profondeur. Dans une chaîne comme
my-bot → @alice:harvest → base, l’auteur demy-botne sait peut-être même pas que base est là, et il tourne quand même. Un paquet charge base quand l’un de ses fichiers l’importe ; le lister dansdependenciessans l’importer ne charge rien.Jamais pendant la lecture des réglages. Asterobot exécute aussi le niveau le plus haut de chaque module quand il lit les réglages d’un paquet, sans bot ni session de jeu. Base reconnaît ce moment, où
session.connectionIdvaut0n(ce qui n’arrive jamais pendant une vraie exécution), et ne démarre pas.
C’est le seul endroit où base fait quelque chose que le guide du développeur déconseille, un effet de bord au niveau le plus haut d’un module. C’est voulu : c’est ce qui permet à chaque paquet d’oublier base.
Un seul point d’entrée
Base exporte connect() pour attendre le monde, ainsi que latency() et toutes les fonctions d’asterobot:movement : move(), changeMap(), useElement(), les voyages (travel(), route(), stopTravel(), trip()) et les fonctions qui trouvent les cartes. Il exporte aussi ce qui joue les plans, run(), stop() et plan(), les conditions sur l’état du jeu, et les étapes de gameplay avec leurs conditions. Les étapes de la connexion elles-mêmes (l’identification, la sélection, l’entrée dans le monde et les réponses au serveur) sont internes. Base les exécute déjà, et un second point d’entrée vers les mêmes étapes ferait la course avec sa propre exécution : une seconde identification considérerait le ticket comme accepté avant que le serveur ait répondu.
Écrire un paquet au-dessus de base
Importez base, et faites
await connect()avant d’agir. Elle se résout tout de suite si le personnage est déjà sur sa carte.Ne refaites pas ce que fait base. Les pings, les relances, la preuve d’identité et l’échange de carte ont déjà leur réponse. Les envoyer à nouveau ferait répondre le bot deux fois.
Vérifiez
session.sharedavant d’agir. Sur un bot MITM, la personne qui joue contrôle le même personnage. Un paquet qui marche ou combat devrait y rester passif, ou demander d’abord.Déplacez-vous avec ce que base réexporte.
move(),changeMap(),useElement(),travel()et les autres sont celles d’asterobot:movement: le bot fait un déplacement à la fois, et après un changement de carte, base charge la nouvelle carte comme il le fait pour chaque carte.Fixez une seule version de base. Deux versions chargées côte à côte répondraient toutes les deux au serveur.
Ce qui se passe, étape par étape
Les attentes sont celles que le vrai client a prises dans la capture, à partir de 100 ms. Base ajoute un peu de bruit autour de chacune, et envoie tout le reste directement, dans l’ordre du client (voir Délais et bruit).
Quand | Base | Serveur |
|---|---|---|
Tout de suite |
| |
| ||
Tout de suite |
| |
Tout de suite |
| |
| ||
1,5 à 3,5 s plus tard, comme une personne qui clique |
| |
| ||
Tout de suite, à chaque fois |
|
|
t0 + 1085 ms | cinq réglages sociaux, tous vides, puis les maisons de guilde, les informations de guilde, la réinitialisation hebdomadaire de guilde | |
t0 + 1349 ms |
| |
+231 ms | l’étape de chaque quête en cours, les prix moyens (une fois par jour), puis les amis, les connaissances, le conjoint, les maisons de guilde | |
| ||
1,2 à 2,4 s plus tard, sur la première carte |
| |
|
Ensuite, tant que le script tourne :
Tous les | Base | Serveur |
|---|---|---|
10 s environ en moyenne |
|
|
4 minutes |
|
|
De temps en temps, à plusieurs minutes d’intervalle |
|
|
Nouvelle carte |
|
|
Une heure après chaque clé |
|
|
API
import { connect, latency, move, changeMap, useElement, travel, route, stopTravel, trip, findMaps, mapAt, mapInfo, nearest, nearestWith, mapsWithin, reachable } from "@asterobot:base";
import { run, stop, plan, conditions, levelAtLeast, kamasAtLeast, lifeBelow, bagsAbove, inFight, hasItem, jobLevelAtLeast, onMap, inSubarea, inArea } from "@asterobot:base";
import { steps, gather, harvestsAtLeast, fightMonsters, fightSettings, diedInLastFight, fightsAtLeast } from "@asterobot:base";
import { rest, eat, equip, unequip, wears } from "@asterobot:base";
Export | Renvoie | Ce qu’il fait |
|---|---|---|
| Promesse de la carte | Attend que le personnage soit sur sa carte. Base l’exécute déjà pendant son chargement. |
| nombre | La latence que le bot déclare au serveur, en millisecondes. |
| Promesse de la cellule d’arrivée | Fait marcher le personnage vers une cellule de sa carte : celle d’ |
| Promesse de la carte et de la cellule | Emmène le personnage sur la carte suivante par un côté : celle d’ |
| Promesse de ce qu’a fait l’utilisation | Utilise une porte, un zaap, une ressource ou une sortie de la carte : celle d’ |
| Promesse de la carte et de la cellule | Emmène le personnage sur n’importe quelle carte, aussi loin soit-elle, carte après carte : celle d’ |
| Le chemin que prendrait | |
| Les fonctions qui trouvent les cartes où voyager : celles d’ | |
| Jouer un plan dessiné dans le Studio d’Asteroboard, l’arrêter, et ce qu’appelle un fichier de plan pendant son import : voir Plans. | |
| booléen, ou promesse d’un booléen | L’état du personnage, sous forme de questions oui/non. |
| Une promesse, un booléen | Récolter les ressources de la carte, et combien de récoltes ont été faites. |
| Une promesse, rien, des booléens | Combattre : attaquer les groupes de monstres de la carte, régler le module de combat, et ce qu’ont donné les combats. |
| Une promesse | Récupérer de la vie, en s’asseyant et en mangeant ce que contiennent les sacs. |
| Une promesse, une promesse, un booléen | Mettre des objets, les retirer, et savoir si le personnage en porte un. |
| des objets | Les déclarations de toutes les étapes et conditions ci-dessus, que le Studio propose dans chaque paquet qui dépend de base. |
Tout le reste de la connexion se fait tout seul, donc base n’en exporte rien de plus. Un second point d’entrée vers les mêmes étapes ferait la course avec sa propre exécution.
connect()
const map = await connect();
Le premier appel, que base fait pendant son chargement, fait ceci dans l’ordre :
Il enregistre les étapes clés de la session dans la mémoire du bot (voir Mémoire).
Sur un bot Full socket, il commence à répondre au serveur : la preuve d’identité, les deux relances et les pings, et chaque carte (voir La preuve d’identité, Garder la connexion en vie et Suivre les cartes).
Il identifie la connexion, sélectionne un personnage et entre dans le monde (voir les étapes).
Elle se résout avec ce que renvoie currentMap() d’asterobot:bot une fois le personnage placé : { mapId, characterId, cellId, actors, elements }, elements étant les éléments interactifs de la carte et ce que chacun propose à cet instant.
La même promesse à chaque fois pendant une exécution du script : chaque appel suivant l’attend simplement.
Un échec est journalisé. La raison va dans la Console du bot sous forme de ligne d’erreur, et la promesse se rejette avec elle.
S’arrêter n’est pas un échec. Une promesse rejetée parce que le script s’arrête ou que la connexion s’est fermée n’écrit rien dans la Console.
Les étapes
L’identification. Base envoie IdentificationRequest avec session.gameToken et session.language, puis attend jusqu’à 30 s la réponse du serveur.
Il n’envoie jamais un second
IdentificationRequestsur une même connexion.Un ticket refusé arrête tout : le ticket est consommé, et seule une nouvelle connexion au compte en apporte un autre.
La sélection d’un personnage. Base joue le personnage que nomme le réglage character, ou le premier de la liste.
Quand un combat attend l’un des personnages du compte, après une déconnexion en plein combat, le serveur choisit lui-même ce personnage, juste après la liste (CharacterForceSelectionEvent). Base répond alors aussitôt, comme le client (CharacterForceSelectionReadyRequest), joue ce personnage, et prévient dans la Console du bot quand le réglage character en nomme un autre.
L’attente de la liste n’a pas de limite : elle arrive après la file d’attente du serveur, quelle que soit sa longueur.
La réponse à la sélection est attendue jusqu’à 30 s, et le chargement jusqu’à 60 s.
Elle échoue quand :
le compte n’a aucun personnage sur le serveur ;
aucun personnage ne porte le nom donné par le réglage (le message liste ceux qui existent) ;
le serveur refuse la sélection.
L’entrée dans le monde. Base envoie ce que le client envoie après le chargement (voir Entrer dans le monde), puis attend jusqu’à 60 s que le personnage soit placé sur sa carte.
Les réponses au serveur tournent dès le départ, et tant que le script tourne.
latency()
La moyenne des 50 derniers allers-retours de ping du bot, en millisecondes, ou 0 avant le premier :
arrondie au pair le plus proche, comme le fait le
Math.Rounddu client ;puis ramenée à un entier signé sur 16 bits, comme le fait la conversion du client.
Les allers-retours sont gardés dans la mémoire du bot, donc ils survivent à un redémarrage du script et à une reconnexion, comme dans un client qui reste ouvert.
Réglages
Base a sa propre carte dans l’onglet Réglages du bot, quel que soit le paquet qui l’utilise.
Réglage | Valeurs | Par défaut | Ce qu’il change |
|---|---|---|---|
| Le nom d’un personnage | Vide | Le personnage à jouer, quelles que soient ses majuscules. Vide joue le premier de la liste, comme le fait la sélection automatique du client. |
|
|
| La communauté de discussion que rejoint |
|
|
|
|
|
|
|
|
Les réglages sont lus au moment où ils servent, donc un changement s’applique à partir de la prochaine exécution de l’étape. Un changement de ping s’applique dès le battement suivant.
Plans
Un plan est un fichier d’un paquet, dessiné dans le Studio d’Asteroboard : des lieux sur la carte du monde, l’ordre dans lequel le bot en fait le tour, et ce qu’il fait à chacun, avec les fonctions d’un paquet ou la logique propre au Studio (attendre, attendre que, seulement si, réessayer…). Base le joue. Le format des plans décrit ce que contient le fichier.
Jouer un plan
import { run } from "@asterobot:base";
import main from "./plans/main.js";
// Les réglages du plan, dans l'onglet Réglages du bot.
export { parameters } from "./plans/main.js";
export default async function behavior() {
// Pas attendu ici, pour que Lancer rende la main tout de suite.
void run(main);
}
C’est l’index.js qu’écrit le Studio quand il crée un paquet. Un script écrit à la main peut appeler run() n’importe où, autant de fois qu’il veut, un plan à la fois.
Export | Renvoie | Ce qu’il fait |
|---|---|---|
| Promesse | Joue un plan : l’export par défaut d’un fichier de plan. Se résout quand le circuit se termine ou est arrêté, se rejette quand le plan échoue (voir Échecs). |
| booléen | Demande au plan en cours de s’arrêter proprement. Dit s’il y en avait un. |
| Le plan | Appelée par un fichier de plan pendant son import, jamais à la main. Elle garde le plan et ce à quoi il fait référence, et ne lève jamais d’erreur : elle s’exécute pendant qu’Asterobot lit les déclarations du paquet sans bot, et une erreur à ce moment empêcherait le Studio de lire sa propre palette. |
run(plan) :
Attend le monde, avec
connect().Vérifie tout le plan, et chaque plan qu’il joue par ses interruptions et ses étapes
plan: le format, chaque sorte et chaque champ, chaque lieu choisi et chaque requête qui restreint la recherche (voir Les lieux), chaque paquet et chaque fonction qu’un appel nomme, chaque argument selon les règles des réglages, chaque argument omis qui n’a rien avec quoi jouer (sa valeur par défaut, sinon le zéro de son type, enfreint ses propres règles, ou il a unrefsans valeur par défaut), le nom et la valeur par défaut de chaque réglage, chaque$settinget la valeur par défaut du réglage qu’il nomme, chaque plan auquel il fait référence, au moins un arrêt. Au moindre problème, elle se rejette tout de suite en listant tous les problèmes, chacun avec le libellé et l’identifiant de son bloc, avant que le bot ne fasse un seul pas.Parcourt le circuit, tour après tour, jusqu’à sa fin.
Un plan à la fois.
run()pendant qu’un autre plan est joué se rejette tout de suite avecbase: a plan is already running. Les plans qu’un plan joue lui-même, pour une interruption ou une étapeplan, ne comptent pas.S’arrêter proprement. Un arrêt vient de
stop(), d’une étapestop, ou d’Asterobot qui demande au script de s’arrêter, par exemple à la fin d’une plage du planning. L’étape en cours se termine alors (une étape longue voitcontext.stoppingpasser à vrai et peut finir plus tôt), unwaitou unwaitUntilse termine tout de suite, un voyage s’arrête à la fin de son tronçon, etrun()se résout, même s’il attendait encore le monde. Asterobot attend cela avant d’arrêter le script, dans le temps que le planning laisse pour finir.Un échec rejette
run(). Rien n’attrape unvoid run(main), donc un plan qui échoue arrête le script, et la page du bot dit pourquoi. Attrapez-le pour faire autre chose.
Un tour
L’arrêt suivant : le suivant dans l’ordre où ils ont été dessinés (
drawn), ou le plus proche qui a encore du travail (nearest).Si son
whenest faux, il est sauté pour ce tour.Son lieu donne ses cartes, visitées de la plus proche à la plus lointaine depuis l’endroit où se trouve le bot. Une carte qu’aucun voyage n’atteint est sautée, avec une ligne de console ; l’arrêt échoue quand aucune ne peut être atteinte.
Sur chaque carte :
travel()jusqu’à elle, sauf si le bot y est déjà, puis les interruptions, qui sont aussi la vérification avant la première étape ; puis chaque étape à son tour : sonwhen, l’étape, sononFailuresi elle échoue, et les interruptions de nouveau avant l’étape suivante. Quand une étape a emmené le bot ailleurs, comme une étapeplan, il revient sur la carte avant l’étape suivante ; après la dernière, il continue directement.Après chaque arrêt,
repeat.until, s’il y en a un. Le dernier arrêt d’un tour compte d’abord le tour, donc « jusqu’à 3 tours » se termine juste à la fin du troisième.À la fin d’un tour,
oncetermine le circuit, etforeveretuntilcommencent le suivant.
Un tour dure au moins une seconde : un circuit sans rien à rejoindre et sans rien qui attende, un seul arrêt sur la carte du bot, tournerait sinon sans jamais laisser le bot faire autre chose.
Les cartes en attente. Une étape qui se résout avec { nothingToDo: true, retryAfter } laisse la carte de cet arrêt de côté pendant retryAfter secondes, ou jusqu’au tour suivant sans valeur. Les deux ordres sautent une carte en attente. nearest choisit la carte la plus proche d’un arrêt qui n’est pas en attente, et un tour se termine une fois que chaque arrêt a été joué ou est en attente. Quand toutes les cartes restantes sont en attente, le runner attend que la première ait de nouveau du travail plutôt que de tourner à vide.
Les interruptions sont vérifiées dans l’ordre à l’arrivée sur chaque carte et avant chaque étape, jamais pendant une étape ou un voyage. C’est la vérification à l’arrivée qui permet d’interrompre aussi un circuit fait uniquement de points de passage. La première dont la condition est vraie joue son plan, imbriqué. Pendant que ce plan tourne, la même interruption n’est plus vérifiée, même depuis l’intérieur de ce plan, donc « sacs pleins : banque » ne peut pas relancer le trajet vers la banque depuis ce même trajet ; les interruptions propres au plan imbriqué s’appliquent bien. Ensuite :
resumerevient sur la carte où était le circuit, et reprend avec l’étape qu’il allait jouer ;stoptermine le plan auquel appartient l’interruption.
Avec resume, le plan de l’interruption doit rendre sa condition fausse : le trajet de banque vide les sacs. Quand la condition est encore vraie juste après le plan, le plan s’arrête avec une erreur qui nomme l’interruption, plutôt que de le rejouer environ une fois par seconde tant que le plan tourne. Un chance dans la condition ne compte pas contre elle, puisque la vérification suivante tire à nouveau.
Les réglages d’un plan
Un plan peut déclarer des réglages, exactement sous la forme des réglages d’un paquet, et n’importe quel argument d’une fonction qu’il appelle peut prendre la valeur d’un réglage au lieu d’une valeur fixe : { "$setting": "monsters" }. Le réglage doit exister et avoir le type de l’argument.
Le nom d’un réglage est un nom qu’un script pourrait écrire : des lettres, des chiffres et
_, sans chiffre au début. Jamais__proto__, que l’objet littéral du fichier du plan lirait autrement, si bien que le réglage disparaîtrait.Un réglage déclare une valeur par défaut quand le zéro de son type enfreint ses propres règles (un
intd’au moins 1 commencerait à 0), ou quand c’est une valeur seule avec unref(0 serait un identifiant que personne n’a choisi) : la page du bot le fait commencer à cette valeur.Un argument lié convient à la valeur par défaut de son réglage. Tant que personne n’a réglé une autre valeur sur la page du bot, l’argument prend la valeur par défaut du réglage, sinon le zéro de son type, donc cette valeur doit passer les règles de l’argument lui-même : un réglage qui vaut 150 par défaut ne peut pas tenir lieu de pourcentage.
Ils deviennent des réglages du paquet, dans l’onglet Réglages du bot, dès que l’entrée du paquet les réexporte. Le Studio propose d’écrire la ligne :
export { parameters } from "./plans/main.js"; // un seul plan, rien d'autre à déclarer
// ou, fusionnés avec ceux de l'entrée :
import { parameters as mainSettings } from "./plans/main.js";
export const parameters = { ...mainSettings, myOwnSetting: { type: "int", default: 3 } };
Lus en direct. Le runner lit un réglage chaque fois qu’un argument s’en sert, donc une valeur changée sur la page du bot s’applique dès l’étape suivante.
Sans réexport, le plan utilise la valeur par défaut de chaque réglage, et la Console le dit en une ligne avant que le bot ne bouge, en les nommant tous.
Plusieurs plans d’un même paquet partagent les valeurs du bot : deux plans qui déclarent le même nom utilisent la même valeur, et doivent le déclarer avec le même type.
Une valeur qui enfreint les règles de l’argument lui-même, comme 150 pour un pourcentage, fait échouer le bloc qui s’en sert.
Échecs
Une seule règle pour tout : un voyage, une étape ou une condition qui échoue arrête le plan, sauf si le plan dit autre chose. Les étapes et les arrêts ont chacun un onFailure :
| Alors |
|---|---|
absent, ou | Le plan s’arrête avec l’erreur |
| Une ligne de console, puis l’étape suivante (pour une étape) ou l’arrêt suivant (pour un arrêt) |
| Réessayé jusqu’à |
Ce qui fait échouer quoi :
Ce qui arrive | Ce qui échoue |
|---|---|
L’étape d’un paquet lève une erreur ou se rejette, un | L’étape |
Le | L’étape |
Le lieu d’un arrêt est introuvable, ou aucune de ses cartes n’est atteignable, un voyage vers l’une de ses cartes échoue, ou son | L’arrêt. Une étape qui échoue ne regarde qu’elle-même, jamais son arrêt. |
La condition d’une interruption échoue, ou le plan qu’elle joue, ou avec | Le plan, qui s’arrête |
Une condition échoue quand elle lève une erreur ou se rejette, comme le fait une condition sur l’état du jeu à propos d’un fait que le jeu n’a pas encore dit, ou quand elle répond autre chose que true ou false. Un when simplement faux saute son étape ou son arrêt, ce qui n’est pas un échec.
Quand le plan s’arrête, le runner écrit l’erreur dans la Console, et run() se rejette avec elle. L’erreur nomme le bloc, par son libellé et son identifiant, puis la cause :
base: Astrub farm › 2 Astrub fields (k3f9) › Fight (p2x1) failed 3 times: no monster to fight
base: Astrub farm › 3 map 191105026 (m8q2) failed: travel: no known way leads to map 191105026
base: Astrub farm › interrupt Bank when full (i7w0) failed: condition Bags above (c0b2): the bags' weight isn't known yet
base: Astrub farm › interrupt Bank when full (i7w0) is still true after its plan plans/bank.js ran: that plan must make it false, or it would run again and again
Les lignes de console d’un plan
Elles restent en anglais, telles que base les écrit :
Niveau | Ligne | Quand |
|---|---|---|
info |
| Un arrêt arrive : son numéro sur le circuit, puis son libellé, ou son lieu s’il n’en a pas |
info |
| Une étape commence : une fonction par son libellé, une étape logique par ce qu’elle fait ( |
info |
| Une étape |
info |
| La condition d’une interruption est vraie |
warn |
| Une étape ou un arrêt est réessayé |
warn |
| Une étape ou un arrêt est sauté, avec |
warn |
| Les cartes d’un lieu qu’aucun voyage n’atteint, toutes sur une ligne |
warn |
| Une fois, avant de bouger, en nommant chaque réglage que l’entrée ne réexporte pas, de tous les plans que l’exécution peut jouer ( |
error |
| Le plan s’arrête |
error |
| La vérification a trouvé des problèmes |
<plan> est le libellé du plan, sinon son chemin dans le paquet, sinon plan quand son fichier ne donne pas son chemin. <block> est l’endroit où se trouve le bloc, avec les identifiants : <plan> › <n> <stop> (<id>) › <step> (<id>).
Où en est le runner
Pendant qu’un plan est joué, le runner garde l’endroit où il en est dans la mémoire du bot, sous @asterobot:base/plan :
{ plan: "plans/bank.js", stop: "k3f9", step: "p2x1", map: 191105026, lap: 2, connectionId: 7n }
planest le chemin du plan joué dans son paquet,nullquand son fichier ne le donne pas.stopetstepsont des identifiants de blocs ;stepvautnullà l’arrivée sur une carte, avant sa première étape.Elle est écrite à l’arrivée sur chaque carte, au début de chaque étape, et quand un plan joué depuis là (celui d’une interruption, d’une étape
plan) se termine, et retirée une fois querun()s’est terminée.
Le Studio en direct d’Asteroboard la lira pour montrer où est le bot. Rien ne la lit encore.
Le format des plans
Le format 1 : ce que le Studio écrit, et ce que le runner lit. Le Studio écrit tout le fichier. Il est documenté ici pour qu’un runner et un Studio s’accordent dessus, et pour qu’un plan puisse se lire sans le Studio.
Le fichier
Un plan est un module .js, où qu’il soit dans le paquet. C’est sa première ligne qui en fait un plan :
// asterobot-studio-plan v1
// Made in Asteroboard's Studio. Edit it there: changes made by hand are lost the next time the Studio saves it.
import * as runner from "@asterobot:base";
import { values } from "asterobot:parameters";
import * as p0 from "@asterobot:farm";
import * as q0 from "./bank.js";
const data = /* studio:plan */ {
"format": 1,
"runner": "@asterobot:base",
…
} /* studio:end */;
export const parameters = data.settings ?? {};
export default runner.plan(data, {
path: "plans/astrub.js",
packages: { "@asterobot:base": runner, "@asterobot:farm": p0 },
plans: { "plans/bank.js": () => q0.default },
settings: values,
});
Le JSON entre
/* studio:plan */et/* studio:end */est le plan. Tout le reste est généré à partir de lui.path: le chemin du fichier lui-même dans le paquet, qui nomme le plan dans les lignes de console quand il n’a pas de libellé, et dans où en est le runner. Un fichier sans lui reste un plan, dont le chemin est inconnu.packages: un import d’espace de noms par paquet qu’uncallnomme, celui du runner compris.plans: un import par autre plan que le plan joue. Ils sont lus au dernier moment,() => q0.default, pour que deux plans qui se jouent l’un l’autre (ferme → banque → ferme) puissent s’importer mutuellement. Dans le JSON, les chemins sont relatifs au paquet (plans/bank.js) ; l’import est relatif au fichier (./bank.js).settings:valuesd’asterobot:parameters, qui dans ce fichier sont les réglages du paquet lui-même, pour que le runner lise les réglages du plan dans le bon paquet.parameters: les réglages du plan, déclarés comme ceux d’un paquet, pour que l’entrée les réexporte (voir Les réglages d’un plan).
Le plan
{
"format": 1,
"runner": "@asterobot:base",
"label": "Astrub farm",
"settings": {
"monsters": { "type": "array", "of": "int", "ref": "monsters", "label": "Monsters to fight", "default": [31, 34] }
},
"circuit": {
"order": "drawn",
"repeat": { "kind": "forever" },
"stops": [
{
"id": "k3f9",
"label": "Astrub fields",
"place": { "kind": "subarea", "id": 95 },
"steps": [
{ "id": "p2x1", "kind": "call", "package": "@asterobot:farm", "step": "fight",
"args": { "monsters": { "$setting": "monsters" } },
"when": { "id": "t5h2", "kind": "time", "from": "08:00", "to": "20:00" },
"onFailure": { "kind": "retry", "times": 2, "then": "skip" } },
{ "id": "w4t6", "kind": "wait", "seconds": 5, "upTo": 15 }
]
},
{
"id": "m8q2",
"place": { "kind": "map", "mapId": 191105026 },
"when": { "id": "n1t3", "kind": "not", "of": { "kind": "call", "id": "c5r1", "package": "@asterobot:base", "condition": "levelAtLeast", "args": { "level": 50 } } },
"steps": []
}
]
},
"interrupts": [
{
"id": "i7w0",
"label": "Bank when full",
"when": { "kind": "call", "id": "c0b2", "package": "@asterobot:base", "condition": "bagsAbove", "args": { "percent": 90 } },
"run": { "plan": "plans/bank.js" },
"then": "resume"
}
]
}
Champ | Signification |
|---|---|
|
|
| Le paquet dont |
| Facultatif. Affiché dans le Studio et dans les lignes de console |
| Facultatif. Les réglages du plan, par nom, chacun sous la forme d’un réglage de paquet (voir Les réglages d’un plan) |
|
|
|
|
| Les arrêts, dans l’ordre où ils ont été dessinés. Au moins un |
| Facultatif. Vérifiées dans cet ordre |
Les quatre circuits du Studio sont des préréglages d’order et de repeat, et toute autre combinaison est valable aussi, comme un balayage jusqu’au niveau 50 :
Circuit |
|
|
|---|---|---|
Boucle |
|
|
Aller simple |
|
|
Balayage |
|
|
Jusqu’à |
|
|
Les identifiants. Chaque bloc qu’une personne peut sélectionner (un arrêt, une étape, une condition, une interruption) a un id : 4 caractères parmi a-z et 0-9, unique dans le plan, donné par le Studio et jamais réutilisé. Les erreurs et la position du runner nomment les blocs par lui.
Les arrêts
Champ | Signification |
|---|---|
| Le libellé est facultatif |
| Où est l’arrêt (voir Les lieux) |
| Comment le bot s’y rend. Absent vaut |
| Condition facultative. Fausse quand l’arrêt arrive : l’arrêt est sauté pour ce tour |
| Ce qui se passe quand l’arrêt échoue (voir Échecs) |
| Jouées dans l’ordre, sur chaque carte du lieu. Vide pour un arrêt qui n’est qu’un point de passage |
Les lieux
| Champs | Cartes |
|---|---|---|
|
| Cette carte |
|
| Ces cartes : une zone peinte dans le Studio |
|
|
|
|
|
|
|
|
|
Un lieu est choisi. Un
mapnomme sonmapId, unsubareaou unareasonid: un lieu que le Studio a passé à l’une de ces sortes n’a pas d’identifiant tant qu’on n’en a pas choisi un, et le runner refuse le plan plutôt que de deviner. 0 est un identifiant comme un autre : la zone 0 est Amakna.Une requête restreint la recherche : elle nomme au moins l’un de
subArea,area,monster,resource,x,y,worldMap. Unzaapseul, ou rien du tout, prendrait toutes les cartes du jeu.Les identifiants tiennent dans ce que lit Asterobot. Les nombres d’une requête et l’
idd’unsubareaou d’unareasont des nombres entiers qui tiennent sur 32 bits : de 0 à 2147483647, et à partir de -2147483648 pourxety. Les identifiants de carte sont des nombres JSON, entiers et au-dessus de 0 : ceux de Dofus y tiennent largement.Un lieu
subarea,areaouquerysuit les données de jeu de la version que joue le bot.
Les étapes
Chaque étape a un id et un kind, et peut avoir un when, une condition vérifiée juste avant elle (fausse, elle saute l’étape, ce qui n’est pas un échec), et un onFailure.
| Champs | Fait | Échoue quand |
|---|---|---|---|
|
| Joue une étape que donne un paquet. Un argument absent prend sa valeur par défaut déclarée ; un argument peut valoir | La fonction lève une erreur ou se rejette |
|
| Attend | Jamais |
|
| Vérifie la condition tout de suite, puis chaque seconde, jusqu’à ce qu’elle soit vraie | Le délai passe avant, ou la condition échoue |
|
| Joue ce plan ici, puis continue | Le plan échoue |
|
| Écrit le message dans la Console | Jamais |
| Termine tout le | Jamais |
L’étape d’un paquet est une fonction run(args, context), éventuellement asynchrone, qui se résout avec rien (fait sur cette carte) ou avec { nothingToDo: true, retryAfter } (rien à faire sur cette carte, peut-être de nouveau dans retryAfter secondes). context vaut { map, stop, lap, stopping } : la carte où se trouve le bot, l’arrêt joué (son JSON), les tours faits à partir de 0, et si un arrêt est demandé. Le guide du développeur d’Asterobot explique comment un paquet déclare ses étapes et ses conditions pour le Studio.
Le runner lit les déclarations d’un paquet comme Asterobot les lit : un default à null n’est pas une valeur par défaut (le zéro du type, ou le premier choix), un ref à null ou "" n’est pas un ref, un min ou un max qui n’est pas un nombre n’est pas une borne, un label qui n’est pas du texte n’en est pas un. Une déclaration veut donc dire la même chose pour Asterobot, le Studio et le runner.
Les conditions
| Champs | Vraie quand |
|---|---|---|
|
| La condition du paquet le dit. Les arguments comme pour une étape |
|
| Chacune l’est |
|
| Au moins une l’est |
|
| Elle ne l’est pas |
|
| Ce temps s’est écoulé depuis le début du |
|
| Le circuit a été fait au moins ce nombre de fois |
|
| L’horloge de la machine qui fait tourner Asterobot indique de |
|
| Un tirage au hasard, nouveau à chaque vérification, tombe sous |
Les conditions gardent un arrêt (when), une étape (when, un waitUntil), le circuit (repeat.until) et une interruption (when). La condition d’un paquet est une fonction run(args, context) qui renvoie un booléen ou une promesse d’un booléen.
Les interruptions
Champ | Signification |
|---|---|
| Comme pour un arrêt |
| Une condition |
|
|
|
|
Ce que ce runner ne connaît pas
Un kind, un order, un repeat.kind, un then ou un champ que ce runner ne connaît pas, ou un paquet, une étape ou une condition que le fichier de plan ne lui donne pas : le runner refuse le plan avant de bouger, en nommant le bloc, et en disant que le plan demande un @asterobot:base plus récent ou un paquet manquant. Le format ne grandit que par des ajouts qui ne changent pas la lecture des plans existants ; tout le reste est un nouveau format.
Conditions sur l’état du jeu
L’état du personnage, sous forme de questions oui/non. Chacune est une fonction que base exporte, utilisable depuis le code, et déclarée dans les conditions de base, donc le Studio la propose dans chaque paquet qui dépend de base :
Condition | Arguments | Vraie quand |
|---|---|---|
|
| Le niveau du personnage vaut au moins |
|
| Il porte au moins |
|
| Sa vie est sous |
|
| Ses sacs sont remplis à plus de |
| Un combat est en cours | |
|
| Il porte au moins |
|
| Ce métier vaut au moins |
|
| Il se trouve sur cette carte |
|
| Il se trouve dans cette sous-zone |
|
| Il se trouve dans cette zone |
Le contraire de chacune est un not autour d’elle. Un objet, un métier, une carte, une sous-zone et une zone sont demandés avec un ref vers leur table de données de jeu (items, jobs, maps, sub_areas, areas), donc le Studio propose une recherche par nom.
Elles lisent ce qu’Asterobot suit du personnage, asterobot:character, et de sa carte, currentMap() et mapInfo(). Un fait que le jeu n’a pas encore dit n’est jamais deviné : la condition lève une erreur qui le nomme, comme the level isn't known yet ou the jobs aren't known yet. Dans un plan, c’est l’échec du bloc que la condition garde, que son onFailure traite. Quand connect() se résout, le jeu a déjà envoyé le personnage, ses caractéristiques et son inventaire, donc cela concerne surtout un plan lancé plus tôt, ou une liste de métiers que le jeu n’a pas encore envoyée.
Depuis le code, chacune prend ses arguments en un seul objet, comme le Studio les passe :
import { bagsAbove, levelAtLeast } from "@asterobot:base";
if (levelAtLeast({ level: 50 }) && bagsAbove({ percent: 90 })) {
// …
}
inSubarea() et inArea() renvoient une promesse, puisqu’elles cherchent la carte dans les données de jeu.
Récolter
Récolter, pour les plans comme pour les scripts : récolter les ressources de la carte avec l’action du métier, comme le client de jeu quand le joueur en clique une. Base déclare l’étape dans son export steps et la condition dans ses conditions, donc le Studio les propose dans chaque paquet qui dépend de base. Depuis le code, elles prennent leurs arguments en un seul objet, comme le Studio les passe :
import { gather } from "@asterobot:base";
// 303, c'est le Bois de Frêne.
await gather({ resources: [303], bagsAbove: 90 });
gather
Récolte les ressources de la carte qui conviennent, l’une après l’autre :
Un combat en cours, comme l’agression d’un monstre de la carte, est attendu d’abord, joué par le module de combat, comme
fightMonstersen attend un. La récolte continue ensuite.Elle choisit la ressource suivante que ses arguments laissent passer : la plus proche d’abord, à la distance en cellules depuis le personnage, ou celle du plus haut niveau d’abord.
Elle utilise l’élément avec la compétence qui la récolte, comme le fait
useElement(): le personnage marche jusqu’à portée de l’élément et le serveur commence la récolte. Elle attend ensuite la fin de la récolte, jusqu’à 5 s après la durée donnée par le serveur.Un élément dont l’utilisation échoue, pris par quelqu’un d’autre entre-temps ou hors de portée, est essayé une fois de plus, puis abandonné.
Elle s’arrête quand plus rien ne convient, après maxHarvests récoltes, ou dès que les sacs sont plus remplis que bagsAbove. Quand rien ne convenait, il n’y avait rien à faire sur cette carte, et le plan n’y revient pas avant 60 s, le temps que les ressources repoussent. Elle échoue quand le personnage meurt dans un combat, avec the character died in the fight, et, sans jamais deviner, quand le jeu n’a pas encore dit les niveaux de métier ou le poids des sacs dont elle a besoin.
Argument | Type | Par défaut | Récolte |
|---|---|---|---|
|
| aucun | Ce qui donne ces objets, comme 289, le Blé. Avec |
|
| aucun | Chaque ressource de ces métiers. Ni |
|
| aucun | Jamais ce qui donne ces objets |
|
|
| Seulement ce que le niveau du personnage dans le métier permet. Le serveur refuserait le reste |
|
|
| La plus proche d’abord, ou la ressource du plus haut niveau d’abord, la plus proche à égalité |
|
| 0 | Termine l’étape après ce nombre de récoltes sur la carte. 0 : tout ce qui convient |
|
| 0 | Termine l’étape dès que les sacs sont plus remplis que cette part de ce qu’ils peuvent porter. 0 : jamais |
Ce qu’offre une carte, c’est ce qu’Asterobot en suit : les éléments de currentMap() et les compétences que chacun offre en ce moment, un élément récolté n’offrant plus rien jusqu’à ce qu’il repousse. La table skills des données du jeu dit quel objet chaque compétence récolte, pour quel métier, à partir de quel niveau, et maps__interactive_elements où se trouve chaque élément.
La condition de récolte
Condition | Arguments | Vraie quand |
|---|---|---|
|
| Le personnage a fait au moins |
Combattre
Combattre, pour les plans comme pour les scripts : attaquer les groupes de monstres de la carte, que le module de combat d’Asterobot joue, régler la façon dont le module joue tant que le plan tourne, et ce qu’ont donné les combats. Base déclare les deux étapes dans son export steps et les deux conditions dans ses conditions, donc le Studio les propose dans chaque paquet qui dépend de base. Depuis le code, chacune prend ses arguments en un seul objet, comme le Studio les passe :
import { fightMonsters, fightSettings } from "@asterobot:base";
fightSettings({ profile: "Prudent" });
await fightMonsters({ monsters: [4109], maxLevel: 30 });
fightMonsters
Attaque les groupes de monstres de la carte qui conviennent, le plus proche d’abord, l’un après l’autre, et attend la fin de chaque combat :
Un combat en cours, celui de l’étape ou un autre venu autrement, comme une agression, est attendu d’abord.
Elle choisit le groupe le plus proche que ses arguments laissent passer, à la distance en cellules depuis le personnage.
Sur un bot Full socket, elle active le module de combat, tant que le script tourne, quand il est désactivé : un combat ne peut pas avancer sans lui. Sur un bot MITM, l’interrupteur reste celui de la personne, et module désactivé, elle joue le combat elle-même.
Elle marche jusqu’à la cellule du groupe et envoie
AttackMonsterRequestavec l’identifiant du groupe, comme le client de jeu. Quand le serveur ne lance aucun combat dans les 5 s, le groupe s’est déplacé ou n’est plus là : base marche de nouveau et attaque depuis sa nouvelle cellule, et renonce à un groupe après trois attaques.Une fois le combat fini, elle attend jusqu’à 10 s le contenu de la carte, qui revient avec les groupes, et passe au suivant.
Elle s’arrête quand plus aucun groupe ne convient, ou après maxFights combats. Quand aucun groupe ne convenait, il n’y avait rien à faire sur cette carte, et le plan n’y revient pas avant 60 s, le temps que des groupes reviennent. Elle échoue quand le personnage meurt dans un combat, avec the character died in the fight : il n’est plus sur la carte, et le onFailure de l’étape décide de la suite.
Argument | Type | Par défaut | Garde les groupes |
|---|---|---|---|
|
| aucun | Avec au moins un de ces monstres. Aucun : n’importe quel groupe |
|
| aucun | Sans aucun de ces monstres |
|
| aucune | Avec un monstre de l’une de ces races. Aucune : n’importe quelle race |
|
| 0 | Dont le niveau, la somme des niveaux de ses monstres comme le jeu l’affiche, vaut au moins celui-ci |
|
| 0 | Dont le niveau vaut au plus celui-ci. 0 : sans limite |
|
| 0 | Avec au plus ce nombre de monstres. 0 : sans limite |
|
| 0 | Sans monstre au-dessus de ce niveau. 0 : sans limite |
|
|
| Sans archimonstre, avec ou sans, ou seulement avec un. Un archimonstre est un monstre que les données du jeu marquent ainsi, le |
|
| 0 | Termine l’étape après ce nombre de combats sur la carte. 0 : chaque groupe qui convient |
Les groupes et leurs monstres sont ce qu’Asterobot suit de la carte : les acteurs de currentMap() et leurs monsters. Les races et les archimonstres viennent de la table monsters des données du jeu.
fightSettings
Règle la façon dont le module de combat joue tant que le script tourne, comme le fait asterobot:fight. Ce qu’elle laisse à keep, ou un profil vide, reste comme le disent les réglages du bot, et ce qu’elle règle dure jusqu’à l’arrêt du script.
Argument | Valeurs | Par défaut | Règle |
|---|---|---|---|
|
|
| L’interrupteur du module. Sur un bot MITM, l’interrupteur reste celui de la personne |
| Le nom d’un profil de combat | vide | Le profil que le bot joue, par son nom sur la page Profils de combat. Vide garde celui en vigueur |
|
|
| La vitesse |
Les conditions de combat
Condition | Arguments | Vraie quand |
|---|---|---|
| Le personnage est mort dans son dernier combat. Fausse avant le premier | |
|
| Le personnage a livré au moins |
Toutes deux suivent chaque combat du bot à partir des événements du module de combat (onFight), dès que base démarre, quel que soit celui qui a lancé le combat.
Se reposer et manger
Récupérer de la vie, pour les plans comme pour les scripts : s’asseoir, comme un joueur avec l’émote assis, et manger ce que contiennent les sacs. Base déclare les deux étapes dans son export steps, donc le Studio les propose dans chaque paquet qui dépend de base. Depuis le code, chacune prend ses arguments en un seul objet, comme le Studio les passe :
import { rest } from "@asterobot:base";
// Manger ce que contiennent les sacs, puis s'asseoir pour le reste, jusqu'à 90 % de la vie.
await rest({ upTo: 90, eat: true });
La vie est ce qu’Asterobot suit du personnage, character().life, qui compte la régénération que le serveur lance entre deux caractéristiques, plus rapide assis. Les objets sont ses piles, items().
rest
S’assoit jusqu’à ce que la vie revienne à une part de son maximum :
Rien à faire quand la vie y est déjà.
Avec
eat, il mange d’abord, commeeat.Il s’assoit avec l’émote du client de jeu,
EmotePlayRequest { emoteId: 1 }, l’émote 1 des données de jeu, S’asseoir, que le serveur confirme avecEmotePlayEvent. Une émote que le serveur ne confirme pas dans les 5 s n’est pas renvoyée, puisque la même émote relève le personnage : la vie revient tout de même debout, plus lentement.Une fois la vie à
upTo, il se relève avec la même émote, au moins 1,2 s après s’être assis, le temps de recharge de l’émote. Le serveur confirme avec l’émote 0.
Un combat qui survient entre-temps, et qui relève le personnage, est attendu comme fightMonsters en attend un, puis il se rassoit. Elle échoue quand le personnage meurt dans un combat, avec the character died in the fight ; quand la vie ne change plus pendant une minute, avec the character's life isn't coming back ; et, sans jamais deviner, tant que le jeu n’a pas dit la vie, avec the character's life isn't known yet.
Argument | Type | Par défaut | Ce qu’il fait |
|---|---|---|---|
|
| 100 | Se relève dès que la vie atteint cette part de son maximum |
|
|
| Mange d’abord ce que contiennent les sacs, ce qui est plus rapide |
|
| aucun | Ne mange que ces objets. Aucun : toute nourriture qui rend de la vie |
eat
Mange ce que contiennent les sacs jusqu’à ce que la vie revienne à une part de son maximum :
Il choisit la nourriture : une pile des sacs dont les effets dans les données de jeu rendent de la vie (action 110 : pains, potions, viandes, poissons, boissons), seulement les objets de
foodquand ils sont donnés. Celle qui soigne le plus sans dépasser ce qui manque passe d’abord, sinon celle qui soigne le moins.Il en utilise une, comme le client de jeu pour un objet sans cible :
ObjectUseRequest { objectUid }. Il attend que la pile diminue, puis que la vie monte avec les caractéristiques suivantes.Une nourriture que le serveur ne prend pas dans les 5 s est abandonnée pour le reste de l’étape.
Il attend de 0,6 à 1,2 s entre deux, au rythme d’une personne. Rien à faire quand la vie y est déjà, ou quand les sacs n’ont rien à manger. Un combat entre-temps est attendu, comme dans rest. Elle prend upTo et food, comme rest.
L’équipement
Mettre et retirer des objets, pour les plans comme pour les scripts, comme le joueur les glisse dans un emplacement ou de nouveau dans les sacs. Base déclare les deux étapes dans son export steps et la condition dans ses conditions :
import { equip, unequip, wears } from "@asterobot:base";
// 1439 et 852 : Mauvaise Pioche et Petit Anneau Magique.
if (!wears({ item: 1439 })) await equip({ items: [1439, 852] });
Les emplacements où un objet peut aller viennent de ses données de jeu : les possible_positions du super-type de son type, donc un anneau va à l’une ou l’autre main et un Dofus dans l’un des six emplacements de Dofus. La requête est celle du client de jeu, ObjectSetPositionRequest { objectUid, position, quantity: 1 }, que le serveur confirme avec ObjectMovementEvent ; un objet déjà dans l’emplacement retourne dans les sacs avec le sien.
equip
Met les items, l’un après l’autre au rythme d’une personne, chacun dans un emplacement libre où il peut aller, sinon dans son premier emplacement, ce qui renvoie l’objet qui s’y trouvait dans les sacs. Un objet déjà porté reste comme il est. Elle échoue pour un objet qui n’est pas dans les sacs, avec item 1439 isn't in the bags, un objet qui ne se porte pas, avec item 1439 can't be worn, et un objet que le serveur n’a pas déplacé dans les 5 s, avec the game didn't move item 1439. Rien à faire quand chacun est déjà porté.
unequip
Retire les items, de nouveau dans les sacs, l’un après l’autre au rythme d’une personne, ou tout ce que porte le personnage si aucun n’est donné. Elle échoue pour un objet que le serveur n’a pas déplacé dans les 5 s, comme equip. Rien à faire quand aucun n’est porté.
La condition d’équipement
Condition | Arguments | Vraie quand |
|---|---|---|
|
| Le personnage porte cet objet |
Les profils de combat
Base fournit 24 profils de combat pour le module de combat d’Asterobot, prêts à jouer. Une fois base installé, ils sont sur la page Profils de combat avec la source Paquet, et dans la liste Profil de chaque bot, sous le nom @asterobot:base/<clé> : @asterobot:base/careful, @asterobot:base/ranged-kite… Un bot en joue un qu’il joue base lui-même ou non, et chaque bot le joue dans la version de base la plus récente installée : mettre base à jour les met à jour dès le combat suivant.
Chacun est indépendant de la classe : aucun ne nomme de sort, donc chacun joue n’importe quelle classe à n’importe quel niveau, avec les sorts et l’arme du personnage. Quand son but n’est pas un rôle, un profil ne donne pas de rôle, donc chaque classe garde celui que le jeu lui donne en premier (un Féca soutient, un Eniripsa soigne), ni de portée, donc il combat à la portée que font ses sorts. Ce qu’un profil ne donne pas est la valeur du profil par défaut.
Clé | Ce qu’il fait | Ce qu’il règle |
|---|---|---|
| Prudent : ne finit jamais un tour là où les ennemis peuvent le blesser gravement, et se cache dès que sa vie est basse |
|
| Tout pour l’attaque : inflige le plus de dégâts possible et ignore le danger, mort comprise |
|
| Se place près, fonce sur l’ennemi le plus proche et frappe au contact |
|
| Se place loin, frappe à distance, puis sort de la portée des ennemis |
|
| Se place loin, frappe à distance, puis finit son tour hors de la vue des ennemis |
|
| Ne bouge que pour atteindre un lancer, pour garder taclés les ennemis à côté de lui |
|
| Achève d’abord l’ennemi qui a le moins de vie |
|
| Tue d’abord l’ennemi qui peut le blesser le plus là où il se tient |
|
| Soigneur : soigne d’abord l’allié le plus faible, puis lui-même, et frappe quand personne n’a besoin de soin |
|
| Garde du corps : protège ses alliés, boucliers et boosts pour le plus faible, et se place pour les abriter |
|
| Affaiblit les ennemis et booste ses alliés avant de frapper |
|
| Passe devant, encaisse les coups pour l’équipe et tacle les ennemis au contact |
|
| Contrôle : pousse les ennemis, leur retire PA et PM et leur donne des états avant de les frapper |
|
| Invocateur : invoque dès qu’il peut et reste derrière ses invocations, hors de la vue des ennemis |
|
| Poseur de pièges : pose pièges et glyphes là où les ennemis vont passer, puis reste hors de leur portée |
|
| Préfère ses sorts de terre, pour un personnage force |
|
| Préfère ses sorts de feu, pour un personnage intelligence |
|
| Préfère ses sorts d’eau, pour un personnage chance |
|
| Préfère ses sorts d’air, pour un personnage agilité |
|
| Frappe avec son arme dès qu’il peut, au contact |
|
| Ne frappe jamais avec l’arme ni à mains nues : ses PA vont à ses sorts |
|
| Joueur d’équipe : ne blesse jamais un allié avec ses zones, et soigne et booste ses alliés dès que ça aide |
|
| Un bas niveau dans un groupe fort : se place loin, reste hors de danger et de vue, et ne frappe que sans risque |
|
| Pour les boss et les donjons : prudent, tue d’abord l’ennemi le plus dangereux et garde le groupe à l’abri |
|
Ce que fait chaque réglage est dans le guide d’Asterobot, Combats > Réglages du profil, et le format dans son guide du développeur, Fight profile format.
Pour en jouer un, choisissez-le dans la liste Profil d’un bot, dans son onglet Combat ; depuis un plan, avec l’étape Fight settings et son nom (
@asterobot:base/careful) ; depuis un script, avecsetProfile("@asterobot:base/careful")d’asterobot:fight.Pour l’adapter, sur la page Profils de combat, copiez-le avec Copier dans vos propres profils pour le modifier, ou faites un profil qui l’
extendset ne donne que ce qui change :{ "version": 1, "name": "Cra prudent", "extends": "@asterobot:base/careful", "elements": ["air"] }. Celui qui l’étend suit les mises à jour de base.Leurs clés ne changent pas. Un bot réglé sur l’un d’eux le garde d’une mise à jour de base à l’autre. Pour en garder un exactement tel quel, copiez-le dans vos propres profils.
Ce sont les fichiers JSON de fightprofiles/, un par profil, chacun nommé d’après sa clé et tel que la page Profils de combat exporte un profil : c’est ainsi qu’un paquet fournit des profils. Ce sont des données, jamais exécutées.
La preuve d’identité
Une fois la session ouverte, le serveur de jeu demande au client de prouver qu’il détient une clé secrète. C’est une preuve d’identité de Schnorr en trois échanges :
ServerSessionReadyEvent -> ClientChallengeInitRequest challengeKey = G^x mod P
ServerVerificationEvent -> ClientIdRequest id = G^k mod P
ServerChallengeEvent { c } -> ClientChallengeProofRequest proof = (k + c*x) mod Q
P est le nombre premier de 768 bits de la RFC 2409, section 6.1, « Oakley group 1 ».
G = 2, et Q = (P - 1) / 2.
x est le secret du client, tiré une fois par processus client.
k est un nonce, tiré à nouveau pour chaque
ServerVerificationEvent.Le serveur vérifie que
G^proof = id × challengeKey^c (mod P).
Base ne fait aucun de ces calculs. Il appelle publicKey(), commitment() et proof(value) d’asterobot:verification, qui s’adressent au code Go d’Asterobot (ankama/game/network/verification), et envoie les chaînes qu’elles renvoient.
Les secrets n’entrent jamais dans JavaScript. x et k restent en Go. Un script ne voit que ce qui part sur le réseau.
x appartient au bot. Il est tiré une fois à la création du bot et gardé toute sa vie, d’un script à l’autre et d’une reconnexion à l’autre, comme un processus client le garde. La même clé publique est publiée à chaque connexion.
k est remplacé à chaque engagement. Un second
ServerVerificationEventavant le défi remplace le nonce, et la preuve répond au plus récent, comme le fait le client.Un défi absent reçoit aussi une réponse. Quand
ServerChallengeEventne porte aucune valeur,proof(undefined)calcule le défi lui-même, comme le fait le client : le SHA-256 de G, de la clé publique, de l’engagement et de l’empreinte de l’appareil envoyée à la connexion du bot. Les deux cas arrivent : le défi d’une capture portait une valeur, celui d’une autre était vide.
La part de base, c’est l’enveloppe et l’ordre :
Chaque réponse part dès que son événement arrive. Le client répond à sa trame suivante, 6 à 55 ms plus tard selon sa charge, ce qui reste du bruit sous celui du réseau. Le serveur attend chaque réponse avant son étape suivante, donc les trois ne peuvent pas se croiser. Extrait d’une capture :
00:14:08.961 IN ServerVerificationEvent 00:14:08.968 OUT ClientIdRequest #10 00:14:08.997 IN ServerChallengeEvent (no value) 00:14:09.003 OUT ClientChallengeProofRequest #11Les trois réponses portent un uid, comme celles du client, envoyées avec
send(..., { correlated: true }). Rien n’y répond jamais.
Les calculs sont couverts par les tests de ankama/game/network/verification dans Asterobot. Ils vérifient le groupe d’après le texte de la RFC, et l’équation du serveur sur chaque branche. Ils vérifient aussi deux vrais contrôles auxquels le client Dofus a répondu, tirés de captures d’un même lancement du jeu :
Contrôle | Connexion | Défi | uids | La preuve du client |
|---|---|---|---|---|
1 |
| aucun : le client le calcule |
| se vérifie avec le défi qu’Asterobot calcule |
2 |
| une valeur de 255 bits venue du serveur |
| se vérifie avec cette valeur |
Le groupe, la preuve et les deux branches sont donc confirmés face au vrai client, pas seulement lus dans son code. Les deux contrôles se vérifient avec la même clé, envoyée sur c3 : le client garde son secret pendant tout le lancement, d’une connexion à l’autre, comme Asterobot en garde un par bot d’une reconnexion à l’autre.
Garder la connexion en vie
Le serveur relance le client avec deux événements à la fois, dans la même milliseconde et tous deux vides : SequenceNumberEvent et BasicLatencyStatsEvent. Extrait d’une capture du client :
23:57:26.192 IN SequenceNumberEvent
23:57:26.192 IN BasicLatencyStatsEvent
23:57:26.192 OUT SequenceNumberRequest { number: 3 } no uid
23:57:26.200 OUT BasicLatencyStatsRequest { latency: 44 } #5
SequenceNumberEvent
Le client répond tout de suite avec SequenceNumberRequest { number }, envoyé sans uid :
numberest le nombre de relances que cette connexion a reçues, celle à laquelle il répond comprise, donc la première réponse vaut 1 ;le compte est gardé dans la mémoire du bot, par connexion. Un script relancé sur la même connexion continue de compter. Le serveur a envoyé chaque relance, il peut donc vérifier le nombre exactement.
BasicLatencyStatsEvent
Le client répond avec BasicLatencyStatsRequest { latency } une trame après la séquence, 8 à 30 ms dans les captures. Il porte un uid, et rien n’y répond.
Base envoie latency() juste après le SequenceNumberRequest de la même relance, dans l’ordre du client.
Les paires arrivent toutes les 4 minutes, selon l’horloge du serveur plutôt qu’à partir de la connexion : à la seconde 26 d’une minute dont le reste de la division par 4 vaut 1, dans deux sessions. La première arrive 0 à 4 minutes après le chargement du personnage. Un SequenceNumberEvent de plus arrive seul, sans question de latence, pendant le chargement du personnage ; il compte comme la première relance, donc 7 paires plus tard la réponse est number: 8.
Chaque paire consomme un uid, un contrôle en consomme deux, et les requêtes numérotées du jeu les leurs : chaque requête numérotée prend l’uid suivant. Dans une session, ClientChallengeInitRequest était #2, puis :
Relance | Heure | Réponse de latence |
|---|---|---|
3 | 23:57:26 |
|
(contrôle) | 00:14:08 |
|
9 | 00:21:26 |
|
Les relances 2 et 3 seules donneraient #3 et #4 : une autre requête numérotée est partie avant la relance 3 dans cette session. Base puise dans le même compteur, donc les mêmes requêtes reçoivent les mêmes numéros.
Pings
Un vrai client n’envoie de pings que lorsqu’une personne l’utilise : toutes les 5 secondes avec son option de ping rapide, sur les battements où il y a eu une action. Avec ping sur active, base reproduit cela :
un battement de 5 secondes qui envoie
PingRequest { quiet: true }environ une fois sur deux ;seulement une fois le personnage chargé ;
chaque
PongEventqui répond à un ping en cours ajoute son aller-retour aux échantillons dontlatency()fait la moyenne, et un pong sans ping en cours est ignoré.
Entrer dans le monde
Ce que le client envoie après CharacterLoadingCompleteEvent, dans l’ordre. Le client espace certains de ces messages d’une trame ou deux, 10 à 90 ms ; base envoie chaque groupe d’un seul tenant et ne garde que les attentes de 100 ms et plus :
Quand | Messages | Avec |
|---|---|---|
t0 + 1085 ms |
| Attend pareil, n’envoie rien |
ensuite |
| Sauté |
t0 + 1349 ms |
| Envoyé |
ensuite |
| Sauté |
+231 ms |
| Sauté |
ensuite |
| Sauté |
ensuite |
| Sauté |
Les étapes de quête suivent le journal. Le serveur envoie
QuestsEvent, le journal de quêtes du personnage, pendant son chargement. Une fois dans le monde, le client demande l’étape de chaque quête en cours, chaque quête deux fois. Dans les captures :1631, 1631avec une quête en cours, et1631, 1637, 1631, 1637avec deux.Seul
ContextCreationRequestest nécessaire pour atteindre la carte. Le reste est ce qui fait ressembler le bot à un client.Le premier écart se mesure à partir du chargement lui-même. Un script relancé en retard n’attend pas une seconde fois la seconde entière.
Le jeton de boutique reste frais. Avec
faithful, base envoie de nouveauBakApiTokenRequestune heure après chaqueBakApiKeyEvent, comme le fait le client.
Suivre les cartes
Chaque MapCurrentEvent { mapId } nomme la carte sur laquelle le serveur a mis le personnage. Pour une carte instanciée, le serveur envoie MapCurrentInstanceEvent { mapId, instantiateMapId } à la place, et base le traite de la même façon avec son mapId : instantiateMapId ne sert à rien, comme dans le client. Il arrive avec un TimeEvent, l’horloge du serveur, auquel rien ne répond. Le client charge l’asset de la carte depuis ses propres fichiers, puis dit qu’il est prêt :
Il attend un temps aléatoire entre 1,2 et 2,4 s pour la première carte de la session, et entre 0,4 et 1,2 s ensuite. Dans les captures, une première carte a pris 2,4 s une fois et 1,197 s une autre, et les cartes suivantes 1,19 s et 407 ms, une carte déjà chargée auparavant allant plus vite.
Sur la première carte seulement, il envoie
AnomalySubareaInformationRequest.Il envoie
ContextReadyRequest { mapId }, puisMapInformationRequest { mapId }, l’un après l’autre, avec l’identifiant qu’a donnéMapCurrentEventouMapCurrentInstanceEvent.Le serveur répond avec
MapComplementaryInformationEvent, un aller-retour plus tard, et le suivi de carte d’Asterobot place le personnage :currentMap()a soncellId.
Le serveur n’envoie rien de la nouvelle carte avant l’arrivée de ces deux requêtes. Dans une capture, les 1,19 s entre MapCurrentEvent et les requêtes ne portaient aucun message entrant :
21:19:44.838 IN MapChangeOrientationEvent, MapCurrentEvent, TimeEvent
21:19:46.028 OUT ContextReadyRequest + MapInformationRequest same map id
21:19:46.058 IN MapComplementaryInformationEvent
Le client déclenche les deux requêtes à partir d’un même événement interne, son moteur de rendu qui démarre sur la carte chargée, donc il n’envoie jamais l’une sans l’autre. Base non plus.
Quand un MapCurrentEvent ou un MapCurrentInstanceEvent plus récent arrive alors qu’une carte est encore en chargement, seule la carte la plus récente est annoncée.
La carte d’un combat n’est pas touchée. Quand un combat commence, le serveur nomme la carte où il se déroule, et le client y répond autrement (FightPingsRequest, puis ContextReadyRequest). Le module de combat d’Asterobot y répond tant qu’il est activé, et rien ne doit le faire quand il est désactivé : base n’annonce donc jamais une carte nommée pendant un combat. La carte nommée une fois le combat fini est annoncée comme les autres.
Avec changeMap() et useElement(). Elles se résolvent une fois le contenu de la nouvelle carte arrivé, donc elles attendent que base le demande. Base le dit à Asterobot avec answerMapLoads(), et Asterobot n’envoie alors jamais lui-même les deux requêtes, quel que soit le temps que prend base : sur un bot qui utilise base, elles partent une seule fois, de base, au rythme du client, même sur un PC lent. Sur un bot dont aucun script ne charge les cartes, Asterobot les envoie lui-même, au rythme du client lui aussi.
Mémoire, et reprise en cours de route
Les variables propres d’un script disparaissent après Arrêter, un rechargement ou Lancer depuis l’éditeur, alors que le bot et sa connexion de jeu continuent. Un script relancé sur la même connexion ne doit pas tout recommencer :
le serveur n’accepte pas un second
IdentificationRequest;le personnage est déjà sélectionné ;
le serveur a compté chaque relance.
Base garde donc ce qu’il sait dans la mémoire du bot, que tous les scripts du bot partagent et qui survit à chacun d’eux.
Clé | Ce qu’elle contient |
|---|---|
| L’état de la connexion de jeu actuelle, à côté de |
|
|
|
|
| Où en est le plan joué, tant qu’il l’est : voir Où en est le runner. |
L’état de la connexion a ces champs. Chacun est absent tant que son étape n’a pas eu lieu.
Champ | Défini quand |
|---|---|
| Toujours : le |
|
|
| Le serveur a répondu au ticket |
|
|
| Le serveur a sélectionné un personnage |
|
|
|
|
| Les requêtes d’après le chargement ont commencé à partir |
|
|
|
|
| La dernière carte nommée par |
| La dernière carte pour laquelle |
| Le nombre de |
| Le dernier |
Ce que fait un script relancé sur la même connexion, selon l’endroit où le précédent s’est arrêté :
Le script précédent s’est arrêté | Le nouveau |
|---|---|
Avant d’envoyer | S’identifie |
Après l’avoir envoyé, avant la réponse | Ne le renvoie pas. Il considère le ticket comme accepté, puisque la connexion est toujours ouverte, et avertit dans la Console. |
Avant qu’un personnage soit sélectionné | Demande la liste et sélectionne |
Après la sélection, avant la fin du chargement | Attend le chargement |
Après le chargement, avant que les réglages sociaux partent | Envoie tout ce qui suit le chargement |
Après le début de l’envoi des réglages sociaux, avant | Saute les groupes sociaux et de guilde, puis envoie |
Après | N’envoie plus rien de ce qui suit le chargement. Les groupes que le script précédent n’a pas atteints sont sautés, pas envoyés en retard. |
Pendant le chargement d’une carte | Annonce cette carte |
N’importe où après | Se résout tout de suite avec la carte actuelle |
Après une reconnexion, session.connectionId est nouveau, l’état de connexion enregistré est ignoré, et tout recommence.
La page du bot affiche ces clés dans Outils > Mémoire.
Réagir à ce que base répond
Un message va à chaque gestionnaire qui l’écoute, donc le fait que base réponde à un message ne le retire pas à votre paquet. Vos propres on() et wait() reçoivent chaque message auquel base répond, MapCurrentEvent compris, sur les deux sortes de bots. Ce script note chaque changement de carte, puis la cellule où arrive le personnage :
import "@asterobot:base";
import { currentMap, botInfo, botWarn } from "asterobot:bot";
import { on, wait } from "asterobot:protocol";
export default async function behavior(launch) {
on("MapCurrentEvent", async (traffic) => {
const mapId = traffic.payload?.mapId;
if (mapId === undefined) return;
botInfo("Heading to map", mapId);
try {
// Son contenu arrive une fois la carte chargée, un instant après ceci.
await wait(
(next) => next.type === "MapComplementaryInformationEvent" && next.payload?.mapId === mapId,
{ timeout: 10000 },
);
botInfo("On map", mapId, "cell", currentMap()?.cellId);
} catch (error) {
// Une erreur qui s'échappe d'un gestionnaire arrête tout le script.
botWarn("Never arrived on map", mapId, String(error));
}
});
}
Ne répondez pas à ce à quoi base répond. Sur un bot Full socket, envoyer vous-même
ContextReadyRequest,MapInformationRequest, ou les réponses à la preuve d’identité et aux relances atteindrait le serveur deux fois. Tout le reste vous appartient : noter, enregistrer, consulter, planifier.Quand
MapCurrentEventarrive, la nouvelle carte n’est pas encore chargée. Base dit qu’il est prêt 0,4 à 2,4 s plus tard, et c’est seulement alors que le serveur envoie son contenu. D’ici là,currentMap()nomme déjà la nouvelle carte, mais sans acteurs nicellId. Pour agir dessus, attendez sonMapComplementaryInformationEvent, comme ci-dessus.Les identifiants sont des BigInt. Comparez-les avec des BigInt, comme
mapId === 153879301n.findPath()prend un identifiant de carte en BigInt, etrecord()d’asterobot:gamedatanon : passez-luiNumber(mapId).Les cartes instanciées sont annoncées avec
MapCurrentInstanceEventau lieu deMapCurrentEvent. Écoutez les deux pour voir chaque changement de carte.Les gestionnaires de base passent avant les vôtres, parce que base se charge avant votre code. Rien n’en dépend.
Sur un bot MITM
Sur un bot MITM, une personne joue le personnage avec son propre client Dofus, qui fait lui-même tout ce qui précède. Base n’envoie alors absolument rien :
il ne répond ni à la preuve d’identité, ni aux relances, ni aux cartes, et n’envoie jamais de ping : le client s’en charge ;
il ne s’identifie pas, puisque le client l’a déjà fait ;
il prend le personnage déjà sélectionné, ou attend que la personne en sélectionne un ;
connect()se résout dès que le personnage se trouve sur une carte, sans limite de temps.
Il suit quand même la session, donc un paquet construit sur base lit les mêmes choses sur les deux sortes de bots et attend le monde de la même façon. Un paquet qui agit, qui marche ou qui combat, devrait vérifier lui-même session.shared, puisque la personne qui joue contrôle le même personnage.
La personne qui joue est aussi protégée : le signalement de mod loader du client Dofus n’arrive jamais chez Ankama. Le client envoie un message, YoshiRequest, quand il trouve un mod loader dans son dossier d’installation (MelonLoader, BepInEx, dotnet, ou les DLL qu’un mod loader génère). Sur un bot MITM, Asterobot empêche ce message d’atteindre le serveur de jeu dès le début de la session, avant Play, avec ou sans base, et la Console du bot affiche Kept the game client's mod-loader report from the game server à chaque fois. Base n’a rien à faire pour cela, et un bot Full socket n’envoie jamais ce message.
Tous les messages que base envoie
Message | Quand | uid | Réglage |
|---|---|---|---|
| Une fois par connexion | aucun | |
| Après l’acceptation du ticket | aucun | |
| Après la liste | aucun |
|
|
| aucun | |
|
| oui | |
|
| oui | |
|
| oui | |
|
| aucun | |
|
| oui | |
| Le battement de ping | aucun |
|
Les cinq réglages sociaux | Après le chargement | aucun |
|
| Après le chargement | aucun |
|
| Après le chargement | aucun | |
| Après le chargement, puis toutes les heures | aucun |
|
| Après le chargement | aucun |
|
| Après le chargement | aucun |
|
| Après le chargement, deux fois pour chaque quête en cours | aucun |
|
| Après le chargement, une fois par jour | aucun |
|
| L’étape | aucun | |
| L’étape | aucun | |
| L’étape | aucun | |
| Les étapes | aucun | |
| Après le chargement | aucun |
|
| Première carte de la connexion | aucun | |
| Chaque | aucun |
« oui » signifie un uid qui lui est propre, comme le client l’envoie. GuildWeeklyNextResetRequest n’en a pas, bien que le serveur y réponde avec une Response : cette réponse revient elle aussi avec l’uid -1.
Tous les messages que base lit
Message | Ce que base en fait |
|---|---|
| Termine l’identification |
| Garde |
| Répond à la preuve d’identité |
| Écrit la position dans la file d’attente dans la Console |
| Choisit le personnage |
| Le serveur a choisi le personnage qu’un combat attend : base lui répond et joue ce personnage |
| Garde les identifiants des quêtes en cours, dans |
| Terminent la sélection du personnage |
| Maintien de la connexion |
| Planifie le prochain jeton de boutique |
| Annonce la carte |
| Annonce une carte instanciée |
| Résout |
| L’étape |
| L’étape |
| L’étape |
| Avertit quand la session n’est pas certifiée |
| Avertit quand le serveur met fin à la session |
| Avertit : base n’y répond pas |
Lignes de console et erreurs
Les lignes que base écrit dans la Console du bot à propos de la connexion. Elles restent en anglais, telles que base les écrit. Les lignes propres à un plan sont dans Les lignes de console d’un plan.
Niveau | Ligne | Signification |
|---|---|---|
info |
| Le serveur a sélectionné le personnage |
info |
|
|
info |
| Le serveur est plein |
warn |
| Un script s’est arrêté entre le ticket et sa réponse. Voir Mémoire. |
warn |
| Bon à savoir ; base continue |
warn |
| Le serveur s’apprête à déconnecter le bot |
warn |
|
|
warn |
| Un envoi a échoué. Base continue. |
warn |
| Un gestionnaire a échoué. Base continue. |
error | La raison avec laquelle | Voir ci-dessous |
Ce avec quoi connect() et les étapes se rejettent :
Erreur | Cause |
|---|---|
|
|
| La liste est vide |
| Le réglage |
|
|
| Aucun |
| Le serveur n’a pas répondu au ticket en 30 s, à la sélection en 30 s, ou au chargement en 60 s |
Délais et bruit
Deux sortes de délais se trouvent dans une capture, et base les traite à part :
Moins de 100 ms : les trames du client. Le client répond à sa trame suivante, donc une réponse arrive 6 à 55 ms après son événement, selon la charge du client. Le serveur le voit à travers l’aller-retour du réseau, 44 ms dans ces captures et plus variable que cela. Base les envoie directement et ne les imite pas.
100 ms et plus : le client qui charge, ou une personne. La seconde qui suit le chargement du personnage, le chargement de la carte, la pause avant de choisir un personnage. Ceux-là sont gardés. Ceux qui ont été mesurés reçoivent un bruit de ±15 % autour de leur valeur, puisqu’un client ne prend jamais deux fois le même temps. Ceux qu’une personne décide, comme regarder la liste des personnages, sont tirés uniformément entre deux bornes.
Ce que le serveur voit exactement, c’est l’ordre des messages, puisque TCP le conserve. Base envoie donc dans l’ordre du client, qu’Asterobot conserve : il écrit les trames d’un script dans l’ordre où elles ont été envoyées.
Toutes les attentes passent par sleep() d’asterobot:timers, et chacune est annulée quand le script s’arrête.
Ce qu’il faut à base dans Asterobot
La mémoire du bot,
memoryd’asterobot:bot, etsession.connectionId.send(..., { correlated: true }), pour les quatre requêtes qui portent un uid et ne reçoivent pas de réponse.asterobot:verification, pour la preuve d’identité.asterobot:movementavecchangeMap(),useElement(), les voyages et les fonctions qui trouvent les cartes, que base réexporte à côté demove(). Un Asterobot qui ne les a pas ne peut pas charger base du tout, puisque l’import échoue.asterobot:character, l’état du personnage, pour les conditions sur l’état du jeu. Un Asterobot qui ne l’a pas ne peut pas non plus charger base.onStopRequest()d’asterobot:bot, pour qu’un plan se termine proprement quand une plage du planning se termine.asterobot:fight, lesmonstersdes groupes de monstres danscurrentMap(),record()d’asterobot:gamedataetdistance()d’asterobot:pathfinding, pour combattre. Un Asterobot qui n’a pasasterobot:fightne peut pas charger base.find()d’asterobot:gamedata, et les tablesskillsetmaps__interactive_elementsdes données du jeu, pour récolter.items()d’asterobot:character, pour manger et l’équipement, et la vie qui suit la régénération, pour se reposer. Un Asterobot sansitems()ne peut pas charger base.Les fichiers
fightprofiles/d’un paquet, pour les profils de combat. Un Asterobot plus ancien laisse ce dossier de côté et n’en liste aucun.
Base n’a pas de asterobot.json, donc pas encore de compatibleVersion : une version de développement d’Asterobot se déclare en 0.0.0 et refuserait toute plage. Une fois qu’une version publiée aura ces fonctionnalités, le paquet publié la nommera.
Différences connues avec le vrai client
Les canaux de discussion ne sont pas réglés. Un client dont les canaux ont été modifiés dans ses options envoie
SubscribeMultipleChannelRequestaprèsSetCommunityChannelRequest, avec l’ensemble complet : dans une capture, Ventes, Recherche, Arène, Communauté et Événement coupés, tous les autres canaux actifs. Une autre capture, d’un client laissé tel quel, n’en a aucun. Les options d’un bot sont celles par défaut, donc base n’en envoie aucun.Les étapes de quête ne sont demandées qu’à l’entrée dans le monde. Le client les redemande aussi quand l’étape d’une quête change. Base ne suit plus les quêtes ensuite.
Les prix moyens sont demandés une fois par jour UTC, et Asterobot oublie qu’il les a demandés quand il redémarre. Le client s’en souvient d’un redémarrage à l’autre.
FileCheckEventne reçoit pas de réponse. Il n’a jamais été vu dans une capture.Le rythme des pings est statistique : environ la moitié des battements de 5 secondes, là où un client envoie un ping sur les battements où une personne l’a vraiment touché.
Une seule version de base par script. Deux paquets qui fixent des versions différentes de base les chargent toutes les deux, et les deux répondent au serveur. Fixez la même version partout.
Améliorer base à partir d’une nouvelle capture
Le premier rôle de base est tout ce que le client Dofus fait de lui-même, pour qu’aucun script qui l’utilise n’ait jamais à penser aux pings, aux contrôles, aux relances ou aux cartes. Chaque nouvelle capture peut l’enrichir, en suivant les mêmes règles :
Seulement ce que le client fait toujours : une réponse qu’il envoie à chaque fois à un message du serveur, ou un message qu’il envoie de lui-même à un moment fixe. Ce qu’une personne décide, comme marcher, combattre ou discuter, n’en fait pas partie : c’est le rôle des plans et des fonctions de gameplay.
Seulement sur un bot Full socket. Sur un bot MITM, le client Dofus le fait déjà, donc chaque gestionnaire rend la main tout de suite quand
session.sharedest vrai.Enregistré dans
connect(), avant l’identification, puisque le serveur peut parler juste après avoir accepté le ticket.L’ordre du client, pas ses trames.
Sous 100 ms, un délai est la fréquence de trame du client : envoyez directement.
Envoyez dans l’ordre du client : Asterobot écrit les trames d’un script dans l’ordre où elles ont été envoyées.
À partir de 100 ms, un délai est le client qui charge ou une personne qui décide : gardez-le. Utilisez une valeur mesurée avec ±15 % de bruit, ou un tirage uniforme entre deux bornes quand les captures montrent une plage.
Ce qui doit survivre à un redémarrage va dans l’état de la connexion (
lib/state.js). Quand envoyer deux fois serait une erreur, enregistrez le fait avant d’envoyer.Rien ne s’échappe d’un gestionnaire. Utilisez
guarded()etsendQuietly(): une erreur qui s’échappe d’un gestionnaire arrête tout le script, celui de base comme celui qui l’utilise.La preuve va dans ce README, à côté du comportement qu’elle prouve : l’extrait de capture, avec ses heures et ses uids.
Fichiers
base/
├── README.md le README, en anglais
├── README.fr.md ce fichier
├── changelog.md ce qui a changé à chaque version
├── index.js exports, réglages, les primitives du runner, démarrage automatique et fonction d'entrée
├── fightprofiles/ les profils de combat que base fournit, un fichier JSON chacun
└── lib/
├── connect.js connect() : tout, dans l'ordre
├── identify.js l'identification
├── character.js la sélection du personnage
├── world.js l'entrée dans le monde, le suivi des cartes, le jeton de boutique
├── verification.js la preuve d'identité
├── keepalive.js les deux relances, latency(), les pings
├── watch.js enregistre les étapes clés de la session, avertissements de console
├── state.js l'état de la connexion dans la mémoire du bot
├── send.js envoyer depuis les gestionnaires sans arrêter le script
├── timing.js délais mesurés avec bruit
├── conditions.js les conditions sur l'état du jeu
├── fight.js combattre : les étapes et les conditions de combat
├── gather.js récolter : l'étape et la condition de récolte
├── recover.js se reposer et manger : les deux étapes
├── equip.js l'équipement : les deux étapes et la condition
└── studio/ le runner, qui joue les plans
├── runner.js run() et stop(), avec les primitives que lui passe index.js
├── plan.js plan(), et la façon dont les lignes nomment les blocs
├── check.js la vérification d'un plan avant de bouger
├── args.js les règles des arguments
├── call.js l'étape ou la condition d'un paquet : arguments, réglages, contexte
├── circuit.js tours, arrêts, cartes, étapes et interruptions
├── steps.js les étapes
├── conditions.js les conditions
├── places.js les cartes d'un arrêt, de la plus proche à la plus lointaine
├── waiting.js les cartes où il n'y a rien à faire pour l'instant
└── failures.js onFailure
Le runner reçoit tout ce qu’il lui faut d’Asterobot en arguments, donc ses tests, dans test/base à côté de ce dossier, le font tourner sous Node avec des imitations : node --test test/base. Ils sont en dehors du paquet parce qu’Asterobot installe chacun de ses fichiers .js.
Nouveautés de la version 0.0.3
libérés
Ajouté
travel(), qui permet au personnage de se rendre sur n’importe quelle carte, quelle que soit la distance, en passant de carte en carte ;route(), qui indique le chemin qu’il emprunterait ;stopTravel()ettrip(); ainsi que les fonctions permettant de trouver une carte où se rendre :findMaps(),mapAt(),mapInfo(),nearest(),nearestWith(),mapsWithin()etreachable(). Ces fonctions sont propres àasterobot:movement, mais sont mises à disposition parbaseafin qu’un package n’ait besoin que d’un seul import. Elles nécessitent une version suffisamment récente d’Asterobot ; une version plus ancienne ne peut pas charger cette version debase.Exécution des plans créés dans le Studio d’Asteroboard : les lieux sur la carte du monde, le parcours du bot entre ces lieux (en boucle, dans un seul sens, en commençant par le plus proche ou jusqu’à ce qu’un événement se produise), ainsi que ses actions à chaque étape, avec des attentes, des conditions, des nouvelles tentatives et des interruptions, par exemple « aller à la banque lorsque les sacs sont pleins ». Un package exécute un plan avec
run(plan)et l’arrête avecstop().Avant que le bot ne fasse le moindre pas,
basevérifie l’intégralité du plan et répertorie toutes les erreurs détectées en une seule fois. Pendant son exécution, la Console du bot indique où il se trouve, et chaque erreur précise l’arrêt ou l’étape dont elle provient.Chaque arrêt et chaque étape d’un plan peut, selon les instructions du plan, continuer malgré une erreur, l’ignorer ou réessayer. Sans instruction particulière, une erreur interrompt le plan afin qu’elle ne passe jamais inaperçue.
Les paramètres d’un plan apparaissent dans l’onglet Settings du bot dès lors que le point d’entrée du package les réexporte. Toute modification prend effet à partir de l’étape suivante.
Un plan s’arrête proprement à la fin du créneau horaire prévu dans une programmation : l’étape en cours se termine, puis le bot s’arrête.
Accès à l’état du personnage, aussi bien pour les plans que pour les scripts : son niveau, ses kamas, ses points de vie, le taux de remplissage de ses sacs, la présence ou non d’un combat en cours, les objets qu’il transporte, ses niveaux de métier, ainsi que la carte, la sous-zone ou la zone dans laquelle il se trouve. Une information que le jeu n’a pas encore communiquée n’est jamais devinée : l’étape ou l’arrêt du plan échoue à la place, et la stratégie qui lui est propre s’applique. Cette fonctionnalité nécessite une version d’Asterobot suffisamment récente pour suivre l’état du personnage ; une version plus ancienne ne peut pas charger cette version de
base.
Corrigé
Sur un PC lent, un changement de carte pouvait entraîner deux demandes de chargement du contenu de la nouvelle carte : une de
baseet une d’Asterobot. Désormais,baseindique à Asterobot qu’il se charge lui-même de demander toutes les cartes. Ainsi, seulbaseeffectue ces demandes, quelle que soit leur durée. Cette correction nécessite une version d’Asterobot suffisamment récente pour disposer deanswerMapLoads().
Rejoignez la conversation
Vous pouvez publier maintenant et vous inscrire plus tard. Si vous avez un compte, connectez-vous maintenant pour publier avec votre compte.