Aller au contenu
Afficher dans l'application

Une meilleure façon de naviguer. En savoir plus.

Asterobot

Une application plein écran sur votre écran d'accueil avec notifications push, badges et plus encore.

Pour installer cette application sur iOS et iPadOS
  1. Appuyez sur Icône de partage dans Safari
  2. Faites défiler le menu et appuyez sur Ajouter à l'écran d'accueil.
  3. Appuyez sur Ajouter dans le coin supérieur droit.
Pour installer cette application sur Android
  1. Appuyez sur le menu à trois points (⋮) dans le coin supérieur droit du navigateur.
  2. Appuyez sur Ajouter à l'écran d'accueil ou Installer l'application.
  3. Confirmez en appuyant sur Installer.

Base 0.1.0

(0 notes)

@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 de my-bot ne 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 dans dependencies sans 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.connectionId vaut 0n (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.shared avant 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

IdentificationRequest { ticketKey, languageCode }

AuthenticationTicketAcceptedEvent, puis une rafale : TimeEvent, ServerSettingsEvent, informations de compte et de confiance, ServerSessionReadyEvent

Tout de suite

CharacterListRequest

Tout de suite

ClientChallengeInitRequest { challengeKey }, avec un uid

QueueStatusEvent, tant que dure la file d’attente du serveur, puis CharacterListEvent

1,5 à 3,5 s plus tard, comme une personne qui clique

CharacterSelectionRequest { characterId }

CharacterSelectionEvent, puis CharacterLoadingCompleteEvent. C’est t0 ci-dessous.

Tout de suite, à chaque fois

SequenceNumberRequest { number }

SequenceNumberEvent

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

ContextCreationRequest, puis le jeton de boutique, l’avertissement de connexion de guilde désactivé, SetCommunityChannelRequest { community }

+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

MapCurrentEvent { mapId }

1,2 à 2,4 s plus tard, sur la première carte

AnomalySubareaInformationRequest, ContextReadyRequest { mapId }, MapInformationRequest { mapId }

MapComplementaryInformationEvent : connect() se résout

Ensuite, tant que le script tourne :

Tous les

Base

Serveur

10 s environ en moyenne

PingRequest { quiet: true }

PongEvent

4 minutes

SequenceNumberRequest { number }, puis BasicLatencyStatsRequest { latency } avec un uid, les deux tout de suite

SequenceNumberEvent et BasicLatencyStatsEvent, ensemble

De temps en temps, à plusieurs minutes d’intervalle

ClientIdRequest { id }, puis ClientChallengeProofRequest { proof }, chacun tout de suite, les deux avec un uid

ServerVerificationEvent, puis ServerChallengeEvent { value }, la valeur étant parfois absente

Nouvelle carte

ContextReadyRequest + MapInformationRequest, 0,4 à 1,2 s plus tard

MapCurrentEvent ou MapCurrentInstanceEvent

Une heure après chaque clé

BakApiTokenRequest

BakApiKeyEvent

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

connect()

Promesse de la carte

Attend que le personnage soit sur sa carte. Base l’exécute déjà pendant son chargement.

latency()

nombre

La latence que le bot déclare au serveur, en millisecondes.

move()

Promesse de la cellule d’arrivée

Fait marcher le personnage vers une cellule de sa carte : celle d’asterobot:movement, réexportée. Sa référence est dans le guide du développeur d’Asterobot.

changeMap()

Promesse de la carte et de la cellule

Emmène le personnage sur la carte suivante par un côté : celle d’asterobot:movement, réexportée. Base demande le contenu de la nouvelle carte, comme pour chaque carte.

useElement()

Promesse de ce qu’a fait l’utilisation

Utilise une porte, un zaap, une ressource ou une sortie de la carte : celle d’asterobot:movement, réexportée.

travel()

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’asterobot:movement, réexportée. Base demande le contenu de chaque nouvelle carte, comme pour chaque carte.

route(), stopTravel(), trip()

Le chemin que prendrait travel(), l’arrêt d’un voyage, et le voyage où il en est : celles d’asterobot:movement, réexportées.

findMaps(), mapAt(), mapInfo(), nearest(), nearestWith(), mapsWithin(), reachable()

Les fonctions qui trouvent les cartes où voyager : celles d’asterobot:movement, réexportées.

run(), stop(), plan()

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.

levelAtLeast(), kamasAtLeast(), lifeBelow(), bagsAbove(), inFight(), hasItem(), jobLevelAtLeast(), onMap(), inSubarea(), inArea()

booléen, ou promesse d’un booléen

L’état du personnage, sous forme de questions oui/non. conditions les déclare pour le Studio : voir Conditions sur l’état du jeu.

gather(), harvestsAtLeast()

Une promesse, un booléen

Récolter les ressources de la carte, et combien de récoltes ont été faites. steps et conditions les déclarent pour le Studio : voir Récolter.

fightMonsters(), fightSettings(), diedInLastFight(), fightsAtLeast()

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. steps et conditions les déclarent pour le Studio : voir Combattre.

rest(), eat()

Une promesse

Récupérer de la vie, en s’asseyant et en mangeant ce que contiennent les sacs. steps les déclare pour le Studio : voir Se reposer et manger.

equip(), unequip(), wears()

Une promesse, une promesse, un booléen

Mettre des objets, les retirer, et savoir si le personnage en porte un. steps et conditions les déclarent pour le Studio : voir L’équipement.

steps, conditions

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 :

  1. Il enregistre les étapes clés de la session dans la mémoire du bot (voir Mémoire).

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

  3. 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 IdentificationRequest sur 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.Round du 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

character

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.

community

server, french, english, spanish, portuguese, german

server

La communauté de discussion que rejoint SetCommunityChannelRequest : 0 à 4 dans cet ordre. server envoie la community de ServerSettingsEvent, ou 0 s’il n’en a pas.

ping

active, idle

active

active envoie des pings comme une personne qui utilise le client, ce qui donne au serveur une vraie latence. idle n’en envoie jamais, comme un client que personne ne touche. Il continue de déclarer la dernière latence mesurée, ou 0 s’il n’a jamais envoyé de ping.

bringUp

faithful, minimal

faithful

faithful envoie tout ce que le client envoie après le chargement. minimal n’envoie que ContextCreationRequest, le seul message nécessaire pour atteindre la carte, et aucun jeton de boutique horaire.

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

run(plan, options?)

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). options est réservé : rien ne le lit encore.

