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.

Smooth

Administrateurs

Tout ce qui a été publié par Smooth

  1. Smooth a posté un record dans Paquets
    A package can use the code of other packages in the library, its dependencies. That's how you share helpers between your own packages, or build on a package someone published. How dependencies work A package declares its dependencies in the dependencies field of its manifest. The key of each dependency is the exact name your code imports. An import gives you what the dependency's index.js exports, and nothing else. Each dependency is locked to one exact version, and that version has to be in the library when the script starts. Starting a script never downloads anything. A dependency's code runs in the same script as your package, on the same bot. Its settings and actions show on the bot's page next to yours: see Settings in dependencies. The modules of your dependencies count toward the module limits of your script. See Limits. Depend on a package from your library Make sure the package you need is in your library, at the version you want: install it from the marketplace, as Install from the marketplace shows, or create it yourself. Open your own package's page: click its name in Library Manager. In the Manifest card, open Add an installed package and pick the package, such as chat-tools@1.0.0. A row appears with its name and version. Click Save manifest. A toast says Manifest saved. Your manifest now holds: { "dependencies": { "chat-tools": { "version": "1.0.0" } } } And your code can import it by that name: import { formatChat } from "chat-tools"; Settings in dependencies has a complete example of both packages. When you save, Asterobot checks that a dependency without a URL is installed at that exact version, and refuses the manifest otherwise. Make it installable for others When someone installs your package from the marketplace, Asterobot also installs its dependencies, but only those with a URL. A dependency without a URL has to be in their library already, at exactly that version, or the installation fails. So every dependency of a package you publish needs a URL, which means it has to be published too. If the dependency is one of your own packages, publish it first: see Publish on asterobot.net. Install it from the marketplace on your own Asterobot. It gets a marketplace name, shown as Will install as before you install, such as @alice:chat-tools@1.2.0. In your package's Manifest card, add it with Add an installed package, then paste the address of its file page in the third box of that row, the one that reads asterobot.net file URL (optional). If a row with its local name remains, remove it. Change your imports to the marketplace name: import { formatChat } from "@alice:chat-tools";. Click Save manifest, then start your package on a bot to check that it still works. About the URL: It's the address of the file's page on asterobot.net, such as https://asterobot.net/files/file/12-chat-tools/. The file's ID alone, 12, works too. The name of the dependency has to match the file. The part before the : must be the publisher Asterobot derives from the file: its author's name, or asterobot for an official file. When they don't match, the installation of your package fails. So a dependency with a local name, such as chat-tools, can't be fetched from a URL. Asterobot only uses the URL when your package is installed or updated from the marketplace. Starting your package, importing it from a .zip or installing it with library install never downloads its dependencies. Choose the version Version On your Asterobot When someone installs your package from the marketplace An exact version, such as 1.2.0 Loads that version, which has to be installed Uses that version if they have it. Otherwise Asterobot downloads it, from the file's current version or from the file's version history on asterobot.net. A range, such as ^1.2.0, ~1.2.0 or >=1.2.0 <2.0.0 Doesn't start Uses the highest version of the file that matches, among its current version and its history, installing it if they don't have it latest, or nothing, for a dependency with a URL Doesn't start Uses the file's current version, installing it if they don't have it Once Asterobot has chosen a version during an installation, it writes that exact version in the installed copy's manifest. The package keeps using it from then on, even when the dependency publishes a newer version. A dependency without a URL always needs an exact version. A range or latest in your own package's manifest stops it from starting on your Asterobot, because Asterobot only loads exact versions. The start fails with a reason starting with resolve package <name>@<version>: dependency "<name>" locked to version "^1.2.0": read package, which Package settings on the bot's Settings tab shows. So use exact versions. Your package then runs for you exactly as it will for the people who install it, and you choose when to move to a newer dependency, by publishing an update of your package. A version in the name A dependency's name can end with @ and an exact version, such as chat-tools@1.0.0. That's how one package uses two versions of the same package at once: { "dependencies": { "chat-tools@1.0.0": { "version": "1.0.0" }, "chat-tools@2.0.0": { "version": "2.0.0" } } } import { formatChat as formatChatV1 } from "chat-tools@1.0.0"; import { formatChat } from "chat-tools@2.0.0"; Write the same version in Version as in the name. Asterobot loads the version written in Version: a dependency whose version is only in its name doesn't start. A version in the name that differs from Version is refused when you save, and so is one that isn't an exact version. Both versions read the same settings, since settings are saved under the package's name. An import can also pick a version with a range, such as chat-tools@^1.0.0, among the versions the script already loads: see Modules and imports. Dependencies of dependencies A dependency declares its own dependencies in its own manifest. They're loaded the same way, and installed with it from the marketplace. A package needed by several packages at the same version is loaded once, and its top-level code runs once. Different versions of the same package can be loaded side by side, each locked by the package that declared it. Every package loaded is checked: an incompatible compatibility range or a checksum that no longer matches, in any dependency, stops the whole script from starting. When something's wrong What you see Cause Fix dependency "<name>" needs <name>@<version> installed, or a URL to fetch it from, on the Manifest card That version of the dependency isn't in your library Install it, or correct the version dependency "<name>" needs an exact version, on the Manifest card A dependency without a URL has no version Fill in Version A reason containing read package <name>@<version> sources, on Package settings The locked version isn't in the library: it was removed, or the version is a range or latest Install that version, or lock an installed version A reason containing has no locked dependency for "<name>" An import uses a name that isn't a key of dependencies, often a typo or an old name Make the import and the key identical A reason containing dependency "<name>" targets package named The version in the dependency's name differs from its Version Write the same version in both Library Manager lets you remove a package other packages depend on, after a warning listing them. Those packages then fail to start until the version they lock is installed again. Error messages lists the other texts. Reuse a connection package Every package that runs on a Full socket bot has to identify the bot on the game server, as Identify and run shows. You can already put that part in a package of your own and depend on it from your other packages. Note Coming soon. Ready-made basic and full connection scripts will be published for you to run and reuse, so your package can depend on one instead of doing the identification itself. Until then, the identification stays in code you write: in your package, or in a package of yours that it depends on. Next, Compatibility.
  2. Smooth a posté un record dans Paquets
    A package is identified by its name and its version together: my-bot 1.0.0 and my-bot 1.0.1 are two packages in your library. The way you number versions matters once other people use your package. Where a name and a version come from Neither is written in the package's files. A package gets them from how it was installed: How Name and version New package, Import a .zip, Copy to a new version The Name and Version typed in the dialog The library install command The --name and --version flags Installing from the marketplace Built from the file on asterobot.net. See How your package is named. Asterobot stores each version in its own folder of the library, named after the package and the version, which is why the two follow the rules below. You can't rename a package or change its version in place. Copy it to a new name or version, or export it and import it again. Local names A local name is a plain name, such as my-bot. Only local packages can be edited in Asteroboard. It can't contain @ or :, since both mean something in marketplace names and in imports. It can't be empty, . or .., and can't contain / or \. Everything else is allowed, dots included, even at the start as in .dotted. Lowercase words joined by hyphens, such as chat-logger, read best in lists and in import statements. Marketplace names A marketplace name looks like @publisher:name, such as @alice:chat-tools. Asterobot gives one to every package installed from the marketplace. The name starts with @, then a publisher, a : and a name, and contains no other @ or :. A publisher of asterobot makes an Official package. Any other publisher makes a Community package. The Import a .zip dialog and library install also accept a name of this shape. The package then shows Community or Official without coming from the marketplace, and it can't be edited or copied in Asteroboard. Name Accepted Why my-bot Yes A local name .dotted Yes A local name @alice:chat-tools Yes A community name @asterobot:core Yes An official name my@bot No Reads as the package my at version bot local:bot No A : introduces a name after a publisher, and there's no @ @alice, @alice:, @:chat-tools No The publisher or the name is missing @alice:chat:tools, @alice:chat@tools No A second : or @ The dialogs refuse a wrong name as you type: Message Where A local package name can't contain ':' or '@'. New package, Import a .zip, Copy to a new version A new package is local, so its name can't start with '@' - those are assigned by the marketplace. New package and Copy to a new version, which only create local packages A marketplace name looks like @publisher:name. Import a .zip A marketplace name has exactly one ':' and one leading '@'. Import a .zip The command line gives Asterobot's own wording, such as local package name "my@bot" must not contain ":" or "@", marketplace package name "@alice" must be "@publisher:name" or marketplace package name "@alice:chat:tools" must contain exactly one ":" and one leading "@". Version numbers A version is required, and Asterobot accepts almost any text: it can't be . or .., and can't contain / or \. New packages start at 1.0.0. A version breaking these rules is refused with invalid version "<version>": contains a reserved character, or empty or reserved path component. Copy to a new version shows that reason, while New package and Import a .zip only show a toast. Use semantic versions Write versions as three numbers separated by dots, such as 1.4.2: a semantic version. Asterobot accepts other text, but several features only understand semantic versions: Feature With semantic versions With other text Check for updates Offers a version only when it's higher than the installed one Offers any different version, even an older one Version ranges such as ^1.2.0 or latest, in imports and in dependencies Consider this version Skip this version, as if it weren't installed Declaring the package as a dependency Works Save manifest refuses the version Copy to a new version Suggests the next version: 1.4.2 becomes 1.4.3 Raises the last number in the text, or suggests the same version when there's no number The usual meaning of the three numbers helps the people who use your package, especially other authors who depend on it with a range such as ^1.2.0, which accepts any 1.x.y from 1.2.0 on: Raise the last number for a fix that changes nothing else: 1.4.2 to 1.4.3. Raise the middle number when you add something without breaking what exists, and reset the last one: 1.4.3 to 1.5.0. Raise the first number when something that worked before stops working: a setting renamed, an exported function removed or changed. Reset the other two: 1.5.0 to 2.0.0. Some details of how Asterobot reads semantic versions: 1, 1.4 and v1.4.2 are understood too, but they're different text from 1.4.2. An exact version is always compared as text, so a dependency locked to 1.4 doesn't match an installed 1.4.0. Write three numbers, without v, everywhere. Numbers can't have leading zeros: 1.04.0 isn't a semantic version. A suffix after a hyphen, as in 2.0.0-rc.1, makes a version that comes before 2.0.0. Check for updates offers 2.0.0 to someone on 2.0.0-rc.1, never the reverse, and ranges only match such versions when the range itself has a suffix. A suffix after +, as in 1.4.2+build.7, doesn't change the order. Versions side by side Each version of a package is installed on its own, and stays until someone removes it. A bot is set to one exact version, so installing a new version never changes what a bot plays. Update packages describes this from the player's side. For you as the author: To start a new version, use Copy to a new version on the current one, as Share your package shows. The old version stays as it was. Give every change you share a new version. Two packages with the same name and version can't be told apart, and the second one installed replaces the first. Settings are saved under the package's name, so a bot moved to a new version keeps its values, and a package copied under a new name starts from its defaults on every bot. See Read settings. A script can load several versions of the same package at once, when its dependencies lock different versions. See Dependencies. Next, Dependencies.
  3. Smooth a posté un record dans Paquets
    asterobot.json is the one file of a package that isn't code. It says which other packages the package depends on, which Asterobot versions it works with and what it declares it uses. Where it lives At the root of the package, next to index.js. It's optional. A new package has none, and Asterobot only writes one when there's something to put in it. The editor doesn't show it. The Files list only holds modules, and saving code never changes the manifest. It doesn't hold the package's name or version. Those come from where the package is installed: the Name and Version typed when the package was created, copied or imported, or the file it came from on asterobot.net. See Names and versions. In Asteroboard, you edit the manifest on the package's own page, in its Manifest card. A manifest can also come with the package: inside a .zip you import, or in a folder you install with library install, as Zip files and the command line explains. An example This is the manifest of a package installed from the marketplace, as Asterobot writes it: { "dependencies": { "@alice:chat-tools": { "url": "https://asterobot.net/files/file/12-chat-tools/", "version": "1.2.0" } }, "compatibleVersion": ">=1.5.0 <2.0.0", "integrity": "sha256-9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "permissions": [ "gamedata" ], "sourceId": 34 } The author wrote dependencies, compatibleVersion and permissions. Asterobot added integrity and sourceId when it installed the package from asterobot.net. Fields Key Type Written by What it holds dependencies Object You The library packages this package imports. Each key is the name the code imports, and each value is an object with a version and a url, both optional. See Dependencies. compatibleVersion String You The range of Asterobot versions the package works with, such as >=1.5.0 <2.0.0. See Compatibility. permissions Array of strings You What the package says it uses. Asterobot doesn't enforce it yet. See Permissions. sourceId Number Asterobot The ID of the asterobot.net file the package was installed from. Check for updates compares the package with that file, and it's how a marketplace package keeps its name when its file is renamed. integrity String Asterobot A checksum of the package's .js and .mjs files, starting with sha256-, recorded when the package is installed from the marketplace. Asterobot compares the files with it each time it loads the package, and refuses them with package source integrity mismatch when they changed. And inside each entry of dependencies: Key What it holds version An exact version such as 1.2.0, a range such as ^1.2.0, or latest. url The address of the dependency's file on asterobot.net, or its ID. Without it, the dependency has to be in the library already. Asterobot keeps only these keys. Anything else in the file, such as a name or a description, is dropped as soon as the package is installed, because Asterobot writes the file again from what it understood. The file has to be valid JSON: a package whose asterobot.json doesn't parse can't be imported or installed, and if the file breaks after installation, the package disappears from Library Manager and doesn't start. What Asterobot does with sourceId and integrity When sourceId integrity Installing or updating from the marketplace Set to the file's ID Computed from the files just downloaded Import a .zip Removed Removed Copy to a new version Removed Removed Save in the editor Kept Removed, since the code changed Save manifest Kept Kept The library install command Kept as they are in the folder's file Kept as they are in the folder's file So whatever you write in these two keys is replaced or removed, except by library install. Leave them out of a package you're working on, which matters most when you start from a package you exported: a leftover integrity stops your package from starting as soon as you change a file, and a leftover sourceId ties it to a file on asterobot.net it didn't come from. The Manifest card To open a package's page, click its name in Library Manager, or click the box icon before the name at the top of the editor. The Manifest card is under the details card. For a local package, the card is a form: Part What it does Dependencies One row per dependency, with three boxes and a remove button. Empty, the boxes read Package name, as imported, Version and asterobot.net file URL (optional). Without any row, the card says "No dependencies." Add an installed package Lists every other package in your library as name@version. Picking one adds a row with its name and version, and no URL. A row that already had that name is replaced. Add by URL Adds an empty row for you to fill in. Permissions One badge per permission, each with a remove button, or "No permissions declared.". Type a new one in the box that reads e.g. gamedata, then click Add or press Enter. Compatible with A single box for the range of Asterobot versions, reading e.g. >=1.5.0 <2.0.0 while empty. Save manifest and Reset Both stay disabled until something changes. Reset puts the card back as the manifest was last saved. When you click Save manifest, spaces around each value are removed, a dependency row with an empty name is left out, and an empty or repeated permission isn't added. What you save replaces the dependencies, permissions and range the manifest had. A toast says Manifest saved. A script that's already running keeps what it loaded, and the next start uses the new manifest. For a package with any other name, the card is read-only and says "Published by someone else, so this is read-only". It lists each dependency with its version, or latest when it has none, then the permissions and the range, or "No constraint declared." when there's no range. When Asterobot refuses a manifest When a save fails, the card shows Couldn't save the manifest with the reason under it. Before saving, Asterobot checks each dependency that has no URL: it needs an exact version, and that version has to be in your library. Then it checks the versions of every dependency. Reason What to do dependency "<name>" needs <name>@<version> installed, or a URL to fetch it from Install that version of the package, or correct the name or the version dependency "<name>" needs an exact version Fill in Version invalid package manifest: dependency "<name>": invalid version "<version>": <details> Write an exact version, a range or latest Error messages lists every reason, including those about a version written in the dependency's name. Two fields aren't checked when you save. A compatibleVersion that isn't a valid range is accepted, and only fails when the package loads: see Compatibility. An empty or repeated permission, which the card doesn't let you add but a file can contain, stops the package from starting with empty package permission or duplicate package permission "<permission>". A manifest inside a .zip or a folder goes through the same version checks when the package is installed. The Import a .zip dialog then only says Couldn't import the package, while library install prints the reason. Next, Names and versions.
  4. A package can use other packages of the library, its dependencies, and each of them can declare settings and actions of its own. A script only reads the settings of its own package, and a bot's Settings tab shows each package's settings apart. Dependencies covers declaring and importing them. Each package reads its own settings Every package gets its own copy of asterobot:parameters. Whichever file imports it, values holds the settings declared in the index.js of the package that file belongs to: In your package, values holds your settings, never a dependency's. In a dependency, values holds the dependency's settings, never yours. An onChange() handler registered by a dependency only hears about the dependency's settings. No package can read or change the settings of another one. A dependency declares its settings the way Declare settings describes, in its own index.js. On the bot's page Under Package settings, on the bot's Settings tab, there's one card per package of the bot's script that declares at least one setting or action: the bot's own package and each of its dependencies, down to the dependencies of dependencies. A card's title gives the package's name, its version and a count such as "2 setting(s) · 1 action(s)". A card only appears for a package whose code the script loads. A dependency listed in the manifest that no file imports declares nothing, so it has no card. A package that declares no setting and no action has no card either. The first card is open and the others are closed. The cards don't come in a fixed order, so the bot's own package isn't necessarily the first one. Apply and Reset work card by card: applying one card leaves the others untouched. A dependency's actions are in its own card, and run the dependency's own run functions. The Add a bot dialog shows the same cards, without Apply and without actions, as soon as a library package is picked. Saved under the dependency's name A bot saves a dependency's settings under the dependency's name, the same way it saves yours. So, on one bot: Every package that uses a given dependency reads the same values for it. When the bot moves from one of these packages to another, the dependency's settings come along. The values carry over between versions of the dependency, under the rules in Read settings. Another bot has values of its own. Write a package for others to use A package meant to be a dependency is an ordinary package. What sets it apart is what its index.js exports: functions and values for the packages that import it, and usually no default export. Here is a small one, chat-tools, with one setting and one function: import { values } from "asterobot:parameters"; /** @type {BehaviorParameters} */ export const parameters = { showChannel: { type: "bool", label: "Show the channel", default: true, }, }; export function formatChat(payload) { const line = `${payload.senderName}: ${payload.content}`; // Read at each call, so an applied change counts from the next line. return values.showChannel ? `[${payload.channel}] ${line}` : line; } A package named chat-logger uses it. Its manifest declares the dependency, as Dependencies shows: { "dependencies": { "chat-tools": { "version": "1.0.0" } } } And its index.js imports formatChat() by the name it declared: import { session, botInfo } from "asterobot:bot"; import { on, send } from "asterobot:protocol"; import { formatChat } from "chat-tools"; export default async function behavior(launch) { on("ChatChannelMessageEvent", (traffic) => { // A message Asterobot couldn't decode has no payload, and an error // thrown in a handler stops the whole script. if (!traffic.payload) return; botInfo(formatChat(traffic.payload)); }); // A new game connection starts with this message. MITM bots never launch // with "initial": the Dofus client has already identified their session. if (launch.reason === "initial") { await send("IdentificationRequest", { ticketKey: session.gameToken, languageCode: session.language, }); } } A bot set to chat-logger shows a single card, chat-tools, with Show the channel: chat-logger declares nothing itself. Turning the switch off and clicking Apply changes the next chat line the bot's console shows. Some advice for packages like chat-tools: Decide who chooses each value. What the person running the bot decides belongs in your settings. What the package using yours decides belongs in the arguments of the functions you export, since that package can't change your settings. Label your settings clearly. They appear under your package's name on every bot whose package uses yours, and the person reading them may never have heard of your package. Several packages on one bot share your settings. Don't design a setting whose right value depends on which of them uses it. Leave out the default export if the package isn't meant to run on its own. It's still listed in the library and can still be picked for a bot, but its start then fails with an error containing has no unambiguous default export. Say what the package is for in its description when you publish it. Keep the top level of index.js to imports, declarations and function definitions. It runs at every start of every package that uses yours, and each time Asterobot reads their settings. Next, the Packages chapter describes the manifest, starting with every field of asterobot.json.
  5. Smooth a posté un record dans Réglages et actions
    An action is a button your package adds to the bot's page. Clicking it calls a function of the running script, with values the person can fill in first. Actions suit one-off things someone decides while the bot plays: report something, say something, move on to the next step. For a choice that should last, declare a setting instead. Declare an action Actions are declared next to settings, as an object exported as actions from index.js. Each key is the action's name, and each value describes the button and holds the function it runs. Warning On a MITM bot (man-in-the-middle: the bot relays the game session of the Dofus client you play), the Say in chat action below sends a real chat message as your character. import { session, botInfo } from "asterobot:bot"; import { on, send } from "asterobot:protocol"; // The last pods the game reported, for the report action. let lastWeight; /** @type {BehaviorActions} */ export const actions = { reportPods: { label: "Report pods", description: "Writes the character's pods to the bot's console.", run() { if (!lastWeight) { botInfo("The game hasn't reported the pods since the script started."); return; } botInfo(`Pods: ${lastWeight.inventoryWeight} / ${lastWeight.weightMax}`); }, }, say: { label: "Say in chat", description: "Sends a chat message as the character.", args: { text: { type: "string", label: "Message", placeholder: "Hello!" }, channel: { type: "string", label: "Channel", choices: [ { value: "GLOBAL", label: "General" }, { value: "GUILD", label: "Guild" }, { value: "PARTY", label: "Party" }, ], }, }, async run({ text, channel }) { // A failed action is reported to the person who clicked, and the script // keeps running, so there's no need to catch errors here. if (text === "") throw new Error("Type a message first"); await send("ChatChannelMessageRequest", { channel, content: text }); }, }, }; export default async function behavior(launch) { on("InventoryWeightEvent", (traffic) => { // A message Asterobot couldn't decode has no payload, and an error // thrown in a handler stops the whole script. if (traffic.payload) lastWeight = traffic.payload; }); // A new game connection starts with this message. MITM bots never launch // with "initial": the Dofus client has already identified their session. if (launch.reason === "initial") { await send("IdentificationRequest", { ticketKey: session.gameToken, languageCode: session.language, }); } } Actions follow the same rules as settings about where they're declared: only the actions export of index.js counts, the buttons keep the order you write them in, and /** @type {BehaviorActions} */ gives completion and checks on the fields. A package can export parameters, actions or both. Fields Field Required What it does label No The button's text. Without it, the button shows the key, such as reportPods. description No Shown when the pointer rests on the button, and at the top of the dialog that asks for the arguments. Without it, that dialog says "Fill in what this action needs, then run it." args No The values the bot's page asks for before running the action. Each one is declared exactly like a setting. run Yes The function the button calls. Other fields are ignored. An action without run, a run that isn't a function, or args that isn't an object of objects is a mistake in the shape of the export: the script doesn't start at all, and the Problems alert shows a text starting with read declarations of. Declarations lists those texts. run and its arguments run receives a single object holding every argument you declared: The value from the dialog, checked against the argument's declaration with the same rules as a setting: the type, the choices, min and max. The argument's default, for an argument that wasn't sent. An argument declared without a default gets its type's zero value or its first choice, which is why channel above starts at GLOBAL. An action without args receives {}. Arguments are never saved. The dialog opens with the declared defaults every time, whatever was typed the last time. run can be async. Asterobot reports the result once its promise settles, not when the call returns. Whatever run returns is ignored. run runs inside the script like a message handler: it shares the script's variables, can use every function of the built-in modules, and waits for its turn while the script handles other events. The code of run that runs without awaiting has the same 250 ms limit as a handler. Past it, the action fails with JavaScript execution deadline exceeded, and the script keeps running. Asterobot checks the arguments against the declaration of the package the bot is set to play, as it's saved in your library. If that declaration can't be read, the values go to run unchecked. What the person clicking sees The buttons are under Actions, in your package's card on the bot's Settings tab, in the Package settings section. Each one has a lightning bolt. For an action with arguments, a dialog opens, titled with the action's label, with one field per argument, and Cancel and Run buttons. Once the action is handed to the script, a toast says Action started. When run returns, or its promise settles, a second toast gives the action's key followed by finished, or followed by failed with the error. A failure toast stays until someone closes it. The toasts name the action by its key, such as say failed, not by its label. Choose keys a person can recognize. An error in run never stops the script. When the action can't even be handed to the script, Couldn't start the action appears instead, with the reason. The two you'll meet most while writing a package are an argument that doesn't fit, such as count: 0 is below the minimum 1, and an action the running script doesn't have. Error messages lists them all. When actions can be used Only while a script runs on the bot. Otherwise the buttons are disabled, and resting the pointer on one shows The bot has to be playing a behavior before an action can run. A button calls the action of the script that's running, while the card describes the package the bot is set to play, as saved in your library. After you add, rename or change an action in the editor, save and start the script again, with Run for example. Until then, a new action fails with the running behavior declares no action "<action>" on package "<package>", and a changed run still runs the old code. Asterobot finds the actions when the script loads, before it calls your default export. The buttons work while the entry function is still awaiting something, and after it has returned. The actions of a dependency are in the dependency's card, and run the dependency's own functions. See Settings in dependencies. The actions of an inline script can't be used: the Settings tab never shows its declarations. When a setting or an argument breaks a rule, Package settings shows Couldn't read this package's settings instead of the cards, so no button can be clicked until the declaration is fixed. Declare settings explains those errors. Next, Settings in dependencies.
  6. Smooth a posté un record dans Réglages et actions
    Once your package declares settings, its script reads their current values from asterobot:parameters. It can also react the moment someone applies a change. asterobot:parameters is the reference for the module. values import { values } from "asterobot:parameters"; values has one property per setting your package declares, named by its key: values.maxAnswers for a setting declared as maxAnswers. Each value has the type of its setting: a boolean for bool, a string for string, a regular number for int and double, an array for array and an object for map. The values are the ones saved for the bot, laid over the defaults of your declaration. A setting nobody changed reads as its default. values is live. After someone applies new settings, the next read returns the new value, without restarting the script. So read a setting at the moment you need it, in a handler or at each turn of a loop, instead of copying it into a variable when the script starts. When a value has to stay the same for a whole exchange, copy it at the start of that exchange, as the chat script in Add a setting does with its reply. A few more things about values: Object.keys(values) lists the settings, and "delay" in values tells whether one exists. It's read-only. Assigning to one of its properties throws a TypeError, and so does deleting one. asterobot:parameters quotes both texts. It only holds your own package's settings. In a dependency, values holds the dependency's settings: see Settings in dependencies. When Asterobot can't read the declaration, values is empty and every read gives undefined. Declare settings says when that happens. An inline script can declare settings, but they never appear on the bot's Settings tab, so it always reads its defaults. When new values arrive Here is what happens when someone changes settings under Package settings on the bot's Settings tab, then clicks Apply: Asteroboard sends every setting of that package's card: the ones that changed and the ones that didn't. Asterobot checks each value against your declaration. If one doesn't fit, nothing is saved, and Couldn't apply the settings appears with the reason. Asterobot saves the values for the bot. If a script is running, Asterobot hands it the new values. values returns them from then on, then the onChange() handlers run for each setting whose value changed. All the values of one Apply reach the script together, before any handler runs, so a script never sees half an edit. When no script is running, the values are only saved, and the next start reads them. The same thing happens in the rare case where the running script already has 32 operations waiting when someone clicks Apply: see Limits. React to a change onChange() registers a function that runs when an applied value differs from the previous one. Use it when a change should do something right away. When the script only needs the current value the next time it looks, reading values is enough. This script writes a warning on the bot's console when the character's pods go over a limit taken from the settings. When someone changes the limit, onChange() checks the last pods the game reported against the new limit at once, instead of waiting for the game to report them again: import { session, botInfo, botWarn } from "asterobot:bot"; import { values, onChange } from "asterobot:parameters"; import { on, send } from "asterobot:protocol"; /** @type {BehaviorParameters} */ export const parameters = { warnAt: { type: "int", label: "Warn when pods reach", unit: "%", default: 90, min: 1, max: 100, }, }; // Kept so the pods can be checked again when the limit changes. let lastWeight; function checkWeight() { if (!lastWeight || lastWeight.weightMax <= 0) return; const percent = Math.floor((lastWeight.inventoryWeight * 100) / lastWeight.weightMax); if (percent >= values.warnAt) { botWarn(`Pods at ${percent}%, the limit is ${values.warnAt}%`); } } export default async function behavior(launch) { on("InventoryWeightEvent", (traffic) => { // A message Asterobot couldn't decode has no payload, and an error // thrown in a handler stops the whole script. if (!traffic.payload) return; lastWeight = traffic.payload; checkWeight(); }); onChange(({ name, value }) => { botInfo(`Setting ${name} is now ${value}`); if (name === "warnAt") checkWeight(); }); // A new game connection starts with this message. MITM bots never launch // with "initial": the Dofus client has already identified their session. if (launch.reason === "initial") { await send("IdentificationRequest", { ticketKey: session.gameToken, languageCode: session.language, }); } } To try it, Run the package on a MITM bot (man-in-the-middle: the bot relays the game session of the Dofus client you play). On the bot's Settings tab, under Package settings, set Warn when pods reach to 1 and click Apply. The bot's Console shows Setting warnAt is now 1, and a warning as soon as the game has reported the character's pods since the script started. The handler receives an object with the setting's name and its new value, of the setting's type. Some details: It runs once for each setting whose value changed, and only for your own package's settings. It doesn't run when the script starts, and an Apply that changes nothing doesn't call it. A list or a map counts as changed only when its content differs. With several handlers, the order they run in isn't fixed. Don't write one that depends on another having run first. A handler that throws stops the whole script, like a message handler. The Problems alert then starts with parameters.onChange handler, or with unhandled Promise rejection: when an async handler's promise rejects. Put anything that can fail in try/catch. onChange() returns a handle. Pass it to offChange() when the script no longer needs to hear about changes: const handle = onChange(({ name }) => { botInfo(`${name} changed`); }); // Once the script doesn't need it any more. offChange(handle); A script can have up to 256 onChange() handlers registered at once, counted apart from its message handlers. See Limits. Saved values Settings are saved per bot, under the package's name, whatever its version. For you as the author, that means: Two bots playing your package each have their own values. A bot moved to another version of your package keeps its values. Asteroboard saves every setting of a package's card at once, the untouched ones included, when someone clicks Apply, and when a bot is added with your package picked in the Add a bot dialog. From then on, the bot has a saved value for each of those settings, and the defaults of your later versions don't change them. A saved value is used only while it fits the declaration of the version the bot plays. When it doesn't fit, the default applies, but the saved value isn't deleted: it comes back if the bot returns to a version where it fits, unless someone applied a new value to that setting in the meantime. Here is what a bot sees after it moves to a new version of your package: In the new version, you A bot with a saved value reads A bot without one reads Add a setting Its default Its default Change a default Its saved value The new default Rename a setting The default, under the new name. The value saved under the old name isn't used. The default Change a setting's type Its saved value if it still fits, such as a whole number saved for a setting now declared double, otherwise the default The default Remove a choice, or narrow min and max Its saved value if it's still allowed, otherwise the default The default Remove a setting Nothing: the setting is gone Nothing So once people use your package, keep the names of its settings. And when a setting should mean something else, give it a new name: an old value that still fits the type would otherwise be read with the new meaning. The settings of a dependency are saved under the dependency's own name. Settings in dependencies explains what follows from that. What a script can't do with settings Change a setting. values is read-only, and the next Apply would send the values shown on the page anyway. Read the settings of another package, whether it's the package that uses it or one of its dependencies. Keep data for its next run. Each start begins again from scratch: see Play, stop and reload. Next, Declare actions.
  7. Smooth a posté un record dans Réglages et actions
    Settings let the people who run your package change what it does without touching its code. Add a setting showed the basics, and Declarations keeps the compact schema with the full list of error texts. Where the declaration goes A package declares its settings in one place: an object exported as parameters from its index.js. Each key is the name of a setting, the name your script reads later in values, and each value describes the setting. /** @type {BehaviorParameters} */ export const parameters = { enabled: { type: "bool", label: "Answer in chat", default: true, }, delay: { type: "double", label: "Wait before answering", unit: "seconds", default: 1.5, min: 0, max: 10, }, maxAnswers: { type: "int", label: "Answers per player", description: "The bot stops answering a player after this many replies.", default: 3, min: 1, }, tone: { type: "string", label: "Tone", choices: [ { value: "polite", label: "Polite" }, { value: "short", label: "Short" }, ], default: "polite", }, ignored: { type: "array", of: "string", label: "Ignored players", description: "Character names the bot never answers.", }, replies: { type: "map", of: "string", label: "Replies", description: "What a player says on the left, what the bot answers on the right.", default: { "!hello": "Hello!", "!help": "Ask me anything." }, }, }; A few rules decide what Asterobot picks up: Only an export of index.js counts. An export const parameters in another file of the package is ignored. Settings appear on the bot's page in the order you write them. The one exception comes from JavaScript itself: keys that look like whole numbers, such as "2", are always listed first. The object is ordinary JavaScript. A default can be a constant from another file of the package, or a value computed when the module runs. Asterobot reads the object by running the top level of your modules, without calling your default export and without a bot or a game session. At that moment values is empty and session holds empty values, so a declaration can't depend on either. The entry function lists what else behaves differently while Asterobot reads it. Each package declares its own settings, dependencies included. See Settings in dependencies. The key is also the name Asterobot saves the value under for each bot. Renaming a setting in a later version loses the values people saved, as Read settings explains, so pick keys you won't want to change. An empty key is refused. Fields Field Used by What it does type Every setting Required. "bool", "string", "int", "double", "array" or "map". of array and map, where it's required The type of the list's items, or of the map's values: "bool", "string", "int" or "double". Refused on every other type. label Every setting The name shown with the input. Without it, the key is shown, such as maxAnswers. description Every setting A line of help shown with the input. placeholder string, int and double Grey text shown while the box is empty. unit int and double A short word shown at the end of the number box, such as seconds or kamas. default Every setting The value until someone applies another one. See Defaults. choices string and int Turns the input into a drop-down list of allowed values. See Choices. min and max int and double The smallest and the largest value allowed. A value equal to a bound is allowed. Asterobot is lenient with what it doesn't understand, which cuts both ways. A field it doesn't know is ignored without an error, so a typo such as lable silently does nothing. A label, description, placeholder or unit that isn't a string is ignored too, and so is a min or max that isn't a number. The JSDoc type described below catches most of these mistakes in the editor. Some combinations have no effect either: min and max don't apply to a setting with choices, nor to the items of an array or a map. placeholder and unit aren't shown on a drop-down list, nor on the items of a list or a map. min greater than max is refused. On the bot's page, min and max also limit the arrows of the number box. Asterobot checks the bounds again when someone clicks Apply, whatever was typed. The six types type The script reads On the bot's page Without a default bool true or false A switch false string A string A text box "" int A whole number, as a regular number A number box whose arrows step by 1 0 double A number A number box 0 array An array of values of the of type One row per item, numbered from 0, each with a remove button, and Add under the rows [] map An object whose keys are strings and whose values have the of type One row per entry with a Key box, the value and a remove button, and Add an entry under the rows {} What each type accepts, for a default or for a value applied on the bot's page: bool takes true or false, nothing else. string takes a string. The number 5 isn't turned into "5". int takes a whole, finite number. 3.0 fits, and 3.5 is refused rather than rounded. The script reads it as a regular number, not as a BigInt like the 64-bit fields of game messages. double takes a finite number, so not NaN and not Infinity. array takes an array whose every item fits of. map takes an object whose every value fits of. Lists and maps only hold the four scalar types: a list of lists can't be declared. On the bot's page, a new item starts at false, "" or 0, depending on of, and a map row whose key is left empty isn't saved. Clearing a number box doesn't store an empty value: the box keeps the last number typed in it. Choices choices turns a string or int setting into a drop-down list. Each choice is either a bare value, which is then its own label, or an object with a value and a label: /** @type {BehaviorParameters} */ export const parameters = { slot: { type: "int", label: "Spell slot", choices: [1, 2, 3], default: 2, }, tone: { type: "string", label: "Tone", choices: [ { value: "polite", label: "Polite" }, { value: "short", label: "Short" }, ], default: "polite", }, }; The script reads the value, never the label: values.tone is "polite", not "Polite". The rules: Only string and int settings can have choices. Every value must fit the setting's type. "2" isn't a valid choice for an int. A choice object without a value is refused. A choice without a label shows its value. The default must be one of the values. Without a default, the setting starts at the first choice. Defaults The default is what a bot reads until someone applies another value. Without a default, or with default: null, a setting starts at its type's value from the last column of the table above, or at its first choice. The default has to fit the declaration exactly like a value typed on the bot's page. Asterobot never converts it: default: "20" on an int is refused, and so is a default below min or above max. A default can be any expression, as long as it doesn't need a setting's value or the session. A default only matters while a bot has no saved value for the setting. Asteroboard saves every setting of a package at once, the untouched ones included, when someone clicks Apply and when a bot is added with the package, so a default you change in a later version doesn't reach those bots. Read settings goes through what happens to saved values. When Asterobot checks the declaration Asterobot reads and checks the declaration each time it needs it: when a bot's Settings tab shows Package settings, when someone picks the package in the Add a bot dialog, and at every start of the script. Saving in the editor checks nothing. After changing a declaration, save, then open Package settings on a bot set to your package and click Refresh. Problems fall into two groups: Problem Examples What happens A setting breaks a rule An unknown type, a default that doesn't fit, choices on a bool, min above max Package settings and the Add a bot dialog show Couldn't read this package's settings with the reason. The script still starts, but with no settings at all: every read from values gives undefined. The export itself has the wrong shape parameters isn't an object, or one of its settings isn't an object The same message on Package settings, and the script doesn't start: the Problems alert shows a text starting with read declarations of. The reason names the package, then the setting, then what's wrong. With default: "friendly" on the tone setting above, Package settings shows: chat-helper@1.0.0: read declarations: package "chat-helper" parameter "tone": parameter "tone" default: friendly is not one of polite, short Declarations lists every text you can get. Typing with JSDoc Put /** @type {BehaviorParameters} */ on the line above export const parameters, as in the examples on this page. The editor then completes the field names, and marks as errors a field that doesn't exist and a type or of that isn't one of the allowed names: mistakes Asterobot would ignore silently, or only report once it reads the declaration. It doesn't check that a default fits its type, so Asterobot's own check still matters. These types need no import: Type What it describes BehaviorParameters The whole parameters object BehaviorParameter One setting, or one argument of an action BehaviorParameterType "bool", "string", "int", "double", "array" or "map" BehaviorScalarType "bool", "string", "int" or "double", the types of accepts BehaviorChoice A choice written as an object, with a value and an optional label Next, Read settings shows how the script uses the values. The arguments of actions are declared with the same fields.
  8. Smooth a posté un record dans Données de jeu
    Some questions need a join, a count or a sort that the lookups can't do. query() runs SQL on the bot's game data and hands back the result. Queries are written in SQLite's dialect of SQL, the same one the game version's SQL tab runs. query() query(sql, params, options) resolves to an object with three properties: Property What it holds columns The name of each column, in the order the query selects them. A name can appear twice, as in SELECT a._id, b._id. rows One array per row, holding the values in the same order as columns truncated true when the query had more rows than the limit let through Option Default What it does limit 200 How many rows to return, 5,000 at most Rows are arrays rather than objects because column names can repeat, so read the values by position: const result = await query( "SELECT language, name FROM _names WHERE record_table = ? AND record_id = ? ORDER BY language", ["monsters", 147], ); for (const [language, name] of result?.rows ?? []) { botInfo(language, name); } This query lists the names of one monster in every language the game data holds, which is also how a script reads a language other than its own. query() resolves to undefined when the bot's game version has no game data, hence result?.rows. Read-only The game data is opened read-only. INSERT, UPDATE, DELETE, CREATE and every other statement that writes fail, and ATTACH is refused too, so a query can't reach any other database file. Every statement in the string runs, and query() returns the result of the last one: SELECT 1; SELECT 2 returns 2. Since nothing can be written, the statements before the last one only cost time. Parameters Write ? wherever a value goes, and pass the values in the params array, in the same order. A parameter keeps its type, so a number is compared as a number, whereas a value written into the SQL between quotes, like '147', is text. params has to be an array: anything else rejects with gamedata.query params must be an array. Pass numbers and strings, and convert BigInt ids with Number() first. Writing the SQL Put text between single quotes, as in 'monsters'. Double quotes only name a table or a column, so "monsters" is never a string, and a mistyped column name gives an error instead of a wrong result. Names go through the _names table, with the columns record_table, record_id, name_id, name, language and ordinal. A record can have several names, and ordinal 0 is its main one. The texts of one language are in the table text_ followed by the language code, such as text_fr, with an id and a text column. In a derived table, whose name contains two underscores, each row points to its parent row with _parent_id. _names_fts is a full-text index of the names, much faster than LIKE '%...%'. The SQL tab's Examples show how to use it. Values come back the way they're stored: numbers as regular numbers, never BigInt, text as strings, and an empty value as null. A column holding nested data comes as an array or an object. A computed column, such as COUNT(*), comes back as the database computed it. Limits and errors Limit Value Past it Rows 200 unless you pass a limit, 5,000 at most The other rows are left out, and truncated is true. Time 10 seconds for the query The query is interrupted and the promise rejects. Length of the SQL 64 KiB The promise rejects with a text such as datacenter: query is 70000 bytes, over the 65536 byte limit. Size of one value the query builds 16 MiB The query fails. Queries at once 4 on one game version, shared by every bot and every open SQL tab Other queries wait for their turn. Calls in progress 32 for a script, counted together with send(), request() and the other game data calls The call rejects with behavior resource limit exceeded: maximum pending operations reached. Problem The promise rejects with sql is empty or isn't a string gamedata.query sql must be a non-empty string sql only holds spaces datacenter: empty query params isn't an array gamedata.query params must be an array A mistake in the SQL A text starting with datacenter: that names the problem, such as no such column: nmae Try it in the SQL tab first The game version's SQL tab runs queries on the same data, with the same limits, and shows the result as a table. It's the quickest way to get a query right before putting it in a script. In Game Manager, open the bot's game version, go to its Game data tab, then to SQL. Type a query, or pick one from Examples. They're grouped by topic: explore, names, monsters, items, harvesting, navigation, crafting, maps, quests and text. Click Run, or press Ctrl+Enter. Next to the buttons, the tab shows the number of rows, stopped at the limit when some were left out, and how long the query took. When the query returns what you want, copy it into your script, and replace the values you typed with ? and params. The tab has no parameters, so write the values into the SQL while you try a query there. It also shows up to 500 rows, where a script gets 200 unless it passes a limit. When a query fails, the tab shows the same text a script would get. SQL describes the tab in full. A complete example This package adds a button that lists the sub-areas where a monster lives, with a join the lookups can't do: import { session, botInfo } from "asterobot:bot"; import { query } from "asterobot:gamedata"; import { send } from "asterobot:protocol"; const SUB_AREAS_OF_MONSTER = ` SELECT DISTINCT area.name FROM _names monster JOIN monsters__subareas link ON link._parent_id = monster.record_id JOIN _names area ON area.record_table = 'sub_areas' AND area.record_id = link.value AND area.language = monster.language WHERE monster.record_table = 'monsters' AND monster.language = ? AND monster.name = ?`; export const actions = { subAreas: { label: "Find where a monster lives", args: { monster: { type: "string", label: "Monster name" }, }, async run({ monster }) { const result = await query(SUB_AREAS_OF_MONSTER, [session.language, monster]); if (result === undefined) throw new Error("No game data for this bot's game version"); const areas = result.rows.map(([name]) => name); botInfo(`${monster}:`, areas.length > 0 ? areas.join(", ") : "no sub-area found"); }, }, }; export default async function behavior(launch) { // A new game connection starts with this message. MITM bots never launch // with "initial": the Dofus client has already identified their session. if (launch.reason === "initial") { await send("IdentificationRequest", { ticketKey: session.gameToken, languageCode: session.language, }); } } The query takes the script's language as its first parameter, so type the monster's French name for now. The button appears under Package settings on the bot's Settings tab once the package runs, as in Lookups.
  9. Smooth a posté un record dans Données de jeu
    Five functions of asterobot:gamedata cover most of what a script looks up: a text by its id, a record by its key, a whole table, the rows matching some values, and records by their name. import { text, record, table, find, search } from "asterobot:gamedata"; What a row looks like record(), table() and find() return rows as plain objects: The keys are the table's column names, as the Game data tab shows them, such as name_id. A column with no value is left out of the object, so reading it gives undefined. Numbers are regular numbers, not BigInt, large ids included. A column holding nested data comes as an array or an object, whose fields keep the game's own names. In a derived table, a table whose name contains two underscores, each row points to its parent row with _parent_id. Most tables key their rows with an _id column, and the others with their first column. That key is what record() looks up. The key and the game's own id _id is the key Asterobot gives each row. Some tables also have an id column that comes from the game, and an id you read in a message can refer to either one. The example queries of the SQL tab, for instance, find an item type with WHERE id = ... before using its _id. Check in the Game data tab which column your value matches, then use record() for the key, and find() for any other column: const byKey = record("item_types", 48); const [byGameId] = (await find("item_types", { id: 48 })) ?? []; search() and the _names table give you keys directly, ready for record(). text() text(key) returns one game text, or undefined. Argument What it looks up A whole number The text with that numeric id, such as the value of a name_id column A string The text with that text key, such as ui.common.classic It returns undefined when no text has that key, and for every key when the game's text file for the script's language is missing. Any other argument, such as a decimal number or a BigInt, throws TypeError: gamedata.text key must be a string or integer. Texts come in the language the script was started with, which is fr for every start made from Asteroboard today. record() record(tableName, key) returns one row, or undefined when the table has no row with that key. const monster = record("monsters", 147); if (monster) { botInfo(text(monster.name_id)); } Problem What it throws, right away The table name is empty or isn't a string TypeError: gamedata.record table must be a non-empty string The key isn't a whole number, a BigInt included TypeError: gamedata.record id must be an integer This game version has no table with that name An error with the message datacenter: no such table: "monstres" Ids in message payloads are often BigInt values. Convert one with Number() before looking it up, as in record("monsters", Number(id)). table() table(tableName, options) resolves to an array of rows, sorted by key. Option Default What it does limit 200 How many rows to return, 5,000 at most offset 0 How many rows to skip first A limit or an offset that isn't a positive whole number is ignored, and its default applies. Read a large table a page at a time: for (let offset = 0; ; offset += 1000) { const rows = await table("items", { limit: 1000, offset }); if (!rows || rows.length === 0) break; botInfo("Read", rows.length, "items starting at", offset); } table() resolves to undefined when the bot's game version has no game data. It rejects with gamedata.table name must be a non-empty string for an empty name, and with datacenter: no such table: "itmes" for a table that doesn't exist. find() find(tableName, filter, options) resolves to the rows whose columns hold every value in filter. const spawns = await find("sub_areas__monsters", { value: 147 }); Each row found here links a sub-area, its _parent_id, to the monster whose key is 147. Derived tables like this one exist to answer such reverse questions. Option Default What it does limit 200 How many rows to return, 5,000 at most find() has no offset. Pass numbers for columns that hold numbers, and strings for columns that hold text. Problem The promise rejects with The table name is empty or isn't a string gamedata.find table must be a non-empty string filter is empty or isn't an object gamedata.find filter must be a non-empty object The table doesn't exist datacenter: no such table: "sub_area_monsters" The table has no column with that name datacenter: no such column: table "sub_areas__monsters" has no column "monster" Like table(), find() resolves to undefined when there's no game data. search() search(query, options) resolves to the records whose name matches the query, best matches first: [ { table: "monsters", id: 147, nameId: 7217, name: "Bouftou Royal" }, // ... ] Field What it holds table The table of the record id The record's key, ready for record(hit.table, hit.id) nameId The id of the game text the name comes from name The name, in the script's language Option Default What it does table Every table Only searches the records of this table limit 200 How many results to return, 5,000 at most Each word of the query matches as the start of a word, so bouftou roy finds Bouftou Royal, and shorter names come first. Without table, the results take turns between the tables that have a match, so the first results show the different kinds of things with that name. A very short query matches so many names that only the first 2,000 are ranked, and the one you want can be missed: add a letter, or a table. Names are in the script's language, fr for a script started from Asteroboard today, so search for French names. Problem The promise rejects with The query is empty or isn't a string gamedata.search query must be a non-empty string options.table isn't a string gamedata.search options.table must be a string The table doesn't exist datacenter: no such table: "monstres" The game data has no text in the script's language datacenter: no text for this language: "fr" search() resolves to undefined when there's no game data. A complete example This package adds a button that looks monsters up by name and writes what it finds to the bot's console: import { session, botInfo } from "asterobot:bot"; import { record, search } from "asterobot:gamedata"; import { send } from "asterobot:protocol"; export const actions = { findMonster: { label: "Find a monster", args: { name: { type: "string", label: "Name" }, }, async run({ name }) { const hits = await search(name, { table: "monsters", limit: 5 }); // undefined rather than an empty array: this game version has no game data. if (hits === undefined) throw new Error("No game data for this bot's game version"); for (const hit of hits) { const monster = record(hit.table, hit.id); botInfo(`${hit.name}: key ${hit.id}, race ${monster?.race}`); } }, }, }; export default async function behavior(launch) { // A new game connection starts with this message. MITM bots never launch // with "initial": the Dofus client has already identified their session. if (launch.reason === "initial") { await send("IdentificationRequest", { ticketKey: session.gameToken, languageCode: session.language, }); } } Run the package on a bot, open the bot's Settings tab, and click Find a monster under Package settings. Type a name, in French for now, and click Run in the dialog. An error in run, such as a misspelled table name, shows as a toast and doesn't stop the script. Declare actions covers buttons in detail. For questions these five functions can't answer, such as a join or a count, see SQL queries.
  10. Smooth a posté un record dans Données de jeu
    Messages are full of numbers that stand for something: an item, a monster, a map, a line of text. asterobot:gamedata lets a script look those up in the game data Asterobot extracted from the bot's game version. What a script can read Function What it reads Result text() One game text, by its numeric id or its text key Returned right away record() One row of a table, by its key Returned right away table() The rows of a table, a page at a time A promise find() The rows of a table whose columns hold given values A promise search() Records, by their name A promise query() The result of a read-only SQL query A promise Lookups covers the first five functions, and SQL queries the last one. The tables hold what Asterobot extracted from the game: items, monsters, spells, sub-areas, maps and much more. Some mirror the game's own data. Others are derived by Asterobot to answer questions the game's files can't, such as which maps hold a given resource. Their names contain two underscores, like maps__interactive_elements. The _names table links every name to its record, in every extracted language. To see which tables exist and what their columns are called, open the bot's game version from Game Manager and go to its Game data tab, described in Game data. The editor's Game panel has the same browser under Datacenter, but it shows the game version this Asterobot is tested against, which isn't always your bot's. Table and column names come from the game's own data, so they can change when Ankama updates the game. After a game update explains how to keep scripts working through updates. Which game version and which language A script reads the game data of the bot's game version, the one set on the bot. text() and search() answer in the language the script was started with, session.language. Right now that's fr for every start made from Asteroboard, whatever language the bot itself is set to, as Identify and run says. So these two functions return French texts and look for French names. To read another language, use query() on the _names table, which has a language column, or on the text table of that language: SQL queries shows how. When game data is available A game version has game data once it has been extracted, as Extract game data explains. A Full socket bot can't connect before that. A MITM bot (man-in-the-middle: the bot relays the game session of a Dofus client someone plays) can, so its script may run without any game data. When a script starts, Asterobot opens the game data of the bot's game version, and the game's text file for the script's language. If one of them isn't there, the script starts anyway: Missing when the script started What the script gets, until its next start The extracted game data record() returns undefined, and table(), find(), search() and query() resolve to undefined instead of results. The game's text file for the script's language text() returns undefined for every key. Extracting the game data while such a script runs doesn't change anything for that run: start the script again. The other way round works: when a game version is extracted again while a script runs, the script's next lookup reads the new data. To tell missing game data apart from an empty result, compare with undefined: const rows = await table("breeds", { limit: 1 }); if (rows === undefined) { botWarn("This bot's game version has no extracted game data."); } Right away or with a promise text() and record() return their result directly. The texts are already in memory, and a record is one indexed read, fast enough to call inside a loop. Their time still counts toward the 250 ms limit of the code calling them, described in Async and timing, so thousands of calls in a row can go over it. table(), find(), search() and query() return promises. They can return thousands of rows, so their work happens outside your code, and the bot keeps handling messages while they run. Limits every lookup shares Limit Value Past it Time for a table(), find(), search() or query() call 30 seconds The promise rejects. No option changes this time. Time for the SQL of a query() 10 seconds The query is interrupted and the promise rejects. Rows returned by one call 200 unless you pass a limit, 5,000 at most Only the first rows come back. query() sets truncated. Calls in progress at once 32, counted together with send() and request() The call rejects with behavior resource limit exceeded: maximum pending operations reached. Queries running at once on one game version 4, shared by every bot and every open SQL tab Other queries wait for their turn, within their time limits. Limits lists every limit, and the asterobot:gamedata reference every function. Next, Lookups.
  11. Smooth a posté un record dans Protocole du jeu
    Ankama patches Dofus regularly, and a patch can change the game's messages. Asterobot absorbs most of those changes before they reach scripts, but not all of them. What a patch changes Messages carry scrambled names on the wire, and those names change between game versions. Asterobot translates them into readable names, so as long as it knows the game version, a script that works with type and payload field names doesn't see the change. A patch can also add messages, remove some, or change their fields. A new message has no readable name until Asterobot gives it one, and reaches scripts with an empty type until then. Readable names can change too, with an Asterobot update, when a message gets a more accurate name. A script using the old name then stops matching the message, and sending under the old name fails. When a bot had the old name selected in its Network tool, Asterobot removes it from the selection the next time it loads the bot, such as when Asterobot starts. While Asterobot doesn't know the new version Right after a patch, Asterobot may not know the new version's names yet. When Dofus updates explains what to do from the player's side. Until Asterobot knows them, a message it can't name reaches scripts this way: In your script What happens on("InventoryWeightEvent", handler) The handler never runs: the message arrives without its name. wait("InventoryWeightEvent", { timeout: 10000 }) The wait rejects with protocol wait timed out when its timeout runs out. onTraffic(handler) or on("*", handler) The handler receives the message with an empty type, unknown set to true, no payload, and a decodeError that starts with obfuscated protobuf wire name is not mapped:. Some messages arrive like this at any time, since only part of the protocol has readable names. After a patch, messages your script used to receive by name can join them. Traffic describes these messages in full. Check a script after an update Update Asterobot when a new version is out, and download the new game version: see Updating and When Dofus updates. Open your package in the editor. The editor loads the message names of the Asterobot it's connected to, so a handler that reads the fields of a renamed message gets errors on the error badge. Run the package on a MITM bot (man-in-the-middle: the bot relays the game session of the Dofus client you play), and play through what the script handles. In the bot's Network tool, select the messages the script relies on and turn on Also mirror unrecovered messages. A message that lost its name appears in Traffic with its wire name in italics. Read the bot's console for the warnings your script writes when something it expects doesn't happen, as in the example below. Write scripts that keep working Rely on type and on payload field names. Don't compare wireType or typeUrl, don't read fixed positions in rawAny, and don't use scrambled field names such as fzbl: all of those change with game versions. Read every field with ?., and give defaults with ??. A field that's gone reads as undefined, which doesn't throw. Give every wait() a timeout, and write something useful when it runs out: a message that lost its name never arrives. Catch errors from send(). semantic protobuf message is not mapped: means the name you send doesn't exist in this version of Asterobot. Say so on the console when expected messages stop coming, instead of failing silently. Game data can change too: its tables and columns come from the game files and follow the game version. See Game data overview. This script puts two of those habits to work. On a Full socket bot, it warns when the game doesn't confirm the identification, for example because a message name no longer matches. On both kinds of bot, it writes one console line for each message it can't name. import { session, botDebug, botWarn } from "asterobot:bot"; import { onTraffic, send, wait } from "asterobot:protocol"; export default async function behavior(launch) { const unnamed = new Set(); onTraffic((traffic) => { // One line per wire name is enough to see what lost its name. if (!traffic.unknown || unnamed.has(traffic.wireType)) return; unnamed.add(traffic.wireType); botDebug(`No name for ${traffic.direction} message ${traffic.wireType}`); }); if (launch.reason === "initial") { // Started before identifying, so a quick answer can't be missed. The catch // handles the timeout without holding up the rest of the script. wait("AuthenticationTicketAcceptedEvent", { timeout: 10000 }).catch((error) => { botWarn("The game didn't confirm the identification:", String(error)); }); } // A new game connection starts with this message. MITM bots never launch // with "initial": the Dofus client has already identified their session. if (launch.reason === "initial") { await send("IdentificationRequest", { ticketKey: session.gameToken, languageCode: session.language, }); } } AuthenticationTicketAcceptedEvent is the message the game server sends once it has accepted the ticket. compatibleVersion A package can declare which Asterobot versions it works with, in the compatibleVersion field of its asterobot.json. Asterobot refuses to start a package whose compatibleVersion doesn't include the version it runs, and the editor shows Incompatible for it. It's about Asterobot versions, not Dofus versions, so it can't tell that the game changed. Use it when your script depends on something a given Asterobot version brought, such as a message's new name. Compatibility explains how to write it. That's the end of the protocol chapter. The next one, Game data, covers reading items, monsters, maps and texts from a script.
  12. Smooth a posté un record dans Protocole du jeu
    A script runs on one bot's game connection, its session. session, from asterobot:bot, describes it, and a few things work differently on a Full socket bot and on a MITM bot. How bots talk to Dofus explains both modes; this page covers what they change for your code. session import { session } from "asterobot:bot"; Field Value What it holds gameToken string The ticket of this game connection, which IdentificationRequest sends. Keep it secret: see Keep the game ticket secret. serverId number The id of the game server the session is on. It doesn't change while the connection lasts. language string The language code the script was started with, such as fr. launchReason "initial", "resume" or "reload" Why the script started: the same value as launch.reason, described in Launch reasons. behaviorGeneration BigInt The same value as launch.generation: 1n for the first start on this game connection, one more for each start after it. shared boolean true on a MITM bot, where a person plays the same character. false on a Full socket bot. session is filled in when the script starts and doesn't change while it runs. It's frozen: assigning to one of its fields throws a TypeError. Each start gets a new session. Keep the game ticket secret Caution session.gameToken is the ticket to the bot's game session, and a stranger who has it could take that session over. Only ever put it in IdentificationRequest. Never log it, write it in a chat message, or put it in any other message. The ticket can also show up where you don't expect it. With IdentificationRequest selected in the Network tool, Traffic shows ticketKey: the one your script sends on a Full socket bot, or the one your Dofus client sent on a MITM bot. Don't share screenshots or copies of those rows. Full socket and MITM, for a script The functions are the same on both kinds of bot, and a script written for one runs on the other. What differs is who else takes part: Full socket bot MITM bot First launch.reason on a connection "initial" "resume" session.shared false true Who identifies the session Your script The Dofus client. A script's IdentificationRequest isn't sent: see Messages that aren't sent. Whose character send() speaks for The bot's The person playing, next to what their client sends send() with { to: "client" } Reaches your script only Reaches the Dofus client and your script Outbound messages in onTraffic() What scripts and the Network tool send Also everything the Dofus client sends What interceptors decide about Messages from the server Messages from the server and from the Dofus client When Asterobot can't keep up with the traffic It reads the connection more slowly, and no message is skipped The game goes on, and messages are skipped for scripts and the Network tool disconnect() Disconnects the bot Ends the game of the person playing On a MITM bot, Traffic in the Network tool warns when messages were skipped, with how many, saying that the bot's session discarded them because something reading the traffic couldn't keep up. On either kind of bot, a script that falls too far behind the traffic stops with behavior event-loop queue overflow: see Listening. Sharing a character On a MITM bot (man-in-the-middle: the bot relays the game session of the Dofus client you play), the person playing and your script control the same character, and nothing coordinates them. They move, fight and open windows while your script runs, and the server receives messages from both. A script that only watches, built from on(), wait(), onTraffic() and logging, works the same on both kinds of bot and has nothing to check. A script that acts competes with the person playing. Check session.shared to leave the controls to them: on("ChatChannelMessageEvent", async (traffic) => { // Someone is at the keyboard: let them do the talking. if (session.shared) return; // ...answer the message }); Everything the script sends goes out as their character, and other players see it. An interceptor makes each of their messages wait for your script, up to 50 ms: see Intercepting. Stopping the script doesn't touch their game: the session goes on without it. When the connection ends A script lives on one game connection. When the connection ends, because the server closed it, the network dropped, or the person playing closed Dofus, the script stops: its waits and sends reject, and the bot's page shows The behavior stopped with an error, with an error that starts with Game session closed:. Nothing starts again on its own. Connect the bot again, or on a MITM bot wait for the next Dofus session, then start the script. Disconnect, delete and restart covers it from the bot's side. disconnect() import { disconnect } from "asterobot:bot"; disconnect(); disconnect() closes the bot's game connection. It returns right away, and calling it again does nothing. The script stops as soon as the connection is closed, so treat disconnect() as the last thing it does. The bot's page then shows The behavior stopped with an error, with behavior requested Game disconnect. What it closes depends on the bot: On a Full socket bot, the bot leaves the game server. It doesn't reconnect on its own. On a MITM bot, it closes both sides of the relayed session. Warning On a MITM bot, disconnect() ends the game of the person playing: their Dofus client loses its connection to the server. Check session.shared before calling it. This script disconnects a Full socket bot when its inventory is full, and only warns on a MITM bot: import { session, botWarn, disconnect } from "asterobot:bot"; import { on, send } from "asterobot:protocol"; export default async function behavior(launch) { on("InventoryWeightEvent", (traffic) => { const weight = traffic.payload?.inventoryWeight; const max = traffic.payload?.weightMax; if (weight === undefined || !max || weight < max) return; // A full inventory ends this bot's session, never a player's. if (session.shared) { botWarn("Inventory full"); return; } botWarn("Inventory full, disconnecting"); disconnect(); }); // A new game connection starts with this message. MITM bots never launch // with "initial": the Dofus client has already identified their session. if (launch.reason === "initial") { await send("IdentificationRequest", { ticketKey: session.gameToken, languageCode: session.language, }); } } Next, After a game update explains what changes for scripts when Ankama patches Dofus, and the asterobot:bot reference has every signature.
  13. Smooth a posté un record dans Protocole du jeu
    A message's fields travel as protobuf values, and scripts work with JavaScript values. Asterobot converts them both ways: when a script reads traffic.payload, and when it builds a payload for send(), request() or an interceptor's { payload }. The two directions aren't symmetric, and this page lists every rule. The Dofus protocol reference gives the type of each field. Types at a glance Field type Read as Accepted when sending string A string A string. bool A boolean A boolean. int32, sint32, sfixed32 A number A whole number from -2147483648 to 2147483647. uint32, fixed32 A number A whole number from 0 to 4294967295. int64, sint64, sfixed64 A BigInt A BigInt, a decimal string such as "123456789012", or a whole number between -Number.MAX_SAFE_INTEGER and Number.MAX_SAFE_INTEGER. uint64, fixed64 A BigInt The same, without negative values. float, double A number A number. bytes A Uint8Array A Uint8Array or an ArrayBuffer. An enum The value's name, such as "GUILD" The value's name, or its number. A message An object An object, or null to leave the field unset. A repeated field An array An array. A map An object An object. Field names Payloads always read with camelCase names: the field sender_name reads as senderName. When sending, both spellings work, so { ticketKey: session.gameToken } and { ticket_key: session.gameToken } set the same field. Give each field only once: the same field under both spellings is an error, even with the same value. Every key of the object you send must be a field of the message. An unknown key is an error, not something Asterobot skips. Present, absent and zero When reading Plain fields are always there. A field the server didn't set reads as its zero value: "", 0, 0n, false, an empty array or object, or for an enum, its first value, such as "GLOBAL" for a chat channel. Fields marked optional in the protocol reference, and fields that hold a message, are only there when they're set. In ChatChannelMessageEvent, for example, originServerId only appears on lines from the cross-server community channels. In a oneof, only the member that's set is there. A field is never null, and never present with the value undefined. An absent field reads as undefined because it isn't there, so read fields with ?., and give defaults with ??: // No originServerId means the line was said on this server. const server = traffic.payload?.originServerId ?? session.serverId; For plain fields, a zero value and an unset field look the same: a 0 may have been sent as 0, or not sent at all. When sending Leave a field out to leave it unset. A plain field then arrives as its zero value. undefined as a value is refused, even though the field would be left out anyway. Watch for variables that can be undefined, and only add the fields you have a value for. null is accepted only for a field that holds a message, and leaves it unset. On any other field, it's refused. const payload = { channel: "PARTY", content: text }; // A metadata key set to undefined would be refused. if (metadata) payload.metadata = metadata; await send("ChatChannelMessageRequest", payload); Numbers and BigInt 64-bit fields read as BigInt values, whatever their size: const id = traffic.payload?.senderCharacterId; // for example 123456789012n if (id === 123456789012n) { // A BigInt equals another BigInt, never a number. } When sending, a 64-bit field takes a BigInt, a decimal string, or a number that's a safe integer. A bigger number is refused, because a JavaScript number can't hold it exactly. 32-bit fields work the other way: they read as numbers and refuse BigInt values. Convert with Number() before putting a BigInt in a 32-bit field. The JavaScript you need covers BigInt itself. Enums An enum field reads as the name of its value, such as "GUILD". When sending, give the name or the value's number. Names are case-sensitive: "guild" is refused. A value Asterobot has no name for reads as its number. Some enum values still have scrambled names, which can change: don't rely on those. Lists A repeated field reads as an array and takes an array. Each item follows the rules of the field's type, and an error in an item gives its position after the field name, such as [2]. Maps A map field reads as a plain object. Its keys are always strings, even when the map's keys are numbers or booleans: the key 12 reads as "12", and true as "true". When sending, give an object with keys written the same way. An error in an entry gives its key after the field name, such as ["12"]. Nested messages A field that holds another message reads as an object with that message's fields, which follow the same rules, and takes an object when sending. Errors give the whole path, from the message name down to the field, separated by dots. Oneofs A oneof is a group of fields of which at most one is set. ChatPrivateMessageRequest has one called target: a private message goes to a character, with name, or to an account, with tag. When reading, only the member that's set is in the payload. When sending, set one member. Setting two is an error, and the editor marks it too. await send("ChatPrivateMessageRequest", { name: "Airelle", content: "On my way" }); Errors A payload that doesn't fit its message makes send() and request() reject, before anything is sent. In an interceptor's { payload }, the same error shows on the bot's console after build replacement payload for and the message name, and the original message continues. The Network tool's Send a game message checks payloads its own way, with different texts: see Send messages by hand. Every error starts with the path to the problem: the message name, then the fields as you wrote them, with list positions and map keys. Error Cause ChatChannelMessageRequest: expected an object The payload is missing, null or undefined. ChatChannelMessageRequest: expected an object, got string The payload isn't an object. The last word says what it was instead. ChatChannelMessageRequest.contnt: unknown protobuf field The message has no such field. IdentificationRequest: fields "ticketKey" and "ticket_key" address the same protobuf field The same field under both spellings. ChatPrivateMessageRequest: oneof target selects both "name" and "tag" Two members of one oneof. ChatChannelMessageRequest.content: explicit undefined is not a protobuf value A field set to undefined. ChatChannelMessageRequest.content: null is only valid for a message field null on a field that doesn't hold a message. ChatChannelMessageRequest.content: expected string A string field got another type. PingRequest.quiet: expected boolean A boolean field got another type. InventoryWeightEvent.weightMax: expected integral Number A 32-bit integer field got a number with decimals, a BigInt, or another type. InventoryWeightEvent.weightMax: Number is outside int32 range The number is too big or too small for a signed 32-bit field. ChatChannelMessageEvent.senderCharacterId: expected signed 64-bit BigInt or decimal string A signed 64-bit field got a number that isn't a safe integer, a string that isn't a whole decimal number, a value out of range, or another type. ChatChannelMessageRequest.channel: unknown enum symbol "GENERAL" The enum has no value with that name. ChatChannelMessageRequest.channel: expected enum symbol or number An enum field got something other than a name or a whole number. The other field types give these texts after the field's path: Text after the path Cause expected integral Number in uint32 range An unsigned 32-bit field got a negative number, a number with decimals, a number too big, or another type. expected unsigned 64-bit BigInt or decimal string The same problems as for a signed 64-bit field, or a negative value. expected Number A float or double field got something other than a number. expected Uint8Array or ArrayBuffer A bytes field got something else. expected an array A repeated field got something that isn't an array. invalid array length A repeated field got an object whose length isn't a whole number from 0 up. expected a plain object map A map field got something that isn't an object. invalid int32 map key "abc" A map key that doesn't fit the map's key type. The text names that type: int32, int64, uint32, uint64 or boolean. Next, Sessions covers what a script knows about its game connection, and Error messages lists every error text a script can meet.
  14. Smooth a posté un record dans Protocole du jeu
    Every other function in this chapter observes messages that have already gone through. An interceptor is asked first, and its answer decides whether a message continues, and in what form. On a MITM bot (man-in-the-middle: the bot relays the game session of the Dofus client you play), that decides what your Dofus client and the game server receive, so read On a MITM bot before using one there. intercept() intercept(selector, handler); // asked about the messages the selector matches intercept(handler); // asked about every message intercept() registers handler and returns a handle to pass to off(). The selector is the same as for on(): a message name, "*", or a function. Listening describes them. Interceptors count towards the 256 handlers a script can register with on() and onTraffic(), and registering one throws the same errors, with protocol.intercept handler must be callable when the handler isn't a function. The handler receives the message's traffic object and returns a decision: Return What happens to the message Nothing, undefined, or any other value No decision. The next interceptor is asked, and when none decides, the message continues unchanged. "drop" The message stops here. { payload: { ... } } The message continues with these fields instead of its own. See Changing a payload. { raw: bytes } The message is replaced by these bytes. See Raw replacements. What "continues" covers depends on the bot. On a Full socket bot, the message reaches your script's handlers and waits, and the bot's page in Asteroboard: the Network tool, live chat and live inventory. On a MITM bot, it also reaches the other side, your Dofus client or the game server. A dropped message reaches none of them, and keeps its sequence number, which leaves a gap. What an interceptor is asked about Bot Messages interceptors decide about Full socket Messages from the game server. MITM Messages from the game server to your Dofus client, and from your Dofus client to the server. Interceptors are never asked about: messages a script sends with send() or request(), towards the server or the client, which is why an interceptor can call send(); messages someone sends from the Network tool's Send a game message; on a MITM bot, the responses to your script's own request() calls, and the first message of the session, your client's IdentificationRequest. Those messages still reach onTraffic() and the Network tool as usual. Answer within 50 ms While an interceptor is registered, each message it could decide about waits for your script's answer, for 50 ms at most. After that, the message continues unchanged, and an answer that comes later is ignored. The handler still runs to its end, so anything it does besides answering, such as writing to the console, still happens. The 50 ms start when the message arrives, not when your handler is called. Interceptors run in the same queue as the rest of your script: when a handler or other code is busy, the time can run out before your interceptor is even asked. Nothing reports a late answer. Three things follow: Keep interceptors short. Do the quick test in the interceptor, and anything slow somewhere else, such as an on() handler. An interceptor must answer synchronously. An async function returns a promise, and a promise isn't a decision, so the message always continues unchanged. An error inside an async interceptor also becomes an unhandled rejection, which stops the script. Once a script has an interceptor, every message of the kinds above waits for the script, even the ones its selector doesn't match, because the selector is checked by the script too. Remove interceptors you no longer need with off(). The first decisive answer wins Interceptors are asked in the order they were registered. The first one that returns "drop", { payload } or { raw } decides, and the ones after it aren't asked. An interceptor whose selector doesn't match isn't asked, and doesn't count as an answer. An interceptor that fails doesn't decide either, and the next one is asked. That covers a handler that throws, a function selector that throws, and a { payload } that can't be built. When an object holds both raw and payload, raw wins. Changing a payload Return { payload } to let a message continue with other values. This interceptor defangs links in the chat lines your handlers receive, and on a MITM bot, the lines your Dofus client shows: intercept("ChatChannelMessageEvent", (traffic) => { const content = traffic.payload?.content; if (typeof content !== "string" || !content.includes("http")) return undefined; // The spread keeps every other field: the replacement is built from this object alone. return { payload: { ...traffic.payload, content: content.replaceAll("http", "hxxp") } }; }); Rules for { payload }: The message is rebuilt from your object alone. A field you leave out is sent unset, or at its zero value, so start from ...traffic.payload. Bytes of the original that Asterobot's definitions don't describe can't be carried over. The message keeps its type, its kind, its uid and its wire name. To send a different message, drop this one and call send(). A message without a readable name can't get a new payload. Your handlers and the Network tool receive the replacement, not the original. When the replacement can't be built, the original message continues unchanged, and the bot's console says why, as in build replacement payload for ChatChannelMessageEvent: ChatChannelMessageEvent.contnt: unknown protobuf field. For a message without a readable name, the line starts with cannot replace the payload of followed by its wire name. Dropping a message Return "drop" to stop a message. On a MITM bot, this script turns the chat lines you type starting with ! into commands for the script: they never reach the server, so other players don't see them. import { session, botInfo } from "asterobot:bot"; import { intercept, send } from "asterobot:protocol"; export default async function behavior(launch) { intercept("ChatChannelMessageRequest", (traffic) => { const content = traffic.payload?.content; if (typeof content !== "string" || !content.startsWith("!")) return undefined; // Writing a line is quick enough to do before answering. botInfo("Command:", content); return "drop"; }); // A new game connection starts with this message. MITM bots never launch // with "initial": the Dofus client has already identified their session. if (launch.reason === "initial") { await send("IdentificationRequest", { ticketKey: session.gameToken, languageCode: session.language, }); } } On a Full socket bot, this interceptor is never asked anything: every message going to the server there comes from a script, and those aren't intercepted. On a Full socket bot, dropping a message keeps it from your own handlers, which makes an interceptor a filter. Dropping a response that a request() is waiting for makes that request() time out. Raw replacements { raw } replaces a message with the bytes you give, as a Uint8Array or an ArrayBuffer. Any other value doesn't count as an answer. The bytes replace the whole message as it travels: the part that states the message's kind, uid and wire name, and the fields inside it. That's more than traffic.rawAny, which only holds the fields, and Asterobot has no function that builds those bytes for you. { raw } is only for a message nothing else can handle, when you know that format yourself. When Asterobot can't read your bytes as a message, your handlers receive the original message. On a MITM bot, the bytes still go to the other side as they are. Warning On a MITM bot, a wrong { raw } sends your Dofus client or the game server bytes they can't read. When an interceptor fails Unlike an on() handler, an interceptor that throws doesn't stop the script, unless it's async (see Answer within 50 ms). The message continues unchanged, and the bot's console shows an error line from the script: The line starts with Cause interceptor failed: The handler threw an error, or ran for more than 250 ms. interceptor selector failed: The function selector threw an error. build replacement payload for A field or value in { payload } doesn't fit the message. cannot replace the payload of { payload } was returned for a message without a readable name. An interceptor that fails on every message writes one of these lines for each message, so fix it before it fills the console. A late answer, a script too busy to answer in time, and an async interceptor aren't reported anywhere: the message continues unchanged. On a MITM bot Caution On a MITM bot, an interceptor acts on the game of the person playing. A dropped message from the server never reaches their Dofus client, a dropped message from the client never reaches the server, and a replacement reaches the other side exactly as you built it. What their client shows and what the server knows can then differ. While an interceptor is registered, every message of the session waits for your script, up to 50 ms each. A script that's slow to answer is felt as lag in the game. If the script stops or runs into trouble, messages continue unchanged: a failing interceptor can't block a session. What interceptors are good for Keeping noise out of your own handlers on a Full socket bot, with "drop". Turning chat lines into commands on a MITM bot, as above. Changing what your Dofus client receives on a MITM bot, with { payload }. Replacing a message with a different one: return "drop" and send() the new message from the interceptor. Next, Payloads lists the rules a { payload } follows, and Sessions what else changes on a MITM bot.
  15. Smooth a posté un record dans Protocole du jeu
    A script acts in the game by sending the messages the Dofus client would send. send() does that for almost everything, request() is for the few requests the game answers with a response, and the to option turns a message around, towards the client. Send messages builds a first example. Warning Everything a script sends goes out as the bot's character. On a MITM bot (man-in-the-middle: the bot relays the game session of the Dofus client you play), that's your character, in your game, and other players see what it says. send() await send(type, payload, options); Argument What to pass type The message's readable name, such as "ChatChannelMessageRequest". payload An object with the fields to send. For a message without fields, pass {}: leaving the payload out is an error. Payloads explains how values convert. options Optional. timeout, in milliseconds, and to, "server" or "client", as in { timeout: 5000, to: "client" }. send() builds the message, sends it, and resolves once it's sent. It doesn't wait for an answer: Waiting shows how to wait for one. The payload is checked first. When it doesn't fit the message, the promise rejects and nothing goes out. The message goes to the game server as a request with uid -1. onTraffic() handlers receive it as an outbound message, and the Network tool shows it when it's selected. Interceptors are never asked about it, which is why an interceptor can call send(). Sending to the client With { to: "client" }, a message travels the other way: it's framed as an event from the server and delivered inbound. await send( "ChatChannelMessageEvent", { channel: "PARTY", senderName: "Tester", content: "!say hello" }, { to: "client" }, ); Where it goes depends on the bot: Bot Where the message goes Full socket To your script only. The bot is the client, so there's no one else to deliver it to, and nothing reaches the server. MITM To your Dofus client, which acts on it as if the server had sent it, and to your script. Either way, on() handlers and waits receive it as an inbound event with uid -1. That makes it a handy way to try a handler without waiting for the game: the example above triggers the !say handler from Waiting, whose reply then really goes to the server. On a MITM bot, your Dofus client believes whatever you send it this way, so only send it messages the server could really send. to accepts "server", the default, or "client". The Network tool's Send a game message offers the same choice as To the server and To the client. request() const response = await request(type, payload, options); request() sends a message with a uid of its own, then resolves with the traffic object of the response that carries the same uid. It takes the same arguments as send(). to is accepted but ignored: a response can only come from the server. Only a few Dofus requests are answered this way. Most are answered with an event, or not at all, and for those request() waits for its whole timeout, 30 seconds unless you pass another one, then rejects with context deadline exceeded. Use send() and wait() for them. To know whether a request gets a response, watch your own Dofus client in the Network tool on a MITM bot. A request the client expects an answer to shows a uid in Traffic, and its response comes back In with the same uid. A request without a uid gets no response. Finding messages explains the rows. The response also reaches your handlers and waits, like any other inbound message. On a MITM bot, your Dofus client never receives it, since it didn't ask for it. Timeouts Function Default What the timeout covers send() 30 seconds Sending the message. request() 30 seconds Sending the message and receiving its response. wait() None Waiting for a matching message. Pass another timeout in the options, in milliseconds: { timeout: 5000 }. It must be greater than zero. Messages that aren't sent On a MITM bot, a script can't send IdentificationRequest to the server, because your Dofus client has already identified the session. send() resolves without sending anything, and the bot's console shows Refused to forward a message that would re-authenticate a relayed session. request() rejects right away instead, with mitm relay refuses to identify: this Game session was already authenticated by the real DOFUS client. A message without a readable name can't be sent: send() and request() only take names Asterobot knows. If Request, Event, Response or Message shows up among the message names, don't send it: those wrap every other message, and sending one rejects with game envelope cannot be sent as a payload: followed by the name. A script can't send bytes of its own as a new message. Only an interceptor can put bytes on the wire, in place of a message going through: see Intercepting. Errors send() and request() never throw: every problem rejects the promise. Error Cause protocol type must be a non-empty semantic message name The name is missing, empty, or not a string. semantic protobuf message is not mapped: ChatChanelMessageRequest Asterobot has no message by that name: a typo, a name that changed, or a message without a readable name. ChatChannelMessageRequest: expected an object The payload is missing. ChatChannelMessageRequest.contnt: unknown protobuf field The message has no such field. Payloads lists every payload error. protocol options must be an object The options aren't an object. unknown protocol option "retries" The options have a key other than timeout and to. timeout/milliseconds must be a finite positive Number timeout is 0, negative, or not a number. send option "to" must be "server" or "client" to isn't a string. unknown send target "clients": expected "server" or "client" to is a string other than "server" and "client". behavior resource limit exceeded: maximum pending operations reached 32 operations are already in progress. Sends, requests and game data lookups count together. context deadline exceeded request() got no response within its timeout. A complete example Party members can ask this script for the character's pods with !pods: import { session, botWarn } from "asterobot:bot"; import { on, send } from "asterobot:protocol"; export default async function behavior(launch) { let pods = "unknown"; on("InventoryWeightEvent", (traffic) => { if (!traffic.payload) return; pods = `${traffic.payload.inventoryWeight} / ${traffic.payload.weightMax}`; }); on("ChatChannelMessageEvent", async (traffic) => { // Only the exact command: our own answer comes back to us too. if (traffic.payload?.channel !== "PARTY" || traffic.payload.content !== "!pods") return; try { await send("ChatChannelMessageRequest", { channel: "PARTY", content: `Pods: ${pods}` }); } catch (error) { // An error that escapes a handler stops the whole script. botWarn("Couldn't answer:", String(error)); } }); // A new game connection starts with this message. MITM bots never launch // with "initial": the Dofus client has already identified their session. if (launch.reason === "initial") { await send("IdentificationRequest", { ticketKey: session.gameToken, languageCode: session.language, }); } } The answer says unknown until the game sends an InventoryWeightEvent after the script started: the script only knows what it has received. Next, Intercepting covers changing messages on their way through, and Payloads every rule for the values you send.
  16. Smooth a posté un record dans Protocole du jeu
    wait() pauses your code until a message you describe arrives, and hands you that message. It's the tool for "do something, then carry on when the game answers". wait() const traffic = await wait(selector, { timeout: 5000 }); wait() returns a promise that resolves with the traffic object of the next message matching selector. The selector is the same as for on(): a message name, "*" for any message, or a function that returns true for the message you want. Listening describes them. The rest of the bot doesn't pause while you wait. Handlers keep running, and other parts of your code can wait for other messages at the same time. Only inbound messages wait() only sees inbound messages: what the game server sends, and what a script or the Network tool sends to the client. A message going to the server never resolves a wait, even with "*". Use onTraffic() to see those. No timeout unless you pass one Important wait() has no timeout of its own. Without { timeout }, a wait that never matches stays open until the script stops, and the code after it never runs. Always pass a timeout. timeout is a number of milliseconds greater than zero. When it runs out, the promise rejects with protocol wait timed out. That's what gets your code going again when a message doesn't come, whether the game never sends it or its name changed after an update. Function Default timeout wait() None: it waits until the script stops send() 30 seconds request() 30 seconds A wait can't be cancelled. It ends when a message matches, when its timeout runs out, or when the script stops, so give it the shortest timeout that makes sense. Start waiting before you send A wait only sees messages that arrive after wait() was called. It doesn't look back. That matters when you send a message and wait for the game's answer. Nothing guarantees that send() resolves before the answer arrives, so a wait started after await send() can miss an answer that came quickly. Start the wait first, and await both together: const [echo] = await Promise.all([ wait( (traffic) => traffic.type === "ChatChannelMessageEvent" && traffic.payload?.content === text, { timeout: 5000 }, ), send("ChatChannelMessageRequest", { channel: "PARTY", content: text }), ]); wait() is called first, so it's in place before the message goes out. Promise.all resolves with both results, the matching message first, and rejects as soon as either one fails. Don't store the wait's promise to await it after send(): // Don't: if send() rejects, this wait is left behind. const pending = wait("ChatChannelMessageEvent", { timeout: 5000 }); await send("ChatChannelMessageRequest", { channel: "PARTY", content: text }); await pending; If send() rejects there, nothing awaits pending any more. Its timeout rejects it later with nothing to handle the rejection, and an unhandled rejection stops the script. Promise.all handles both promises, so a rejection that comes after the other one failed does no harm. Several waits for one message When several waits match the same message, they all resolve with it, in the order they were started. Waits resolve after every on() and onTraffic() handler has run for that message. A function selector that throws stops the whole script, not only the wait, with an error that includes evaluate protocol wait selector:. A script can have 128 waits open at once. One more rejects with behavior resource limit exceeded: maximum waits reached. Patterns Carry on when nothing comes Catch the timeout and decide what happens next: try { const traffic = await wait("InventoryWeightEvent", { timeout: 10000 }); botInfo("Pods:", traffic.payload?.inventoryWeight); } catch (error) { // No update can also mean the weight didn't change. botWarn("No weight update:", String(error)); } Wait for one of several messages A function selector can accept several names, and the message you get tells you which one came: const names = new Set(["InventoryWeightEvent", "InventoryContentEvent"]); const traffic = await wait((candidate) => names.has(candidate.type), { timeout: 10000 }); botInfo("Received", traffic.type); Wait inside a handler Waiting in an async handler doesn't hold up other handlers or messages, which is how the handler gets to see the answer arrive. Put the wait in try/catch: a timeout that escapes a handler stops the script. This script repeats in the party channel what a party member asks it to say with !say, and checks that the line went through: import { session, botInfo, botWarn } from "asterobot:bot"; import { on, send, wait } from "asterobot:protocol"; export default async function behavior(launch) { on("ChatChannelMessageEvent", async (traffic) => { const content = traffic.payload?.content; if (traffic.payload?.channel !== "PARTY" || typeof content !== "string" || !content.startsWith("!say ")) return; const text = content.slice("!say ".length); // Never repeat a command: the game sends our own line back to us. if (text === "" || text.startsWith("!")) return; try { await Promise.all([ wait( (echo) => echo.type === "ChatChannelMessageEvent" && echo.payload?.content === text, { timeout: 5000 }, ), send("ChatChannelMessageRequest", { channel: "PARTY", content: text }), ]); botInfo("Said in party:", text); } catch (error) { // An error that escapes a handler stops the whole script. botWarn("Couldn't say it:", String(error)); } }); // A new game connection starts with this message. MITM bots never launch // with "initial": the Dofus client has already identified their session. if (launch.reason === "initial") { await send("IdentificationRequest", { ticketKey: session.gameToken, languageCode: session.language, }); } } Errors wait() never throws: every problem rejects its promise. Error Cause protocol wait timed out The timeout ran out before a message matched. behavior resource limit exceeded: maximum waits reached 128 waits are already open. timeout/milliseconds must be a finite positive Number timeout is 0, negative, or not a number. timeout/milliseconds is too large timeout is too large to be a duration. protocol options must be an object The second argument isn't an object. unknown protocol option "retries" The options have a key other than timeout and to. to is accepted and ignored. protocol selector cannot be empty The selector is "". protocol selector must be a semantic type, '*', or predicate The selector is neither a string nor a function. Next, Sending covers send() and request(), and Limits lists every limit.
  17. Smooth a posté un record dans Protocole du jeu
    A script spends most of its time waiting for the game to say something. on() and onTraffic() register functions that Asterobot calls for each message, until you remove them or the script stops. React to messages builds a first handler; this page covers everything else. on() const handle = on(selector, handler); on() registers handler for every inbound message that matches selector, and returns a handle to pass to off() later. It returns right away. The handler then runs each time a matching message arrives, with the message's traffic object as its argument. on() only receives inbound messages: what the game server sends, and what a script or the Network tool sends to the client. Messages going to the server never reach it, whatever the selector. onTraffic() receives those. This script warns when the character's pods are nearly full: import { session, botWarn } from "asterobot:bot"; import { on, send } from "asterobot:protocol"; export default async function behavior(launch) { on("InventoryWeightEvent", (traffic) => { const weight = traffic.payload?.inventoryWeight; const max = traffic.payload?.weightMax; // A message Asterobot couldn't decode has no payload, and an error // thrown in a handler stops the whole script. if (weight === undefined || !max) return; if (weight >= max * 0.9) { botWarn(`Pods: ${weight} / ${max}`); } }); // A new game connection starts with this message. MITM bots never launch // with "initial": the Dofus client has already identified their session. if (launch.reason === "initial") { await send("IdentificationRequest", { ticketKey: session.gameToken, languageCode: session.language, }); } } inventoryWeight and weightMax are 32-bit fields, so they read as regular numbers, not as BigInt values. Selectors Selector Matches A message name, such as "InventoryWeightEvent" Messages whose type is exactly that name. Case matters. "*" Every message, including the ones without a readable name. A function Messages for which the function returns a truthy value. It receives the traffic object. A few rules: Asterobot doesn't check the name. A misspelled name, or one your version of Asterobot doesn't have, is accepted, and the handler never runs. The editor can help: see Finding messages. A name never matches a message without a readable name, since its type is empty. Use "*" or a function to receive those. A function selector is called for every inbound message, so keep it quick. If it throws, the script stops with an error that includes evaluate protocol handler selector:. A function can match on more than a name. This handler only receives guild chat: on( (traffic) => traffic.type === "ChatChannelMessageEvent" && traffic.payload?.channel === "GUILD", (traffic) => botInfo(`[guild] ${traffic.payload?.senderName}: ${traffic.payload?.content}`), ); onTraffic() const handle = onTraffic(handler); onTraffic() has no selector: its handler receives every message, in both directions. Besides what on() receives, it gets the outbound messages, the ones going to the server: on a Full socket bot, what scripts send and what someone sends from the Network tool; on a MITM bot (man-in-the-middle: the bot relays the game session of the Dofus client you play), also everything your Dofus client sends. Test traffic.direction and traffic.type in the handler to keep what you need. On a MITM bot, this shows what your client does while you play: onTraffic((traffic) => { if (traffic.direction === "outbound" && traffic.type !== "") { botDebug("Sent:", traffic.type); } }); The order handlers run in Asterobot handles one message at a time, in the order messages go through the bot. For each message: Its handlers run one after the other, in the order they were registered. on() and onTraffic() handlers share that order. The wait() calls it matches resolve. The next message is handled. Interceptors aren't part of this order: they decide about a message before it's delivered. See Intercepting. An async handler runs until its first await, and the next handler starts then. The rest of it continues later, while other messages are handled. Handlers don't wait for each other, so an async handler can see other messages arrive before it finishes. Changes made while a message is being handled work this way: A handler that an earlier handler removed with off() isn't called for the current message. A handler registered while a message is being handled starts with the next message. off() off(handle); off() removes a handler registered with on(), onTraffic() or intercept(). It returns nothing and never throws: a handle that was already removed, or anything that isn't a handle, is ignored. A handler can remove itself. This one only reacts to the first weight update: const handle = on("InventoryWeightEvent", (traffic) => { off(handle); botInfo("First weight update:", traffic.payload?.inventoryWeight); }); To act on the next matching message once, wait() is often simpler: see Waiting. Settings handlers registered with onChange() are removed with offChange(), not off(): see Read settings. Limits A script can register 256 handlers with on(), onTraffic() and intercept() together. One more throws maximum behavior handlers reached, and removing one with off() frees its place. onChange() handlers have a separate limit of 256. Each run of a handler's code has 250 ms. Past that, Asterobot stops the script, with JavaScript execution deadline exceeded in the error. An await ends a run: the code after it gets its own 250 ms. While your code runs, arriving messages wait in a queue of 64 entries, which they share with finished sends, timers and other events. When a burst of messages fills it, the script stops with behavior event-loop queue overflow. Short handlers keep the queue moving. Limits lists every limit, and Async and timing explains how the 250 ms are counted. Errors on() and onTraffic() throw right away, as ordinary exceptions, when they can't register a handler: Error Cause protocol.on handler must be callable The handler passed to on() isn't a function. onTraffic() says protocol.onTraffic handler must be callable. protocol selector cannot be empty The selector is "". protocol selector must be a semantic type, '*', or predicate The selector is neither a string nor a function. maximum behavior handlers reached The script already has 256 handlers. Once a handler is registered, an error that escapes it stops the whole script: a thrown error, or, in an async handler, an await that fails without a catch. The behavior stopped with an error then appears on the bot's page, with an error that includes Game traffic handler failed: for a thrown error, or unhandled Promise rejection: for a failed await. So check the payload before reading it, and put the awaits of a handler in try/catch: on("ChatChannelMessageEvent", async (traffic) => { // Only the exact command: the game also sends our own reply back to us. if (traffic.payload?.content !== "!ping") return; try { await send("ChatChannelMessageRequest", { channel: "GLOBAL", content: "pong" }); } catch (error) { // An error that escapes a handler stops the whole script. botWarn("Couldn't answer:", String(error)); } }); When handlers stop Handlers live as long as the script. When it stops, because you clicked Stop, an error ended it, Run restarted it or the game connection closed, all its handlers go with it. The next start registers them again, from the top of your code. Next, Waiting covers wait(), and the asterobot:protocol reference has every signature.
  18. Smooth a posté un record dans Protocole du jeu
    Before writing a handler, you need the name of the message and the names of its fields. Three tools give you those, and each one is best at a different question: Tool Best for The Dofus protocol reference Learning what a message is for and what each field holds. The editor's completion Writing names and fields correctly while you type. The bot's Network tool Seeing which messages the game really uses for something, with real values. Readable names and wire names Every traffic object carries two names. type is the readable name, such as ChatChannelMessageEvent, and wireType is the scrambled name the message has on the wire. Scripts should rely on type only: Wire names change with game versions. A script that compares wireType breaks at the next patch. on(), wait() and intercept() compare a name with type, and send() and request() take a readable name. None of them accept a wire name. Readable names come from Asterobot, which keeps them when the wire names change. An Asterobot update can still rename a message when it finds a better name for it: After a game update explains what to watch. Only part of the protocol has readable names so far. A message without one reaches scripts with an empty type, as described in Traffic. Field names The game's definitions and the protocol reference write field names with underscores, like sender_name. Scripts read them in camelCase, like senderName: each underscore is removed and the letter after it is capitalized. When sending, a script can use either spelling, but not both for the same field in one payload. Some fields and enum values still have short scrambled names, such as fzbl. Asterobot doesn't know what they mean yet, and their names can change, so don't build a script on them. The Dofus protocol reference The Dofus protocol reference on asterobot.net describes the game's messages: what each one is for, its fields, and what they mean. Start there when you know what you want to do but not which message does it, and come back to it whenever a field's meaning isn't obvious from its name. Completion in the editor The package editor knows every message the Asterobot it's connected to has a readable name for. Hover IntelliSense in the editor's top bar to see how many. With those names, the editor: completes the fields of the payload you pass to send() and request(), and marks a field the message doesn't have, or a value of the wrong type, as an error; knows the fields of traffic.payload in on(), wait() and intercept() handlers that name a message, so reading them completes too; refuses two members of the same oneof in one payload. Payloads explains oneofs. The editor doesn't check the message name itself. A misspelled name in send() only fails when the script runs, with semantic protobuf message is not mapped: followed by the name. In on() or wait(), a misspelled name is accepted and never matches anything, but the editor then knows no field of traffic.payload, and reading one is marked as an error. Errors add up on the editor's error badge. They don't stop you from saving or running the package. When the typings can't be loaded, hovering IntelliSense says Couldn't load the typings - editing works, completion doesn't. The typings describe the Asterobot the editor is connected to. After updating Asterobot, open the editor again to get the names of the new version. The Network tool On a bot's page, Tools > Network shows the messages going through the bot. Network traffic describes the whole tool. Here's what matters when you're looking for a message. Choose what to watch Settings lists every message this Asterobot has a readable name for, which are the names a script can use in on() or send(). Traffic shows nothing until you tick some of them and click Apply. The filter box narrows the list, and the select button next to it ticks every message the filter shows. Also mirror unrecovered messages adds the messages that have no readable name, the ones scripts receive with an empty type. What you select only changes what Asteroboard shows. A script receives every message, selected or not. Caution Traffic shows every field of the messages you select, secrets included. With IdentificationRequest selected, it shows ticketKey, the ticket to the bot's game session: the one your script sends on a Full socket bot, or the one your Dofus client sent on a MITM bot. Don't share screenshots of it. Read a message In Traffic, each row shows the message's name, In or Out, its sequence number after #, and its uid when it has one. Click a row to see its fields. They have the names a script reads, in camelCase. The one difference is 64-bit numbers: they show in quotes here, while a script receives them as BigInt values. Some rows are marked: On the row Meaning What a script receives A name in italics The message has no readable name, so the row shows its wire name. The message with an empty type. Schema No readable name, but the game's definitions describe the message, so the tool decodes its fields under their scrambled names. unknown: true and the bytes in rawAny, without the fields. Raw Neither the name nor the fields are known, so the row shows the bytes. unknown: true and the bytes in rawAny. Decode error Decoding reported a problem. Open the row to read it. The problem in decodeError. The search box above the list filters rows. Type a word, or name a field, as in name:Ping direction:inbound. A - in front of a term excludes the rows that match it. Messages your script sends show up too, as long as they're selected: what it sends to the server as Out, and what it sends with { to: "client" } as In. Try a message by hand Send a game message sends one message from a form, the same way send() does. It's the quickest way to try a message before writing code for it. Send messages by hand describes the form, and Send messages walks through an example. Find the message for an action A MITM bot (man-in-the-middle: the bot relays the game session of the Dofus client you play) is the best place to look, because you do the action yourself and watch what goes through: On the bot's page, open Tools > Network > Settings. Type a word related to the action in the filter, such as Inventory or Chat, tick the messages that look related, and click Apply. Open Traffic, then do the action in Dofus. Read the new rows from the top. Out is what your client sent, and In is what the server sent. Click a row to see its fields. Look the names up in the Dofus protocol reference to learn what the fields mean. Write your handler with the names you found, as in React to messages. If Settings then shows a badge such as "12 message(s) dropped - the stream couldn't keep up", select fewer messages and do the action again. Those drops only affect what Asteroboard shows, never what scripts receive. Next, Listening covers every way to receive the messages you found, and Payloads how their fields become JavaScript values.
  19. Smooth a posté un record dans Protocole du jeu
    Every message a script receives comes as a traffic object: in on() and onTraffic() handlers, from wait() and request(), and in interceptors. What a traffic object looks like A chat line on the general channel reaches a handler like this: { sequence: 1842n, direction: "inbound", kind: "event", uid: -1, type: "ChatChannelMessageEvent", wireType: "abc", // an example: the real name is scrambled and changes between game versions typeUrl: "type.ankama.com/abc", unknown: false, payload: { channel: "GLOBAL", senderName: "Airelle", senderCharacterId: 123456789012n, content: "Anyone up for a dungeon?", // ...and the other fields of the message }, rawAny: new Uint8Array([/* the bytes of the fields */]), } Properties Property Value What it tells you sequence BigInt The message's place in the bot's traffic. See Sequence numbers. direction "inbound" or "outbound" "inbound" for a message travelling towards the client, "outbound" for one travelling towards the game server. kind "event", "response", "request" or "unknown" Which kind of message it is. See Direction and kind. uid number The number that pairs a response with its request. -1 for events and for requests that don't expect a response. type string The readable name, such as ChatChannelMessageEvent. An empty string when Asterobot has no name for the message. wireType string The scrambled name the message carries on the wire. It changes from one game version to the next. typeUrl string The full type identifier on the wire: type.ankama.com/ followed by the scrambled name. payload object, or absent The message's fields, when Asterobot could decode them. Payloads explains how each kind of field reads. unknown boolean true when there's no payload. decodeError string, or absent What went wrong while decoding the message. It can be present next to a payload. rawAny Uint8Array The bytes of the message's fields, exactly as they arrived. Always present. It's empty when there were no bytes, as for a message without fields. The editor marks every property as read-only. Changing one in a handler only changes what the next handlers see: One object for every handler explains why. Direction and kind Message direction kind uid Something the server announces "inbound" "event" -1 The server's answer to a request that expects one "inbound" "response" The request's uid A request sent with send(), from the Network tool, or by the Dofus client of a MITM bot "outbound" "request" -1 A request sent with request(), or one the Dofus client of a MITM bot expects an answer to "outbound" "request" 0 or more A message sent to the client, by a script with { to: "client" } or with To the client in the Network tool "inbound" "event" -1 "unknown" exists in the typings, but Asterobot never delivers a message it couldn't classify, so scripts don't receive it. When a message is so damaged that Asterobot can't even tell its kind, a Full socket bot's game connection closes, and the script stops with an error that starts with Game session closed:. A MITM bot passes such a message on without delivering it to scripts. On a Full socket bot, the game server isn't expected to send requests. If one arrives, it's still decoded, and its decodeError is unexpected inbound game envelope. Messages Asterobot can't decode When Asterobot can't decode a message, its traffic object has no payload, unknown is true, type is empty, and decodeError says why: decodeError starts with Why obfuscated protobuf wire name is not mapped: Asterobot has no readable name for this message. This is the common case: only part of the protocol has readable names so far. unmarshal game Any The name is known, but the bytes don't match the message's definition. invalid Ankama protobuf type URL: The message's type identifier isn't valid. wireType is empty too. game envelope has no Any payload The message carried nothing at all. wireType and typeUrl are empty too. With an empty type, a handler registered with a message name never receives these messages. To catch them, use the "*" selector, a function selector, or onTraffic(), and test traffic.unknown. Listening describes selectors. What stays usable is wireType and rawAny. wireType tells two unknown messages apart, but only within one game version, and a message without a readable name can't be sent. The bot's Network tool sometimes shows fields for such a message, with the Schema badge, because it also decodes messages under their scrambled names. Scripts don't get that: for a script the message stays unknown, with only its bytes. Finding messages explains the badges. Raw bytes rawAny holds the bytes of the message's fields before decoding, whether decoding worked or not. It's a copy, so changing it changes nothing. It doesn't include the part around the fields that states the message's kind and name, so it isn't something you could send back as it is. Asterobot has no function that decodes these bytes for you. For an unknown message, the bytes are all there is. This script writes the start of each unknown message to the bot's console: import { session, botDebug } from "asterobot:bot"; import { onTraffic, send } from "asterobot:protocol"; export default async function behavior(launch) { onTraffic((traffic) => { if (!traffic.unknown) return; // Sixteen bytes keep each console line readable. const start = Array.from(traffic.rawAny.slice(0, 16), (byte) => byte.toString(16).padStart(2, "0")); botDebug(`${traffic.direction} ${traffic.wireType}, ${traffic.rawAny.length} bytes: ${start.join(" ")}`); }); // A new game connection starts with this message. MITM bots never launch // with "initial": the Dofus client has already identified their session. if (launch.reason === "initial") { await send("IdentificationRequest", { ticketKey: session.gameToken, languageCode: session.language, }); } } Sequence numbers sequence counts the messages of one game connection, both directions together, starting at 1n. A new game connection starts again from 1n. The numbers a script sees aren't always consecutive or in order: A message an interceptor drops keeps its number and never reaches handlers, which leaves a gap. On a MITM bot, Asterobot skips messages for scripts when it can't keep up with the traffic, which also leaves gaps. Sessions explains when that happens. A message a script sends from inside an interceptor goes out before the message being decided, and reaches handlers first, while carrying a higher number. Handlers receive messages in the order they went through the bot. Use sequence as a label, not to sort messages. On a MITM bot, the traffic object request() resolves with has sequence set to 0n. Handlers receive the same response with its real number. One object for every handler When several handlers and waits receive the same message, they all get the same object. A handler that changes traffic.payload changes what the handlers after it see, and nothing on the wire. To change a message on its way through the bot, use an interceptor, which receives its own copy: see Intercepting. Next, Finding messages shows how to find the name and fields of the message you need. The asterobot:protocol reference lists every function that receives traffic.
  20. Scripts run modern JavaScript, with three constructs left out and a few built-in objects missing. JSDoc comments make what the editor tells you more precise. What works The syntax of JavaScript up to ES2022 works, apart from the three constructs in the next section. That includes: classes, with private fields such as #count and static blocks async functions and await, with Promise.all(), Promise.allSettled() and Promise.any() generators (function*) and for...of optional chaining ?., ?? and ??=, spread and rest, destructuring, template literals BigInt, and typed arrays such as Uint8Array top-level await and import.meta numeric separators, as in 60_000 recent methods such as Array.prototype.at(), flatMap(), Object.hasOwn(), String.prototype.replaceAll() and matchAll() Every file is an ES module, so it runs in strict mode: assigning to a variable you never declared throws, for example. TypeScript isn't supported. A package's files must end in .js or .mjs. Three constructs that don't work Construct What happens Async generators, async function* The file can't be loaded, so the script doesn't start. for await (... of ...) The file can't be loaded, so the script doesn't start. import() called as a function The file loads, but the call rejects when it runs, with dynamic import() is not supported by this Asterobot runtime. Each has a plain replacement. Instead of an async generator, write an async function that returns an array. Instead of for await, put the await in the body of a regular loop: for (const pending of promises) { const value = await pending; } Instead of import(), import the module at the top of the file with a regular import statement. The editor's warnings The editor puts a warning on each of the three, in every file of the package, whether the file is open or not: On Warning async function* Says that async generators aren't supported for await ( Says that for await...of isn't supported import( Dynamic import() is refused at runtime - a behavior's imports must be static. Warnings don't count in the error badge, and they don't stop you from saving or running, but the script won't work while one of these constructs is there. The editor finds them by searching the text, so the same words in a comment or a string get a warning too. The editor also checks your code the way TypeScript checks JavaScript. It knows the ES2022 built-in objects, the asterobot: modules and the game's messages, and no browser or Node.js global, so document, setTimeout() or require() are marked as errors. The check isn't strict: a value that could be undefined isn't flagged, for example. Built-in objects that don't exist The editor completes three built-in objects that scripts don't have: Intl, WeakRef and FinalizationRegistry. Using one throws a ReferenceError when the code runs. Without Intl, format numbers and dates yourself. Modules and imports lists the browser and Node.js globals that are missing too. JSDoc types Scripts are plain JavaScript, but the editor reads JSDoc comments and uses their types for completion and checks. These types can be used in any file, without an import: Type What it describes BehaviorLaunch The launch object the entry function receives ProtocolTraffic A traffic object. ProtocolTraffic<GameMessageResults["ChatChannelMessageEvent"]> also describes its payload. GameMessageResults The payload of each message as a script receives it, by message name GameMessagePayloads The payload of each message as send() accepts it, by message name BehaviorParameters An export const parameters object BehaviorActions An export const actions object The message types are part of the typings behind the editor's IntelliSense badge. When the badge says the typings couldn't be loaded, the editor doesn't know any message name. A handler you pass to on() with a message name already gets a typed payload, without any comment. JSDoc helps most in your own functions: import { session, botInfo } from "asterobot:bot"; import { on, send } from "asterobot:protocol"; /** * @param {ProtocolTraffic<GameMessageResults["ChatChannelMessageEvent"]>} traffic * @returns {string} */ function formatChat(traffic) { const payload = traffic.payload; if (!payload) return "(a chat line Asterobot couldn't decode)"; return `[${payload.channel}] ${payload.senderName}: ${payload.content}`; } /** @param {BehaviorLaunch} launch */ export default async function behavior(launch) { on("ChatChannelMessageEvent", (traffic) => { botInfo(formatChat(traffic)); }); // A new game connection starts with this message. MITM bots never launch // with "initial": the Dofus client has already identified their session. if (launch.reason === "initial") { await send("IdentificationRequest", { ticketKey: session.gameToken, languageCode: session.language, }); } } In the same way, put /** @type {BehaviorParameters} */ on the line above export const parameters to have each setting's fields checked. Declare settings describes them. The next chapter, Game protocol, goes into messages in detail.
  21. When a script doesn't do what you expect, its own output is usually the quickest way to find out why, once you know which of the two consoles it went to. Two places to write Functions Module Where the lines appear botDebug(), botInfo(), botWarn(), botError() asterobot:bot The bot's Console tab, as Script lines, at every level debug(), log(), info(), warn(), error(), also available on the console object asterobot:console Server > Console, and the terminal Asterobot runs in The bot's console This is the one you want while you write a script. Your lines show at their level, among Asterobot's own lines about the bot and, when Live chat is on, the game chat. In Sources, keep only Script to see your output alone. Asterobot keeps the last 200 lines of each bot's console, and that's what you see when you open the tab. A script that logs on every message pushes the other lines out quickly, so remove chatty lines once you're done. Console describes the tab. Asterobot's log asterobot:console writes to the log of Asterobot itself, where log() is the same as info(). The lines don't say which bot wrote them, so when several bots play the same package, you can't tell their lines apart. Two settings decide which levels reach each place: Where Setting Default Server > Console Server level, on that page, which is the logger.webLevel setting INFO The terminal logger.cliLevel INFO Both leave out debug lines by default. Set Server level to DEBUG and they show from then on, without restarting Asterobot. Status and console describes that page. How values come out Each call joins its values with spaces. Asterobot turns the values into text itself, not with JavaScript's String(), so some of them look different from a browser's console: You log The line shows "Bouftou" Bouftou 42, 1.5, true 42, 1.5, true 123456789012n 123456789012 undefined or null <nil> [1, 2, 3] [1 2 3] { pods: 5, kamas: 10 } map[kamas:10 pods:5] An Error map[] So log an error as String(error) or error.message, which give its text. For objects and arrays, JSON.stringify() makes a more readable line, but it throws a TypeError with the message Do not know how to serialize a BigInt as soon as it meets a BigInt, and message payloads often hold some. Convert them on the way: function toJson(value) { return JSON.stringify(value, (key, item) => (typeof item === "bigint" ? item.toString() : item)); } botInfo("Payload:", toJson(traffic.payload)) then prints the payload with its 64-bit numbers written as digits. Keep the game ticket out of your logs Caution Never log session.gameToken. It's the ticket to the bot's game session, and anyone who reads it could take that session over. The ticket also travels in the ticketKey field of IdentificationRequest. A handler registered with onTraffic() receives the messages your own script sends, so a script that logs every payload writes the ticket to the console when it identifies. Leave that message out whenever you log traffic, as the sample below does. A debugging routine Before running, look at the editor's error badge. A misspelled function or a field a message doesn't have is cheaper to fix there. In the editor, pick a MITM bot and click Run. On a MITM bot (man-in-the-middle: the bot relays the game session of the Dofus client you play), you can make things happen in the game yourself and watch the script react. If the toast says Couldn't run the package and no alert appears on the bot's page, the code didn't load. Open the bot's Settings tab: Package settings shows the reason. Open the bot's Console tab and keep only Script in Sources. Log a line when the script starts, such as botInfo("Started:", launch.reason), to be sure it runs at all. If The behavior stopped with an error appears, click Copy and look at how the text starts. Errors says what each beginning means. When a handler never seems to run, check what really arrives: log the type of every message for a moment with the sample below, or use the Network tool as React to messages shows. When a lookup returns nothing, try it on the game version's Game data tab first. SQL queries shows how to try a query there. Fix the code and click Run again: it saves your change and starts it. This handler writes the direction and the name of every message to the bot's console: import { session, botDebug } from "asterobot:bot"; import { onTraffic, send } from "asterobot:protocol"; export default async function behavior(launch) { onTraffic((traffic) => { // Its payload holds the game ticket, which must never reach a log. if (traffic.type === "IdentificationRequest") return; botDebug(traffic.direction, traffic.type || traffic.wireType); }); // A new game connection starts with this message. MITM bots never launch // with "initial": the Dofus client has already identified their session. if (launch.reason === "initial") { await send("IdentificationRequest", { ticketKey: session.gameToken, languageCode: session.language, }); } } traffic.type is empty for a message Asterobot has no readable name for, so the line shows its name on the wire instead. Traffic describes every property. A busy session fills the console within seconds, so run this only while you look. Next, JavaScript support.
  22. Smooth a posté un record dans Fonctionnement des scripts
    Most mistakes in a script stop it until its next start. Knowing which ones do, where they show up and how their texts are built saves a lot of guessing. Where errors show up Where What it tells you The behavior stopped with an error, at the top of the bot's page The script started, then stopped because of an error. The alert shows the whole text, with Copy and Dismiss. The bot's Behavior badge, and its column in Bot Manager, say Failed. Couldn't start the behavior after Play, or Couldn't run the package after Run The start failed. When the code couldn't even be loaded, this toast is the only sign, and it gives no reason. Couldn't load the selection, after confirming Load a behavior selection The package couldn't be loaded on the bot, for example because a dependency isn't installed or the package isn't compatible with your Asterobot. The editor's error badge A problem found before running, such as a syntax error, an import of a file that doesn't exist, or a misspelled function. When the code couldn't be loaded, the bot's Settings tab gives the reason under Package settings, as The entry function explains. The alert goes away when the script starts again, when you load or unload a package, or when the bot disconnects. Dismiss only hides the text you dismissed: a different error shows up again. What stops a script The text in the alert starts with where the failure happened: What happened The text starts with The top-level code of a module threw during the start evaluate behavior module "library/my-first-script/1.0.0/index.js": The parameters or actions export isn't an object of objects, or an action has no run function read declarations of "library/my-first-script/1.0.0/index.js": The entry function threw or rejected, or anything else failed before it returned initialize behavior module "library/my-first-script/1.0.0/index.js": A handler registered with on() or onTraffic() threw Game traffic handler failed: A function you passed to on() or wait() to pick messages threw evaluate protocol handler selector: or evaluate protocol wait selector: An onChange() handler threw parameters.onChange handler followed by a number A promise rejected and nothing caught it, which includes an error thrown in an async handler or an async interceptor unhandled Promise rejection: The code that continued after an await went over the time limit drain JavaScript Promise jobs: A few failures have their own text, whichever of the beginnings above comes first: What happened The text contains A piece of code ran for more than 250 ms without awaiting JavaScript execution deadline exceeded The bot's queue filled up behavior event-loop queue overflow The game connection closed on its own Game session closed The script called disconnect() behavior requested Game disconnect The default export has its own three errors, listed in The entry function. Reading an error text After the beginning comes the error itself. An error your code threw shows as its type and message, such as Error: no zaap on this map or TypeError: gamedata.record id must be an integer. When it was thrown by a handler or a selector function that isn't async, at follows, with the file, the line and the column where it happened. An error from an Asterobot function shows Asterobot's text. Promise rejected: in front of it means an await rejected. For example, sending token instead of ticketKey while the script starts gives: initialize behavior module "library/my-first-script/1.0.0/index.js": Promise rejected: IdentificationRequest.token: unknown protobuf field Error messages lists the texts of Asterobot's functions, with their causes. What doesn't stop a script What happens What you see instead An action button's run throws or rejects A toast named after the action, such as recall failed, with the error text. The script keeps running. An interceptor that isn't async throws, or returns a payload that can't be built That interceptor makes no decision: the next one is asked, and when none decides, the message goes through unchanged. The bot's console shows the error. An answer that comes after 50 ms is ignored without any report. An async interceptor never decides, and an error in it stops the script. See Intercepting. A promise rejects and your code catches it Whatever your catch does record() finds no record, or text() finds no text They return undefined. off() or offChange() receives a handler that's already removed Nothing The logging functions, botInfo() and the others, never throw. Functions that throw right away A few functions throw as soon as they're called, instead of returning a promise that rejects. A try/catch catches them without any await: Function Throws when on(), onTraffic(), intercept(), onChange() The handler isn't a function, the selector is empty or neither a name nor a function, or 256 handlers are already registered: maximum behavior handlers reached. on(), onTraffic() and intercept() count together, and onChange() has its own 256. text() The key is neither a string nor a whole number record() The table name is empty, the id isn't a whole number, or the table doesn't exist packages.info() The name is empty, or matches no package the bot runs Keep a script running A handful of habits prevent most failures, and every sample in these docs follows them: Put each await of a handler inside try/catch. An error that escapes a handler, async or not, stops the whole script. Check that traffic.payload exists before reading its fields: a message Asterobot couldn't decode has no payload. traffic.payload?.content checks and reads at once. Give every wait() a timeout. Without one, a wait that never matches stays open until the script stops. Open the wait before you send what it answers, as Waiting shows. When you start an async function without awaiting it, catch its errors, inside it or where you start it: refreshLoop().catch((error) => botWarn("Loop stopped:", String(error)));. Check what lookups return. undefined means nothing was found, or that the bot's game version has no game data. Convert BigInt ids with Number() before you pass them to record() or text(). Log errors as String(error). Logging and debugging explains why. On a Full socket bot, make sure nothing after the identification can fail while the script starts. Otherwise the next start is "initial" again, and the script identifies a second time. This handler reads chat lines starting with !json and parses what follows. Without the try, anyone typing !json { in a channel your character reads would stop the script: import { session, botInfo, botWarn } from "asterobot:bot"; import { on, send } from "asterobot:protocol"; export default async function behavior(launch) { on("ChatChannelMessageEvent", (traffic) => { const content = traffic.payload?.content; if (!content?.startsWith("!json ")) return; try { const data = JSON.parse(content.slice(6)); botInfo("Parsed:", JSON.stringify(data)); } catch (error) { // An error that escapes a handler stops the whole script. botWarn("Not valid JSON:", String(error)); } }); // A new game connection starts with this message. MITM bots never launch // with "initial": the Dofus client has already identified their session. if (launch.reason === "initial") { await send("IdentificationRequest", { ticketKey: session.gameToken, languageCode: session.language, }); } } Limits lists every limit and what happens past it. Next, Logging and debugging.
  23. A bot runs its script one piece of code at a time. Once you know how those pieces are scheduled, the 250 ms limit, long loops and the order of handlers stop being surprises. One piece of code at a time Each bot runs its own copy of the script, and bots run side by side without sharing anything. Inside one bot, a single piece of your code runs at any moment: the entry function, a handler, or the rest of an async function after an await. The next piece only starts once the current one has finished or reached an await. Everything ready to run waits in the bot's queue, and is handled in the order it arrived: a message, for your handlers and your wait() calls the result of a send(), a request() or a game data call a sleep() that's over, or a wait() whose timeout ran out settings applied from Package settings, or an action button someone pressed a question for your interceptors about a message on its way await keeps the bot responsive When your code reaches await on something that hasn't settled yet, that piece of code ends there and the bot goes back to its queue: other messages are handled and other handlers run. Once the awaited thing settles, the rest of your function continues as a new piece. That's what lets a handler await wait() for an answer while the bot keeps receiving messages. A message only resolves the wait() calls already open when it arrives, so open the wait before you send what it answers: Waiting shows how. Code that runs without awaiting does the opposite. As long as it runs, nothing else happens for that bot: no handler runs, messages pile up in the queue, and interceptors can't answer, so after 50 ms the message they were asked about goes through unchanged (see Intercepting). The 250 ms limit A piece of code may run for 250 ms at most. Past that, Asterobot interrupts it, and the script stops with an error containing JavaScript execution deadline exceeded. The bot stays connected. The limit applies to each of these on its own: the top-level code of your modules, at each start the entry function, up to its first await each call of a handler registered with on(), onTraffic() or onChange() the code that continues after an await. When several awaited things settle together, the code that continues from all of them shares one 250 ms. An interceptor or an action button's run that goes over the limit doesn't stop the script: the interceptor's message goes through unchanged, and the action is reported as failed. Only time spent running counts. Waiting on an await doesn't: a sleep(10_000) or a five-second wait() is fine. What goes over is work done in one go, such as a long loop, a big calculation, or thousands of synchronous calls like record() and text() in a row. To get through a long job, let the bot breathe now and then with await sleep(0). Each part then runs as its own piece, with its own 250 ms: for (let i = 0; i < ids.length; i++) { handle(ids[i]); // Every 500 items, let the bot handle its queue and start a new 250 ms. if (i % 500 === 499) await sleep(0); } For game data, a single find() or query() often replaces thousands of record() calls, and it does its work outside your code's 250 ms. See Game data overview. sleep() import { sleep } from "asterobot:timers"; await sleep(1500); sleep() is the only timer scripts have. It takes a number of milliseconds, and 0 is allowed. Its promise resolves once the time has passed, and your code continues when the bot reaches it in its queue, so a busy bot resumes a little late. When sleep() rejects with The duration is negative, infinite or not a number timeout/milliseconds must be a finite positive Number 128 sleeps are already in progress behavior resource limit exceeded: maximum timers reached The script stops The reason it stopped, such as behavior runtime closed after Stop There's no setTimeout(). To do something later without holding up the function you're in, call an async function without awaiting it, and let that function await sleep() first. Catch the errors inside it: a rejected promise that nothing catches stops the script. Loops Start long loops without awaiting them A start only counts once the entry function has returned, as The entry function explains. A while (true) loop awaited inside the entry function keeps the Play button loading for good, and keeps the next start "initial". Start the loop from the entry function without awaiting it: import { session, botInfo, botWarn } from "asterobot:bot"; import { send } from "asterobot:protocol"; import { sleep } from "asterobot:timers"; async function doOneRound() { botInfo("One more minute"); } async function everyMinute() { while (true) { try { await doOneRound(); } catch (error) { botWarn("This round failed:", String(error)); } // Outside the try: once the script stops, sleep() rejects, and that // rejection is what ends the loop. await sleep(60_000); } } export default async function behavior(launch) { // A new game connection starts with this message. MITM bots never launch // with "initial": the Dofus client has already identified their session. if (launch.reason === "initial") { await send("IdentificationRequest", { ticketKey: session.gameToken, languageCode: session.language, }); } // Not awaited: the start only counts once this function has returned. everyMinute(); } Keep the await sleep() outside the try. When the script stops, every pending sleep() rejects, and so does every later call. A catch around the sleep() would catch that rejection and send the loop round again instead of ending it. To let people pause such a loop, read a setting at each round, as in if (values.enabled) await doOneRound();. Don't skip the sleep() when the setting is off: a loop that never awaits runs into the 250 ms limit. Read settings covers values. React to messages instead of polling Most scripts don't need a loop at all. A handler registered with on() runs each time a message arrives, and wait() pauses a function until one does. Listening and Waiting cover both. The order things run in When a message arrives: Your interceptors are asked first, in the order they were registered, and the first decisive answer wins. A message they drop never reaches the next steps. See Intercepting. Each handler matching the message is called, in the order it was registered. Handlers registered with on() only receive inbound messages, and those registered with onTraffic() receive both directions. The wait() calls matching the message resolve, the oldest first. The code that continues because of this message, such as a function awaiting one of those wait() calls, runs after every handler has been called. Handlers don't wait for each other. An async handler runs until its first await, then the next handler is called, and the rest of the first one runs later, possibly after other messages. A handler that throws stops the script: see Errors. Messages are handled in the order they arrive. The onChange() handlers of settings are the exception to any ordering: they have no fixed order. When the bot falls behind The queue holds 64 items. If your code keeps the bot busy while messages keep arriving, which happens quickly on a busy MITM session (man-in-the-middle: the bot relays the game session of a Dofus client someone plays), the queue fills up and the script stops with behavior event-loop queue overflow. Keep handlers short, and don't do heavy work on every message. Interceptors never fill the queue: when it's full, the message they would have been asked about goes through unchanged. A few calls have caps of their own. At most 32 send(), request() and game data calls can be in progress at once, all of them counted together, and the next one rejects with behavior resource limit exceeded: maximum pending operations reached. At most 128 wait() calls can be open, and the next one rejects with behavior resource limit exceeded: maximum waits reached. Limits lists every limit. Next, Errors.
  24. Every file of a package is an ES module. Scripts reach Asterobot's functions, their own files and other packages through import statements, and through nothing else. The built-in modules Module Exports Reference asterobot:protocol send, request, wait, on, off, onTraffic, intercept asterobot:protocol asterobot:bot session, currentMap, disconnect, botDebug, botInfo, botWarn, botError asterobot:bot asterobot:timers sleep asterobot:timers asterobot:gamedata text, record, table, find, search, query asterobot:gamedata asterobot:console console, debug, log, info, warn, error asterobot:console asterobot:runtime packages, whose packages.info() describes a package asterobot:runtime asterobot:parameters values, onChange, offChange asterobot:parameters asterobot:pathfinding findPath, cellToPoint, pointToCell, distance, direction asterobot:pathfinding asterobot:movement move asterobot:movement These modules only have named exports. Import each function by its name: import { on, send } from "asterobot:protocol"; import { record, text } from "asterobot:gamedata"; There's no default export and no object grouping a module's functions: import gamedata from "asterobot:gamedata" and import { gamedata } from "asterobot:gamedata" both fail. If you prefer one name for a module, use a namespace import: import * as protocol from "asterobot:protocol", then protocol.send(...). asterobot:console is the one module that also exports an object, console, so that console.log() reads the way it does elsewhere. asterobot:parameters is scoped to the package that imports it. In your package, values holds your package's settings. Imported from a dependency, it holds the dependency's own. Settings in dependencies explains how those show on the bot's page. A built-in name that doesn't exist, such as asterobot:fs, keeps the script from loading. The reason contains built-in module "asterobot:fs" is not registered. Note Coming soon. Asterobot has room for extension modules, named asterobot:extension/ followed by a name, which would add functions of their own. None is available yet, so importing one fails. See Extension modules. Importing your own files // index.js import { formatChat } from "./lib/format.js"; // lib/format.js export function formatChat(channel, sender, content) { return `[${channel}] ${sender}: ${content}`; } A relative import follows these rules: It starts with ./ or ../, and is resolved from the folder of the importing file. It names the file completely, extension included: ./lib/format.js, not ./lib/format. It stays inside the package. ../ can't lead to another package's files. It never starts with / and never contains a backslash. A path without ./, such as lib/format.js, isn't a file path: Asterobot reads it as the name of a package your package depends on, and fails with has no locked dependency for "lib/format.js". When an import breaks a rule, the script doesn't load, and the toast gives no reason. The reason contains one of these texts: Problem Error The file doesn't exist, or the extension is missing module "library/my-first-script/1.0.0/lib/format" imported by "library/my-first-script/1.0.0/index.js" is not installed The path leaves the package relative import "../other.js" escapes package root "library/my-first-script/1.0.0" The path starts with / absolute module specifier "/lib/format.js" is forbidden The editor underlines an import of a file that doesn't exist as an error, and the bot's Settings > Package settings shows the full reason, as The entry function explains. Each module runs once per start, however many files import it, so a variable at the top level of lib/format.js is shared by every file that imports it. Each bot has its own copy: two bots playing the same package never share a variable. Importing another package To import another package from your library, first declare it in the dependencies of your asterobot.json, as Dependencies describes. Then import it by the name you declared: import { formatChat } from "chat-tools"; import { findSpots } from "@alice:mining"; You receive what the dependency's index.js exports, and only that. A path into the dependency, such as "chat-tools/lib/format.js", isn't supported and fails with has no locked dependency for "chat-tools/lib/format.js". A package that wants to share something exports it from its index.js. A name your package doesn't declare fails the same way. That's also what happens with "lodash", "fs" or any other npm or Node.js module name. Versioned imports An import can carry a version after an @: Import What it picks "chat-tools@1.2.0" Version 1.2.0 of chat-tools. When your manifest declares two versions of the same package side by side, this is how you pick one. "chat-tools@^1.0.0" The highest version of chat-tools that matches the range "chat-tools@latest" The highest version of chat-tools A range or latest only chooses among the versions of that package that are already part of what the bot runs, brought in by your manifest or by a dependency's. It never installs anything. When none matches, the script doesn't load, and the reason contains no installed version of "chat-tools" satisfies "^1.0.0". What a script can't import Not possible What happens import() called as a function The call rejects when it runs, with dynamic import() is not supported by this Asterobot runtime. The editor warns about it. require() It doesn't exist, and calling it throws a ReferenceError. npm packages and Node.js modules There's no npm. A name like "lodash" is read as an undeclared dependency. JSON, text or image files A package only holds .js and .mjs modules, so there's no such file to import. No browser or Node.js globals Scripts have the standard JavaScript built-ins, such as Math, JSON, Date, Map, Promise and BigInt, and the functions of the asterobot: modules. The globals of a browser or of Node.js don't exist: Global you might expect In a script console Import botInfo() and the rest from asterobot:bot, or console from asterobot:console. setTimeout(), setInterval(), setImmediate() await sleep(milliseconds) from asterobot:timers fetch(), XMLHttpRequest, WebSocket Not available window, document, localStorage Not available process, Buffer, require, __dirname Not available structuredClone(), TextEncoder, TextDecoder, queueMicrotask() Not available The editor knows this: it marks these names as errors. JavaScript support lists the language features scripts have and the few they don't. Next, Async and timing.
  25. A script runs from the moment a bot starts it until something stops it, and it keeps little from one run to the next. States and controls describes the same buttons from the player's side. Starting a script Where Control What it does Package editor Run Saves the package, stops the script the chosen bot is running, sets this package as the bot's package, then starts it. Bot's page, or its row in Bot Manager Play Starts the bot's package, or the inline script loaded last. It's disabled until the bot is connected to a game. When the bot has nothing to play, it opens Load a behavior selection instead. Menu next to Play on the bot's page Load a package and play it Opens Load a behavior selection, then starts what you picked. Each start builds a new copy of the script from the files saved in the library at that moment, then calls its entry function. Saving in the editor doesn't change a script that's already running: click Run, or Stop then Play, to start the code you saved. After a script has failed, Play starts a new copy right away. There's nothing to stop first. Stopping a script What stops it The game connection Error shown Stop Stays open No Load a new package, once you confirm the dialog Stays open No Unload package Stays open No Disconnect Closed No Delete bot Closed No An error in the script Stays open Yes, that error The script calls disconnect() Closed Yes, starting with behavior requested Game disconnect The connection closes on its own, for example when the game server ends it, or when the Dofus client of a MITM bot (man-in-the-middle: the bot relays the game session of a Dofus client someone plays) closes Gone Yes, starting with Game session closed An error shown means the alert The behavior stopped with an error at the top of the bot's page, and Failed in the Behavior column of Bot Manager. Errors explains each text. Once a script is stopped, its handlers receive nothing more and its interceptors aren't asked anymore, so messages go through unchanged. The calls it was still waiting on reject: a sleep(), wait(), send(), request() or game data call in progress. After Stop, their error is behavior runtime closed. If a catch in your script logs errors, that line can appear in the bot's console right after you stop it. What starts over and what's kept Each start is a fresh copy. Every variable in your modules starts again from its initial value, and top-level code runs again. The handlers, waits and sleeps of the previous run are gone, and the messages that arrived while no script was running are never delivered to the new one. What survives lives outside the script: the game connection and the character's situation in the game, the package and version the bot is set to play, and the values on the bot's Package settings. launch.generation also keeps counting for as long as the connection stays open. A script has nowhere to keep data for its next run. It can't write files, and values is read-only. Anything it needs again has to come from the game, or from a setting someone fills in. Changing the code or the version Save only changes what the next start loads, as described above. When you switch the bot to another installed version of its package from the bot's page, the running script also keeps going. The toast says Now set to version followed by the version, and adds The running behavior keeps going until you play or reload. when a script is running. The next start uses the new version. Settings you apply from Package settings are the exception: they reach the running script without a restart. See Read settings. The "reload" launch reason A start can replace a running script without stopping it first. The new copy then takes over the game connection directly, and the messages that arrive during the switch wait for it instead of being lost. That start's launch.reason is "reload". No button in Asteroboard starts a script this way, and there's no Reload button. Run, Load a new package and Load a package and play it all stop the running script first, so the script that follows starts with "resume", or with "initial" on a Full socket connection where no script has started successfully yet. Handle "reload" like "resume" in your code. When the connection drops Nothing restarts by itself. When a Full socket bot connects again, or when a Dofus client connects through a MITM bot again, the bot shows Connected to game server with no script running. Click Play. That start is the first one on a new connection: launch.generation is 1n, and launch.reason is "initial" on a Full socket bot, "resume" on a MITM bot. After Asterobot restarts When Asterobot starts again, every bot comes back disconnected and no script runs. Asterobot doesn't connect a bot or start a script on its own. Each bot keeps the package and version it's set to play, whether that came from Run, from Load a new package with a library package, from the Add a bot dialog or from the bot's Settings. Its package settings are kept too. An inline script isn't: load it again after the restart. Closing Asteroboard stops nothing, because scripts run inside Asterobot. Disconnect, delete and restart covers these cases from the player's side. Next, Modules and imports.

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.