stop()

booléen

Demande au plan en cours de s’arrêter proprement. Dit s’il y en avait un.

plan(data, refs)

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) :

  1. Attend le monde, avec connect().

  2. 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 un ref sans valeur par défaut), le nom et la valeur par défaut de chaque réglage, chaque $setting et 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.

  3. 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 avec base: a plan is already running. Les plans qu’un plan joue lui-même, pour une interruption ou une étape plan, ne comptent pas.

  • S’arrêter proprement. Un arrêt vient de stop(), d’une étape stop, 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 voit context.stopping passer à vrai et peut finir plus tôt), un wait ou un waitUntil se termine tout de suite, un voyage s’arrête à la fin de son tronçon, et run() 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 un void 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

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

  2. Si son when est faux, il est sauté pour ce tour.

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

  4. 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 : son when, l’étape, son onFailure si elle échoue, et les interruptions de nouveau avant l’étape suivante. Quand une étape a emmené le bot ailleurs, comme une étape plan, il revient sur la carte avant l’étape suivante ; après la dernière, il continue directement.

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

  6. À la fin d’un tour, once termine le circuit, et forever et until commencent 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 :

  • resume revient sur la carte où était le circuit, et reprend avec l’étape qu’il allait jouer ;

  • stop termine 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 int d’au moins 1 commencerait à 0), ou quand c’est une valeur seule avec un ref (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 :

onFailure

Alors

absent, ou { "kind": "stop" }

Le plan s’arrête avec l’erreur

{ "kind": "skip" }

Une ligne de console, puis l’étape suivante (pour une étape) ou l’arrêt suivant (pour un arrêt)

{ "kind": "retry", "times": n, "wait": s, "then": "stop" \| "skip" }

Réessayé jusqu’à n fois (1 à 10), à s secondes d’intervalle (5 par défaut). S’il échoue encore, then (stop par défaut). Un arrêt est réessayé en entier : sa condition, son lieu et toutes ses cartes.

Ce qui fait échouer quoi :

Ce qui arrive

Ce qui échoue

L’étape d’un paquet lève une erreur ou se rejette, un waitUntil dépasse son délai, le plan d’une étape plan échoue

L’étape

Le when d’une étape, ou la condition d’un waitUntil, échoue

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 when échoue

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 resume sa condition est encore vraie après ce plan, ou repeat.until échoue

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

base: <plan> › <n> <stop>

Un arrêt arrive : son numéro sur le circuit, puis son libellé, ou son lieu s’il n’en a pas

info

base: <plan> › <n> <stop> › <step>

Une étape commence : une fonction par son libellé, une étape logique par ce qu’elle fait (wait 5–15 s, wait until, play plans/bank.js, stop)

info

base: <plan> › <n> <stop> › <message>

Une étape log

info

base: <plan> › interrupt <label>: playing <path>

La condition d’une interruption est vraie

warn

base: <block> failed, trying again in <s> s (<i> of <n>): <cause>

Une étape ou un arrêt est réessayé

warn

base: <block> failed, skipped: <cause>

Une étape ou un arrêt est sauté, avec <n> times après failed quand il a d’abord été réessayé

warn

base: <plan> › <n> <stop> (<id>): no trip reaches map <id>, skipped

Les cartes d’un lieu qu’aucun voyage n’atteint, toutes sur une ligne

warn

base: <plan>: settings <name>, <name> aren't among the package's settings, so their defaults are used: re-export the plan's settings from the package's entry

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 (setting <name> isn't one of the package's settings, so its default is used pour un seul)

error

base: <block> failed: <cause>

Le plan s’arrête

error

base: <plan> can't be run: puis - <problème>, une ligne chacun

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 }
  • plan est le chemin du plan joué dans son paquet, null quand son fichier ne le donne pas.

  • stop et step sont des identifiants de blocs ; step vaut null à 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 que run() 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’un call nomme, 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 : values d’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

format

1. Un runner refuse un format qu’il ne connaît pas

runner

Le paquet dont plan() et run() jouent le plan : @asterobot:base

label

Facultatif. Affiché dans le Studio et dans les lignes de console

settings

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)

circuit.order

drawn : les arrêts dans leur ordre. nearest : l’arrêt le plus proche qui a encore du travail

circuit.repeat

{ "kind": "once" }, { "kind": "forever" }, ou { "kind": "until", "until": <condition> }

circuit.stops

Les arrêts, dans l’ordre où ils ont été dessinés. Au moins un

interrupts

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

order

repeat

Boucle

drawn

forever

Aller simple

drawn

once

Balayage

nearest

forever

Jusqu’à

drawn

until

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

id, label

Le libellé est facultatif

place

Où est l’arrêt (voir Les lieux)

go

Comment le bot s’y rend. Absent vaut { "kind": "travel" }, la seule sorte du format 1

when

Condition facultative. Fausse quand l’arrêt arrive : l’arrêt est sauté pour ce tour

onFailure

Ce qui se passe quand l’arrêt échoue (voir Échecs)

steps

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

kind

Champs

Cartes

map

mapId

Cette carte

maps

mapIds, au moins une

Ces cartes : une zone peinte dans le Studio

subarea

id

findMaps({ subArea: id }), quand l’arrêt arrive

area

id

findMaps({ area: id }), quand l’arrêt arrive

query

query : les champs de la requête de findMaps() (subArea, area, monster, resource, zaap, x, y, worldMap)

findMaps(query), quand l’arrêt arrive

  • Un lieu est choisi. Un map nomme son mapId, un subarea ou un area son id : 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. Un zaap seul, 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’id d’un subarea ou d’un area sont des nombres entiers qui tiennent sur 32 bits : de 0 à 2147483647, et à partir de -2147483648 pour x et y. Les identifiants de carte sont des nombres JSON, entiers et au-dessus de 0 : ceux de Dofus y tiennent largement.

  • Un lieu subarea, area ou query suit 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.

kind

Champs

Fait

Échoue quand

call

package, step, args

Joue une étape que donne un paquet. Un argument absent prend sa valeur par défaut déclarée ; un argument peut valoir { "$setting": "<nom>" }

La fonction lève une erreur ou se rejette

wait

seconds (0 ou plus), upTo (facultatif, au moins seconds)

Attend seconds, ou un temps tiré au hasard entre seconds et upTo

Jamais

waitUntil

until (une condition), timeout (en secondes, facultatif)

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

plan

plan (un chemin dans le paquet)

Joue ce plan ici, puis continue

Le plan échoue

log

message

Écrit le message dans la Console

Jamais

stop

Termine tout le run(), plans imbriqués compris, comme une fin normale

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

kind

Champs

Vraie quand

call

id, package, condition, args

La condition du paquet le dit. Les arguments comme pour une étape call

all

of : des conditions, au moins une

Chacune l’est

any

of : des conditions, au moins une

Au moins une l’est

not

of : une condition

Elle ne l’est pas

elapsed

minutes (0 ou plus)

Ce temps s’est écoulé depuis le début du run() de plus haut niveau

laps

count (1 ou plus)

Le circuit a été fait au moins ce nombre de fois

time

from, to ("HH:MM")

L’horloge de la machine qui fait tourner Asterobot indique de from, compris, à to, exclu. Un from après son to passe minuit

chance

percent (0 à 100)

Un tirage au hasard, nouveau à chaque vérification, tombe sous percent

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

id, label

Comme pour un arrêt

when

Une condition

run

{ "plan": "<chemin dans le paquet>" } : le plan à jouer

then

resume : retour là où en était le circuit. stop : le plan se termine

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

levelAtLeast

level (int, 1 ou plus)

Le niveau du personnage vaut au moins level

kamasAtLeast

kamas (int, 0 ou plus)

Il porte au moins kamas kamas

lifeBelow

percent (int, 1 à 100)

Sa vie est sous percent de son maximum

bagsAbove

percent (int, 1 à 100)

Ses sacs sont remplis à plus de percent de ce qu’ils peuvent porter

inFight

Un combat est en cours

hasItem

item (int, un objet), count (int, 1 ou plus, 1 par défaut)

Il porte au moins count exemplaires de item, équipés ou non

jobLevelAtLeast

job (int, un métier), level (int, 1 ou plus)

Ce métier vaut au moins level. Un métier qu’il n’a pas est au niveau 0

onMap

map (int, une carte)

Il se trouve sur cette carte

inSubarea

subarea (int, une sous-zone)

Il se trouve dans cette sous-zone

inArea

area (int, une 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 :

  1. Un combat en cours, comme l’agression d’un monstre de la carte, est attendu d’abord, joué par le module de combat, comme fightMonsters en attend un. La récolte continue ensuite.

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

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

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

resources

array d’int, des objets

aucun

Ce qui donne ces objets, comme 289, le Blé. Avec jobs, ce que l’un ou l’autre demande

jobs

array d’int, des métiers

aucun

Chaque ressource de ces métiers. Ni resources ni jobs : chaque ressource de la carte

avoid

array d’int, des objets

aucun

Jamais ce qui donne ces objets

myLevel

bool

true

Seulement ce que le niveau du personnage dans le métier permet. Le serveur refuserait le reste

order

nearest, level

nearest

La plus proche d’abord, ou la ressource du plus haut niveau d’abord, la plus proche à égalité

maxHarvests

int, 0 ou plus

0

Termine l’étape après ce nombre de récoltes sur la carte. 0 : tout ce qui convient

bagsAbove

int, 0 à 100

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

harvestsAtLeast

count (int, 1 ou plus)

Le personnage a fait au moins count récoltes avec gather depuis le démarrage du script

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 :

  1. Un combat en cours, celui de l’étape ou un autre venu autrement, comme une agression, est attendu d’abord.

  2. Elle choisit le groupe le plus proche que ses arguments laissent passer, à la distance en cellules depuis le personnage.

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

  4. Elle marche jusqu’à la cellule du groupe et envoie AttackMonsterRequest avec 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.

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

monsters

array d’int, des monstres

aucun

Avec au moins un de ces monstres. Aucun : n’importe quel groupe

avoid

array d’int, des monstres

aucun

Sans aucun de ces monstres

races

array d’int, des races de monstres

aucune

Avec un monstre de l’une de ces races. Aucune : n’importe quelle race

minLevel

int, 0 ou plus

0

Dont le niveau, la somme des niveaux de ses monstres comme le jeu l’affiche, vaut au moins celui-ci

maxLevel

int, 0 ou plus

0

Dont le niveau vaut au plus celui-ci. 0 : sans limite

maxMonsters

int, 0 à 8

0

Avec au plus ce nombre de monstres. 0 : sans limite

maxMonsterLevel

int, 0 ou plus

0

Sans monstre au-dessus de ce niveau. 0 : sans limite

archmonsters

avoid, allow, only

avoid

Sans archimonstre, avec ou sans, ou seulement avec un. Un archimonstre est un monstre que les données du jeu marquent ainsi, le isMiniBoss du client

maxFights

int, 0 ou plus

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

module

keep, on, off

keep

L’interrupteur du module. Sur un bot MITM, l’interrupteur reste celui de la personne

profile

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

speed

keep, slow, normal, turbo

keep

La vitesse

Les conditions de combat

Condition

Arguments

Vraie quand

diedInLastFight

Le personnage est mort dans son dernier combat. Fausse avant le premier

fightsAtLeast

count (int, 1 ou plus)

Le personnage a livré au moins count combats depuis le démarrage du script

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 :

  1. Rien à faire quand la vie y est déjà.

  2. Avec eat, il mange d’abord, comme eat.

  3. 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 avec EmotePlayEvent. 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.

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

upTo

int, de 1 à 100

100

Se relève dès que la vie atteint cette part de son maximum

eat

bool

false

Mange d’abord ce que contiennent les sacs, ce qui est plus rapide

food

array de int, des objets

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 :

  1. 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 food quand ils sont donnés. Celle qui soigne le plus sans dépasser ce qui manque passe d’abord, sinon celle qui soigne le moins.

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

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

wears

item (int, un objet)

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

careful

Prudent : ne finit jamais un tour là où les ennemis peuvent le blesser gravement, et se cache dès que sa vie est basse

risk 15, poids safety 2 ; sous 35 % de vie, se replie à couvert

all-in

Tout pour l’attaque : inflige le plus de dégâts possible et ignore le danger, mort comprise

risk 100 ; poids damage 1,2, kill 1,5, safety 0,25

melee-charge

Se place près, fonce sur l’ennemi le plus proche et frappe au contact

range melee, placement close, positioning charge

ranged-kite

Se place loin, frappe à distance, puis sort de la portée des ennemis

range ranged, placement far, positioning kite, risk 40

ranged-hide

Se place loin, frappe à distance, puis finit son tour hors de la vue des ennemis

range ranged, placement far, positioning hide, risk 30

hold-position

Ne bouge que pour atteindre un lancer, pour garder taclés les ennemis à côté de lui

positioning hold, risk 60

finish-the-weakest

Achève d’abord l’ennemi qui a le moins de vie

focus lowestLife, poids kill 2

kill-the-most-dangerous

Tue d’abord l’ennemi qui peut le blesser le plus là où il se tient

focus mostDangerous, poids kill 1,5

healer

Soigneur : soigne d’abord l’allié le plus faible, puis lui-même, et frappe quand personne n’a besoin de soin

role healer, allies protect ; un allié sous 60 % est soigné d’abord, puis lui-même sous 40 %

bodyguard

Garde du corps : protège ses alliés, boucliers et boosts pour le plus faible, et se place pour les abriter

allies protect, risk 70, poids buff 1,5 ; un allié sous 50 % est boosté d’abord

support

Affaiblit les ennemis et booste ses alliés avant de frapper

role support, allies help, risk 40

tank

Passe devant, encaisse les coups pour l’équipe et tacle les ennemis au contact

role tank, range melee, placement close, positioning charge, allies protect, risk 85, poids control 2

crowd-control

Contrôle : pousse les ennemis, leur retire PA et PM et leur donne des états avant de les frapper

risk 50, poids control 3

summoner

Invocateur : invoque dès qu’il peut et reste derrière ses invocations, hors de la vue des ennemis

positioning hide, risk 35, poids summon 2,5

trapper

Poseur de pièges : pose pièges et glyphes là où les ennemis vont passer, puis reste hors de leur portée

range mid, positioning kite, risk 40, poids mark 2,5

earth-strength

Préfère ses sorts de terre, pour un personnage force

elements earth

fire-intelligence

Préfère ses sorts de feu, pour un personnage intelligence

elements fire

water-chance

Préfère ses sorts d’eau, pour un personnage chance

elements water

air-agility

Préfère ses sorts d’air, pour un personnage agilité

elements air

weapon-fighter

Frappe avec son arme dès qu’il peut, au contact

weapon.use prefer, range melee, placement close, positioning charge

spells-only

Ne frappe jamais avec l’arme ni à mains nues : ses PA vont à ses sorts

weapon.use never

team-player

Joueur d’équipe : ne blesse jamais un allié avec ses zones, et soigne et booste ses alliés dès que ça aide

allies protect ; poids allyHarm 10, heal 1,5, buff 1,5

carried

Un bas niveau dans un groupe fort : se place loin, reste hors de danger et de vue, et ne frappe que sans risque

placement far, positioning hide, risk 0, poids safety 3 ; sous 50 % de vie, se replie à couvert

boss-fight

Pour les boss et les donjons : prudent, tue d’abord l’ennemi le plus dangereux et garde le groupe à l’abri

focus mostDangerous, allies protect, risk 25 ; sous 40 % de vie, se replie à couvert

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, avec setProfile("@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’extends et 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 ServerVerificationEvent avant 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 ServerChallengeEvent ne 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  #11
    
  • Les 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

c3

aucun : le client le calcule

#10, #11

se vérifie avec le défi qu’Asterobot calcule

2

c7

une valeur de 255 bits venue du serveur

#12, #13

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 :

  • number est 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

#5

(contrôle)

00:14:08

ClientIdRequest #10, ClientChallengeProofRequest #11

9

00:21:26

#13

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 PongEvent qui répond à un ping en cours ajoute son aller-retour aux échantillons dont latency() 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 minimal

t0 + 1085 ms

ContactWarnOnAchievementCompleteSetRequest, FriendSetWarnOnConnectionRequest, FriendSetWarnOnLevelGainRequest, ContactWarnOnPermanentDeathSetRequest, FriendSetStatusShareRequest, tous vides : le client rejoue ses options sociales, et un bot n’en a aucune

Attend pareil, n’envoie rien

ensuite

GuildInformationRequest { informationType: INFO_HOUSES }, GuildInformationRequest { informationType: INFO_GENERAL }, GuildWeeklyNextResetRequest

Sauté

t0 + 1349 ms

ContextCreationRequest

Envoyé

ensuite

BakApiTokenRequest, GuildMemberWarnOnConnectionStopRequest, SetCommunityChannelRequest { community }

Sauté

+231 ms

QuestStepInformationRequest { questId } pour chaque quête en cours, deux fois : un passage par identifiant, puis le même passage à nouveau

Sauté

ensuite

ObjectAveragePricesRequest, seulement s’il n’a pas encore été demandé aujourd’hui (UTC)

Sauté

ensuite

FriendListRequest, AcquaintanceListRequest, SpouseInformationRequest, GuildInformationRequest { informationType: INFO_HOUSES }

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, 1631 avec une quête en cours, et 1631, 1637, 1631, 1637 avec deux.

  • Seul ContextCreationRequest est 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 nouveau BakApiTokenRequest une heure après chaque BakApiKeyEvent, 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 :

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

  2. Sur la première carte seulement, il envoie AnomalySubareaInformationRequest.

  3. Il envoie ContextReadyRequest { mapId }, puis MapInformationRequest { mapId }, l’un après l’autre, avec l’identifiant qu’a donné MapCurrentEvent ou MapCurrentInstanceEvent.

  4. Le serveur répond avec MapComplementaryInformationEvent, un aller-retour plus tard, et le suivi de carte d’Asterobot place le personnage : currentMap() a son cellId.

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

@asterobot:base/connection

L’état de la connexion de jeu actuelle, à côté de session.connectionId. Une valeur enregistrée pour une autre connexion, d’avant une reconnexion du bot, est ignorée.

@asterobot:base/latency

{ samples } : les 50 derniers allers-retours de ping, en millisecondes.

@asterobot:base/prices

{ day } : le jour UTC du dernier ObjectAveragePricesRequest.

@asterobot:base/plan

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

id

Toujours : le session.connectionId auquel il appartient

identified

IdentificationRequest va être envoyé. Il est défini avant l’envoi, donc un script arrêté entre les deux n’en envoie jamais un second.

accepted, refused

Le serveur a répondu au ticket

serverCommunity

ServerSettingsEvent est arrivé

characterId

Le serveur a sélectionné un personnage

activeQuests

QuestsEvent est arrivé : les identifiants des quêtes en cours

loadedAt

CharacterLoadingCompleteEvent est arrivé (Date.now())

bringUp

Les requêtes d’après le chargement ont commencé à partir

contextCreated

ContextCreationRequest va être envoyé

anomalyAsked

AnomalySubareaInformationRequest va être envoyé

pendingMapId

La dernière carte nommée par MapCurrentEvent ou MapCurrentInstanceEvent

readyMapId

La dernière carte pour laquelle ContextReadyRequest est parti

sequence

Le nombre de SequenceNumberEvent arrivés

bakKeyAt

Le dernier BakApiKeyEvent est arrivé

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 IdentificationRequest

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 ContextCreationRequest

Saute les groupes sociaux et de guilde, puis envoie ContextCreationRequest et tout ce qui suit

Après ContextCreationRequest

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 MapCurrentEvent arrive, 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 ni cellId. Pour agir dessus, attendez son MapComplementaryInformationEvent, comme ci-dessus.

  • Les identifiants sont des BigInt. Comparez-les avec des BigInt, comme mapId === 153879301n. findPath() prend un identifiant de carte en BigInt, et record() d’asterobot:gamedata non : passez-lui Number(mapId).

  • Les cartes instanciées sont annoncées avec MapCurrentInstanceEvent au lieu de MapCurrentEvent. É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

IdentificationRequest

Une fois par connexion

aucun

CharacterListRequest

Après l’acceptation du ticket

aucun

CharacterSelectionRequest

Après la liste

aucun

character

CharacterForceSelectionReadyRequest

CharacterForceSelectionEvent : un combat attend l’un des personnages du compte

aucun

ClientChallengeInitRequest

ServerSessionReadyEvent

oui

ClientIdRequest

ServerVerificationEvent

oui

ClientChallengeProofRequest

ServerChallengeEvent

oui

SequenceNumberRequest

SequenceNumberEvent

aucun

BasicLatencyStatsRequest

BasicLatencyStatsEvent

oui

PingRequest

Le battement de ping

aucun

ping

Les cinq réglages sociaux

Après le chargement

aucun

bringUp

GuildInformationRequest ×3, GuildWeeklyNextResetRequest

Après le chargement

aucun

bringUp

ContextCreationRequest

Après le chargement

aucun

BakApiTokenRequest

Après le chargement, puis toutes les heures

aucun

bringUp

GuildMemberWarnOnConnectionStopRequest

Après le chargement

aucun

bringUp

SetCommunityChannelRequest

Après le chargement

aucun

bringUp, community

QuestStepInformationRequest

Après le chargement, deux fois pour chaque quête en cours

aucun

bringUp

ObjectAveragePricesRequest

Après le chargement, une fois par jour

aucun

bringUp

AttackMonsterRequest

L’étape fightMonsters, une fois sur la cellule du groupe

aucun

EmotePlayRequest

L’étape rest : pour s’asseoir, puis pour se relever

aucun

ObjectUseRequest

L’étape eat, et rest avec eat : une fois par nourriture mangée

aucun

ObjectSetPositionRequest

Les étapes equip et unequip : une fois par objet déplacé

aucun

FriendListRequest, AcquaintanceListRequest, SpouseInformationRequest

Après le chargement

aucun

bringUp

AnomalySubareaInformationRequest

Première carte de la connexion

aucun

ContextReadyRequest, MapInformationRequest

Chaque MapCurrentEvent et MapCurrentInstanceEvent

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

AuthenticationTicketAcceptedEvent, AuthenticationTicketRefusedEvent

Termine l’identification

ServerSettingsEvent

Garde community pour le réglage server

ServerSessionReadyEvent, ServerVerificationEvent, ServerChallengeEvent

Répond à la preuve d’identité

QueueStatusEvent

Écrit la position dans la file d’attente dans la Console

CharacterListEvent

Choisit le personnage

CharacterForceSelectionEvent

Le serveur a choisi le personnage qu’un combat attend : base lui répond et joue ce personnage

QuestsEvent

Garde les identifiants des quêtes en cours, dans activeQuests. Le serveur l’envoie pendant le chargement du personnage, sans qu’on le demande.

CharacterSelectionEvent, CharacterLoadingCompleteEvent

Terminent la sélection du personnage

SequenceNumberEvent, BasicLatencyStatsEvent, PongEvent

Maintien de la connexion

BakApiKeyEvent

Planifie le prochain jeton de boutique

MapCurrentEvent

Annonce la carte

MapCurrentInstanceEvent

Annonce une carte instanciée

MapComplementaryInformationEvent

Résout connect() ; après un combat, l’étape fightMonsters continue une fois qu’il est arrivé

ContextCreationEvent

L’étape fightMonsters : le combat que son attaque a lancé

InteractiveUseEndedEvent

L’étape gather : la fin d’une récolte

EmotePlayEvent

L’étape rest : la réponse du serveur quand le personnage s’assoit et se relève

TrustStatusEvent

Avertit quand la session n’est pas certifiée

SystemMessageDisplayEvent

Avertit quand le serveur met fin à la session

FileCheckEvent

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

base: playing <name>

Le serveur a sélectionné le personnage

info

base: in the world, on map <id>

connect() s’est résolue

info

base: waiting in the server's queue, position <n> of <total>

Le serveur est plein

warn

base: resuming after an identification whose answer was not seen

Un script s’est arrêté entre le ticket et sa réponse. Voir Mémoire.

warn

base: the server does not certify this session (TrustStatusEvent)

Bon à savoir ; base continue

warn

base: the server is ending the session (system message <id>)

Le serveur s’apprête à déconnecter le bot

warn

base: the server asked to check <file>, which base does not answer

FileCheckEvent. Il n’a jamais été vu dans une capture.

warn

base: couldn't send <type>: <reason>

Un envoi a échoué. Base continue.

warn

base: <message> failed: <reason>

Un gestionnaire a échoué. Base continue.

error

La raison avec laquelle connect() s’est rejetée

Voir ci-dessous

Ce avec quoi connect() et les étapes se rejettent :

Erreur

Cause

base: the server refused this connection's ticket; sign the bot in again for a new one

AuthenticationTicketRefusedEvent

base: this account has no character on this server

La liste est vide

base: no character is called "<name>" on this server (there are: ...)

Le réglage character ne nomme aucun d’entre eux

base: the server refused to select <name>

CharacterSelectionEvent portait une erreur

base: the character's map never arrived within a minute of entering the world

Aucun MapComplementaryInformationEvent dans les 60 s qui suivent les requêtes d’après le chargement

protocol wait timed out

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, memory d’asterobot:bot, et session.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:movement avec changeMap(), useElement(), les voyages et les fonctions qui trouvent les cartes, que base réexporte à côté de move(). 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, les monsters des groupes de monstres dans currentMap(), record() d’asterobot:gamedata et distance() d’asterobot:pathfinding, pour combattre. Un Asterobot qui n’a pas asterobot:fight ne peut pas charger base.

  • find() d’asterobot:gamedata, et les tables skills et maps__interactive_elements des 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 sans items() 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 SubscribeMultipleChannelRequest après SetCommunityChannelRequest, 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.

  • FileCheckEvent ne 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 :

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

  2. 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.shared est vrai.

  3. Enregistré dans connect(), avant l’identification, puisque le serveur peut parler juste après avoir accepté le ticket.

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

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

  6. Rien ne s’échappe d’un gestionnaire. Utilisez guarded() et sendQuietly() : une erreur qui s’échappe d’un gestionnaire arrête tout le script, celui de base comme celui qui l’utilise.

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

libérés

Base devient la boîte à outils sur laquelle repose chaque bot : il exécute les plans conçus dans le Studio d’Asteroboard, récolte, combat, se repose, mange et change d’équipement à votre place, et fournit 24 profils de combat prêts à l’emploi pour toutes les classes.

✨ Ajouté

  • Exécution des plans conçus dans le Studio d’Asteroboard : emplacements sur la carte du monde, méthode de parcours (en boucle, dans un seul sens, du plus proche au plus éloigné ou jusqu’à ce qu’un événement se produise) et actions à effectuer à chaque emplacement, avec des attentes, l’exécution d’un autre plan sur place, des messages dans la console, des conditions (temps de jeu, nombre de tours, heure de la journée, probabilité), des nouvelles tentatives et des interruptions telles que « aller à la banque lorsque les sacs sont pleins ». Un package exécute un plan avec run(plan) et l’arrête avec stop().

  • Fini les déplacements inutiles : une carte sur laquelle une étape n’a rien à faire, par exemple parce qu’il ne reste aucune ressource à récolter ou aucun groupe à combattre, est ignorée jusqu’à ce qu’elle redevienne utile. Lorsque toutes les cartes sont en attente, le bot attend que la première redevienne disponible au lieu de parcourir des cartes vides.

  • Détection des erreurs avant que le bot ne bouge : Base vérifie l’intégralité du plan et répertorie toutes les erreurs 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.

  • Des plans capables de continuer malgré les erreurs : chaque arrêt et chaque étape peut, selon la configuration du plan, continuer malgré une erreur, l’ignorer ou réessayer. Sans configuration particulière, une erreur arrête le plan afin qu’elle ne passe jamais inaperçue.

  • Les paramètres d’un plan apparaissent dans l’onglet Paramètres du bot dès lors que le point d’entrée du package les réexporte. Toute modification est appliquée à partir de l’étape suivante.

  • Un plan se termine proprement à la fin d’un créneau horaire planifié : l’étape en cours se termine, puis le bot s’arrête.

  • Conditions basées sur l’état du personnage, utilisables aussi bien dans les plans que dans les scripts : niveau, kamas, points de vie, taux de remplissage des sacs, combat en cours ou non, objets transportés, niveaux des métiers et carte, sous-zone ou zone actuelle. Aucune valeur n’est supposée : si le jeu n’a pas encore fourni une information, l’étape ou l’arrêt échoue et la stratégie d’erreur définie dans le plan s’applique.

  • Récolte automatique : une étape récolte successivement les ressources de la carte, de la plus proche à la plus éloignée ou du niveau le plus élevé au plus faible, en utilisant l’action propre au métier. Choisissez les ressources précises ou toutes les ressources de certains métiers, excluez celles que vous ne voulez pas récolter, limitez-vous aux ressources accessibles selon le niveau du métier, définissez un nombre maximal de récoltes et arrêtez lorsque les sacs atteignent le taux de remplissage souhaité. Si une agression survient, le module de combat la prend en charge pendant que la récolte continue. Une condition permet de compter les récoltes.

  • Chasse automatique aux monstres : une étape attaque successivement les groupes de monstres présents sur la carte, en commençant par les plus proches, et attend la fin de chaque combat, géré par le module de combat d’Asterobot. Filtrez les groupes selon les monstres ou les races à rechercher ou à éviter, leur niveau, leur nombre, le niveau du monstre le plus puissant et la présence d’archimonstres, puis définissez le nombre de combats à effectuer. Si un groupe s’éloigne, le bot le suit et l’attaque à nouveau, plusieurs fois si nécessaire. L’étape active le module de combat s’il est désactivé, sauf lors d’une session relayée où cette activation reste sous le contrôle du joueur. Elle échoue si le personnage meurt.

  • Une autre étape permet de configurer l’activation, le profil et la vitesse du module de combat pendant l’exécution du plan. Deux conditions permettent de vérifier si le personnage est mort lors de son dernier combat et de compter le nombre de combats effectués.

  • Récupération entre les combats : une étape fait asseoir le personnage jusqu’à ce que ses points de vie atteignent le pourcentage choisi, avec la possibilité de consommer d’abord des objets de l’inventaire. Une autre étape permet de manger sans se reposer : pains, potions, viandes, poissons ou tout objet restaurant des points de vie, en privilégiant celui qui correspond le mieux aux points de vie manquants. Si un combat survient, le bot attend sa fin tandis que la récupération se poursuit.

  • Gestion de l’équipement : une étape équipe les objets indiqués, chacun dans un emplacement libre compatible. Une autre les déséquipe, et une condition permet de vérifier si le personnage porte un objet donné.

  • 24 profils de combat prêts à être utilisés par n’importe quel bot sous la forme @asterobot:base/<key>, disponibles dans la page Profils de combat et dans la liste des Profils de chaque bot. Tous fonctionnent avec toutes les classes, à tous les niveaux, puisqu’aucun ne référence de sort précis : jeu prudent ou offensif, charge au corps à corps, harcèlement à distance ou dissimulation, maintien de position, élimination du plus faible ou de la menace la plus dangereuse en priorité, soigneur, garde du corps, soutien, tank, contrôle de foule, invocateur, poseur de pièges, un profil par élément, combat à l’arme ou exclusivement aux sorts, jeu en équipe, accompagnement par un groupe plus puissant et combat contre un boss. Copiez un profil pour le modifier ou étendez-le pour continuer à bénéficier des mises à jour de Base. Il s’agit de simples fichiers JSON dans fightprofiles/.

  • Déplacements vers n’importe quelle destination avec un seul import : travel() déplace le personnage jusqu’à n’importe quelle carte, quelle que soit la distance, en traversant autant de cartes que nécessaire. route() indique le chemin qui serait emprunté, stopTravel() et trip() permettent respectivement d’arrêter un déplacement et de lancer un trajet. Les fonctions findMaps(), mapAt(), mapInfo(), nearest(), nearestWith(), mapsWithin() et reachable() permettent de rechercher des destinations et des itinéraires. Ces fonctions proviennent du module asterobot:movement et sont réexportées par Base, afin qu’un package n’ait besoin que d’un seul import.

  • Reprise correcte d’un combat interrompu par une déconnexion : lorsque le serveur sélectionne le personnage attendu par le combat, Base répond immédiatement, comme le ferait le jeu. La Console l’indique également lorsque le paramètre character désigne un autre personnage.

🔄 Modifié

  • Le module de combat d’Asterobot indique désormais la carte sur laquelle se déroule un combat, comme le fait le jeu. Base laisse donc cette tâche au module.

  • Cette version de Base nécessite la dernière version d’Asterobot, intégrant le module de combat. Les anciennes versions d’Asterobot ne peuvent pas la charger.

🐛 Corrigé

  • Les trois requêtes de guilde envoyées après le chargement du personnage sont de nouveau transmises. Elles demandent désormais les sections attendues par le client officiel. Les noms de sections précédents n’étant plus reconnus par le jeu, les requêtes n’étaient jamais envoyées et la Console affichait un avertissement à chaque fois.

  • Sur les PC lents, un changement de carte ne déclenche plus deux chargements du contenu de la nouvelle carte. Base indique désormais à Asterobot qu’il se charge lui-même de demander le contenu de chaque carte : seul Base effectue cette demande, quelle que soit la durée nécessaire.


Version d'Asterobot requise

0.6.0

Commentaires des utilisateurs

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.

Invité
  • Ceci ne sera pas visible par les autres utilisateurs.

  • Votre avis Requis
    Ajouter un avis...

Compte

Navigation

Recherche

Recherche

Configurer les notifications push du navigateur

Chrome (Android)
  1. Appuyez sur l'icône de cadenas à côté de la barre d'adresse.
  2. Tap Autorisations → Notifications.
  3. Ajustez vos préférences.
Chrome (Desktop)
  1. Cliquez sur l'icône représentant un cadenas dans la barre d'adresse..
  2. Select Paramètres du site.
  3. Find Notifications et ajustez vos préférences.