Tout ce qui a été publié par Smooth
-
Lookups
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.
-
Game data overview
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.
-
After a game update
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.
-
Sessions
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.
-
Payloads
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.
-
Intercepting
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.
-
Sending
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.
-
Waiting
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.
-
Listening
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.
-
Finding messages
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.
-
Traffic
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.
-
JavaScript support
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.
-
Logging and debugging
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.
-
Errors
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.
-
Async and timing
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.
-
Modules and imports
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.
-
Play, stop and reload
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.
-
The entry function
Every script starts in one function: the default export of the package's index.js. Asterobot calls it each time a bot starts the package, and the way you write it decides whether that start goes through. The default export export default async function behavior(launch) { // ... } Asterobot checks three things before and after calling it: Rule Error when it's broken index.js has a default export behavior module "library/my-first-script/1.0.0/index.js" has no unambiguous default export The default export is a function behavior module "library/my-first-script/1.0.0/index.js" default export is not callable The function returns a promise behavior module "library/my-first-script/1.0.0/index.js" default export must be async and return a Promise Declaring the function async takes care of the last rule. Its name doesn't matter, and an arrow function works as well: export default async (launch) => { ... }. Asterobot calls the function once per start. Only the one in the package the bot plays is called: the default exports of its dependencies never are. The launch object The function receives one argument, launch: Property Type Value launch.reason string Why the script is starting: "initial", "resume" or "reload". launch.generation BigInt 1n for the first script start on the bot's current game connection, then one more for every start on that connection. The same two values can be read anywhere in your code, not only in the entry function: session.launchReason and session.behaviorGeneration, from asterobot:bot. Both start over with a new game connection: when a Full socket bot connects again, or when a Dofus client connects through a MITM bot (man-in-the-middle: the bot relays the game session of a Dofus client someone plays). launch.generation is a BigInt, so compare it with another BigInt, as in launch.generation === 1n. What to do for each launch reason launch.reason When a script starts with it "initial" On a Full socket bot's new game connection, as long as no script has started successfully on that connection yet. "resume" Every later start on the same connection, such as Play after Stop, or Run again. On a MITM bot, every start, the first one included. "reload" When a running script is replaced without being stopped first. No button in Asteroboard does that. Here is what each one asks of your script. "initial" Nothing has been sent on this game connection yet. On a Full socket bot the script identifies first, with the lines from Identify and run. Choosing a character and entering the game are up to scripts too. "resume" The connection is already identified, either by a script that started on it before or, on a MITM bot, by the Dofus client. The character may already be in the game, in a fight or talking to a merchant. Your script didn't receive the messages that arrived while no script was running, so it can't know what happened before it started: work from what arrives from now on. On a MITM bot, someone also plays that character, and session.shared is true. "reload" For the game, this is the same as "resume". The difference is that the previous script was replaced without being stopped, so the messages that arrived during the switch are delivered to the new script instead of being lost. No button in Asteroboard starts a script this way. Handle it like "resume". Most scripts only need to single out "initial": import { session, botInfo } from "asterobot:bot"; import { send } from "asterobot:protocol"; 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, }); } botInfo(`Start ${launch.generation} on this connection (${launch.reason})`); } When a start counts Asterobot waits for the promise the function returns, and the start is complete once the function has returned without an error. Until that happens: The Play button keeps loading, and Run in the editor doesn't show its Running on toast. The script already runs. The handlers you registered receive messages, Stop works, and the package's action buttons can be used. If the function fails before returning, the start doesn't count. On a Full socket connection where no start has succeeded yet, the next start is "initial" again. So set things up and return. Returning doesn't stop the script: the handlers you registered keep working until something stops it. Work that goes on for the whole session belongs in handlers, or in a loop you start without awaiting it, as Async and timing shows. Top-level code The code at the top level of your modules, outside any function, runs before the entry function is called, at every start, in every module index.js reaches. A dependency's top-level code runs before the code of the module that imports it. Top-level await works. Asterobot also runs that code when it reads the package's settings, for the Package settings section of the bot's Settings tab and when a bot starts the package. That reading happens without a bot or a game session, so the built-in modules behave differently while it lasts: During the reading What happens session Holds empty values: gameToken and language are "", and launchReason is "initial". values, from asterobot:parameters Is empty. on(), onTraffic(), intercept(), onChange() Return as usual, but the handler is never called. botInfo() and the other logging functions Do nothing. packages.info() Works as usual. send(), request(), wait(), sleep(), disconnect() and every function of asterobot:gamedata Throw an error ending with not available while reading a package's declarations. A top-level await on something that can't settle during the reading fails with package awaits at import time, which cannot be answered without a Game session. When the reading fails, Package settings shows Couldn't read this package's settings with the reason, and the script starts without its settings: every read from values gives undefined. Keep the top level to imports, constants, declarations and function definitions, and put everything else in the entry function. When the start fails What went wrong What you see The code can't be loaded: a syntax error, an import that doesn't resolve, a module limit exceeded Only a toast: Couldn't start the behavior after Play, or Couldn't run the package after Run. A rule of the default export is broken The toast, and The behavior stopped with an error at the top of the bot's page, with one of the texts above. The top-level code throws The toast, and the alert with a text starting with evaluate behavior module. export const parameters or export const actions isn't an object of objects, or an action has no run function The toast, and the alert with a text starting with read declarations of. The entry function throws, or an await in it rejects without a catch The toast, and the alert with a text starting with initialize behavior module. The top-level code or the function runs for 250 ms without awaiting The toast, and the alert with a text containing JavaScript execution deadline exceeded. When the code can't be loaded, open the bot's Settings tab: reading the settings loads the same code, so Package settings shows Couldn't read this package's settings followed by the reason. That section describes the package the bot is set to play, which is the one Run gave it. It doesn't cover an inline script. Errors covers what can go wrong once the script runs, and Play, stop and reload what each start keeps from the previous one.
-
Package layout
A package is a small set of JavaScript files with a fixed starting point, index.js, and not every file in it runs when a bot starts the package. What a package contains A package with a few files looks like this: index.js asterobot.json lib/chat.js lib/format.mjs File Required What it's for index.js Yes The entry module. Every script starts here. Other .js and .mjs files No Modules that index.js imports, directly or through other modules. They can sit at the root of the package or in folders. asterobot.json No The manifest: dependencies, the Asterobot versions the package works with, and declared permissions. Asterobot reads nothing else. A JSON data file, an image or a README placed in a package is left out: it's not part of the script, and no script can read it. .js and .mjs files are handled the same way, as ES modules. A package you create in Library Manager starts with index.js alone. Create a package shows how to add files in the editor. index.js index.js is always the entry module, at the root of the package. It can't be renamed, and the editor shows no trash icon for it. Asterobot looks for three exports in it. The default export is the function called when a bot starts the package, described in The entry function. export const parameters and export const actions declare the settings and the buttons shown on the bot's Settings tab: see Declare settings and Declare actions. When your package is a dependency of another package, its index.js is also what that package receives when it imports yours. Modules and imports explains how. Only what index.js reaches runs When a bot starts the package, Asterobot begins with index.js, follows its import statements, then the imports of those modules, and so on. A file that isn't reached this way is never loaded: its code doesn't run, and a mistake in it doesn't stop the script. The module limits count the same files, dependencies included: Limit Value Modules reached from index.js 256 Total size of their source code 4 MiB Length of an import chain 64 modules Past any of them the script doesn't start. Limits lists every limit a script runs under. Module paths A module's path is its place inside the package, such as lib/chat.js. Every file you add in the editor follows these rules: Rule Accepted Refused Ends in .js or .mjs lib/chat.js lib/chat.ts, data.json Relative to the root of the package lib/chat.js /lib/chat.js Uses forward slashes lib/chat.js lib\chat.js Stays inside the package lib/chat.js ../chat.js Written without ./ and without doubled slashes lib/chat.js ./lib/chat.js, lib//chat.js The Add a file dialog checks the path while you type: Message Cause A module must be a .js or .mjs file. The path doesn't end in .js or .mjs. A path must stay inside the package. The path starts with /, or contains .. or a backslash. This package already has a file at that path. Another file of the package already has this path. The dialog doesn't check the last rule of the first table. A file added as ./lib/chat.js or lib//chat.js appears in the Files list, but Save then fails with Couldn't save. Remove that file and add it again as lib/chat.js. Zip files and the command line describes how the files of an imported .zip are laid out. How error messages name your files Error messages refer to a module by where its package sits in your library: library/, the package name, the version, then the path. The file lib/chat.js of my-first-script 1.0.0 appears as library/my-first-script/1.0.0/lib/chat.js. A package installed from the marketplace has its name split at the colon: index.js of @alice:mining 1.4.2 is library/@alice/mining/1.4.2/index.js. An inline script, loaded with Inline script, isn't in the library, so its name starts with local/ instead. asterobot.json asterobot.json is optional. A new package doesn't have one, and Asterobot only writes the file once there's something to put in it. You don't edit it in the code editor: open the package's own page and use its Manifest card. Field Written by What it holds dependencies You The other library packages this package imports, and their versions compatibleVersion You The Asterobot versions the package works with, such as >=1.5.0 <2.0.0 permissions You What the package says it needs. Asterobot doesn't enforce them yet: see Permissions. sourceId Asterobot The asterobot.net file the package was installed from integrity Asterobot A checksum of the package's files, for a package installed from the marketplace. Asterobot checks it each time it loads the package. The manifest describes every field, and Dependencies shows how to declare one. Next, The entry function.
-
Share your package
Your package only exists in your own library until you share it. You can send someone a .zip file, or publish the package on the marketplace so anyone with Asterobot can install it. Export a .zip Open Library Manager. Open the menu at the end of your package's row, or right-click the row. Choose Export. Your browser downloads a file named after the package and its version, such as my-first-script-1.0.0.zip. The same menu is also on the package's own page. The archive holds the package's files as they are in your library: index.js, your other modules, and asterobot.json if the package has one. The settings people chose for your package on their bots aren't part of it. Import it on another Asterobot Whoever receives the file installs it from their own Library Manager: Click Import a .zip. Drop the file on Drop a .zip here, or click to choose one, or click that area to pick the file. Type a Name. Version fills in by itself when the file name ends with a version, as in -1.0.0.zip. Click Import. A Package imported toast confirms it. The name and version come from the dialog, not from the archive. If a package with the same name and version is already installed, the dialog warns This replaces the installed my-first-script 1.0.0. and Import replaces it. Archives can't be bigger than 32 MiB. Manage your library covers importing from the player's side, and Zip files and the command line describes the archive in detail. Give each change a new version A package is identified by its name and its version together. Your library can hold my-first-script 1.0.0 and 1.0.1 side by side, and each bot plays one exact version. So when you change a package you've already shared, give the change a new version instead of reusing the old number. Two different packages under the same name and version can't be told apart, and importing one replaces the other. To start a new version from the current one: In Library Manager, open your package's menu and choose Copy to a new version. The Copy this package dialog suggests the same name, with the last number of the version raised by one: 1.0.0 becomes 1.0.1. Change it if you want another number. Click Copy. The editor opens on the new version. Dependencies, permissions and the compatible version are copied along with the code. Your bots keep playing the version they were on until you Run the new one on them. Names and versions explains how to number versions. Publish on asterobot.net The marketplace is the Library category of asterobot.net, and any asterobot.net member can publish a package there by uploading the .zip you exported. Before you publish has a checklist, and Publish on asterobot.net walks you through it. When you publish a new version later, follow Publish an update.
-
Add a setting
The chat bot from Send messages has its trigger and its reply written in the code. Turn them into settings, and anyone who runs your package can change them from the bot's page, without editing code and without stopping the script. Declare the settings A package declares its settings by exporting an object named parameters from index.js. Each key is the name of a setting, and its value describes the setting: export const parameters = { enabled: { type: "bool", label: "Answer in chat", default: true, }, trigger: { type: "string", label: "Trigger", description: "The chat message the bot answers.", default: "!hello", }, reply: { type: "string", label: "Reply", description: "What the bot says back.", default: "Hello from my first script", }, }; Field What it does type bool, string, int, double, array or map. It decides the input shown on the bot's page: a switch for bool, a text box for string, a number box for int and double, a list for array and map. label The name shown next to the input. Without it, the setting's own name is shown. description A line of help shown with the input. default The value the setting has until someone changes it. Without it, the setting starts empty: false, "", 0, an empty list or an empty map, depending on its type. Settings can also have a placeholder, a unit, a min and a max, a list of choices, and, for lists and maps, an of type. Declare settings describes all of them and the rules they follow. Asterobot reads these declarations by running the top level of your modules, without calling your default export. Keep the top level of index.js to imports and declarations: any other code placed there also runs when Asterobot reads the package's settings. Use them in the script This is the script from Send messages, reading its trigger and its reply from the settings: import { session, botInfo, botWarn } from "asterobot:bot"; import { values, onChange } from "asterobot:parameters"; import { on, send, wait } from "asterobot:protocol"; export const parameters = { enabled: { type: "bool", label: "Answer in chat", default: true, }, trigger: { type: "string", label: "Trigger", description: "The chat message the bot answers.", default: "!hello", }, reply: { type: "string", label: "Reply", description: "What the bot says back.", default: "Hello from my first script", }, }; export default async function behavior(launch) { onChange(({ name, value }) => { botInfo(`Setting ${name} is now ${value}`); }); on("ChatChannelMessageEvent", async (traffic) => { // Read on every message, so an applied change counts from the next line. if (!values.enabled || traffic.payload?.content !== values.trigger) return; // Kept for the whole exchange, because the setting can change meanwhile. const reply = values.reply; // A reply equal to the trigger would answer its own echo forever. if (reply === values.trigger) return; try { // The wait starts before the reply goes out, so a quick echo isn't missed. await Promise.all([ wait( (echo) => echo.type === "ChatChannelMessageEvent" && echo.payload?.content === reply, { timeout: 5000 }, ), send("ChatChannelMessageRequest", { channel: "GLOBAL", content: reply }), ]); botInfo("Reply delivered"); } catch (error) { // An error that escapes a handler stops the whole script. botWarn("Reply not delivered:", 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, }); } } values, from asterobot:parameters, holds your package's current settings by name, so values.trigger is whatever the Trigger field says. It's live: when someone applies new settings on the bot's page, the running script reads the new values from then on, without restarting. That's why the handler reads values each time a message arrives instead of copying the settings once at the start. values is read-only, so don't assign to it or delete from it: assigning throws a TypeError with 'Set' on a dynamic object returned false, and deleting throws Could not delete property "<name>" of a dynamic object. Either way, the setting keeps the value set on the bot's page. A setting declared as int or double reads as a regular number, not as a BigInt. onChange() is for doing something the moment a setting changes. Its handler runs once for each setting whose value changed, only for your own package's settings, and receives the setting's name and its new value. Like a message handler, an onChange() handler that throws an error stops the script. Change the settings in Asteroboard Run the package on your MITM bot. On the bot's page, open Settings, then Package settings. Your package's section lists Answer in chat, Trigger and Reply with their current values. Change Reply: Not applied appears next to the package's name. Click Apply. A toast says my-first-script settings applied, and the bot's Console shows Setting reply is now followed by your text. Say the trigger in the Dofus chat, and the bot answers with the new reply. Reset throws away the changes you haven't applied. Settings are saved with the bot, even while no script is running, and the next start uses them. Package settings shows the settings of the bot's package, the one that Run or Load a new package gave it, whether or not it's running. A bot without a package shows No package selected. When you change the declarations in the editor, save them and click Refresh in this section to see the new fields, then Run the package again so the running script gets them too. When settings are refused What you see What it means Couldn't read this package's settings, in Package settings A declaration is invalid, for example a default that doesn't match its type. The alert gives the reason, ending with something like parameter "enabled" default: want a bool, got string. Until you fix it, the script starts with no settings at all: every read from values returns undefined. Couldn't apply the settings A value you applied doesn't fit its declaration, for example a number below the setting's min. The toast gives the reason, such as retries: 0 is below the minimum 1, and nothing from that form is saved. A package can also put buttons on the bot's page: see Declare actions. Next, Share your package.
-
Send messages
Reading the game is half of what a script does. Here the chat script learns to answer: when someone says !hello, the bot replies in chat and checks that its reply went through. Warning On a MITM bot (man-in-the-middle: the bot relays the game session of the Dofus client you play), everything the script sends goes out as your character. The reply on this page is a real chat message that other players can see. Try the message by hand first Before writing code, send the message from the Network tool. If it works there, you know the payload is right. On the bot's page, open Tools > Network > Send a game message. Keep To the server selected, and pick ChatChannelMessageRequest in the list of messages. Type some text in content and leave the other fields as they are. Empty fields aren't sent, and a channel left out means GLOBAL, the general channel. Check the payload under This is what will be sent., then click Send ChatChannelMessageRequest. Your line appears in the game. If ChatChannelMessageEvent is selected in Settings, Traffic also shows your request going Out and your line coming back In. send() await send("ChatChannelMessageRequest", { channel: "GLOBAL", content: "Hello" }); send() takes a message name and a payload holding the message's fields. It builds the message and sends it to the game server, and its promise resolves as soon as the message is sent: it doesn't wait for an answer. The promise rejects when the message can't be built or sent: Problem Error A field the message doesn't have ChatChannelMessageRequest.contnt: unknown protobuf field A value of the wrong type ChatChannelMessageRequest.content: expected string A channel name that doesn't exist ChatChannelMessageRequest.channel: unknown enum symbol "GENERAL" A message name Asterobot can't send semantic protobuf message is not mapped: MyMessageRequest Sending takes longer than the timeout The promise rejects after 30 seconds, or after the timeout you pass channel takes a channel's name as a string, such as "GLOBAL", "GUILD" or "PARTY", or the channel's number. The Dofus protocol reference lists every channel. Wait for the answer The game doesn't answer a chat line with a response. It sends the line to everyone on the channel as a ChatChannelMessageEvent, you included. To know your line went through, wait for that event. A wait only sees messages that arrive after wait() is called, and nothing guarantees that send() resolves before the game's answer comes in. So start the wait first, then send, and await both together: await Promise.all([ wait( (traffic) => traffic.type === "ChatChannelMessageEvent" && traffic.payload?.content === "Hello", { timeout: 5000 }, ), send("ChatChannelMessageRequest", { channel: "GLOBAL", content: "Hello" }), ]); wait() is called first, so it's already listening when the line goes out. Promise.all resolves once both have succeeded, and rejects as soon as either one fails. Two other ways of writing this look fine and aren't: a wait started after await send() can miss an answer that came quickly, and a wait kept in a variable to await after send() is left behind when send() fails. Waiting explains both. wait() resolves with the next inbound message that matches, and you choose what matches: a message name, or a function that returns true for the message you want. The function above checks the name and the text, so another player's line doesn't count. wait() has no timeout of its own. A wait that never matches stays open until the script stops, so always pass { timeout }, in milliseconds. When the time runs out, the promise rejects with protocol wait timed out. request() request() sends a message and resolves with the game's response to it. Only a handful of Dofus requests get a response, matched to them by uid, and none of them is something a first script needs to send. For every other message nothing comes back: request() waits until its timeout, 30 seconds unless you pass another one, and then rejects with context deadline exceeded. So for almost everything, wait for the event the game answers with while you send(), as this page does. Sending covers request(), and the to option that sends a message to the Dofus client instead of the server. Timeouts Function Default timeout To change it send() 30 seconds { timeout: milliseconds } as the third argument request() 30 seconds { timeout: milliseconds } as the third argument wait() None: it waits until the script stops { timeout: milliseconds } as the second argument A timeout must be a positive number of milliseconds. Anything else rejects with timeout/milliseconds must be a finite positive Number. The complete script import { session, botInfo, botWarn } from "asterobot:bot"; import { on, send, wait } from "asterobot:protocol"; const TRIGGER = "!hello"; const REPLY = "Hello from my first script"; export default async function behavior(launch) { on("ChatChannelMessageEvent", async (traffic) => { // Only the exact trigger: the game also sends our own reply back to us, // and answering that echo would start a loop. if (traffic.payload?.content !== TRIGGER) return; try { // The wait starts before the reply goes out, so a quick echo isn't missed. await Promise.all([ wait( (echo) => echo.type === "ChatChannelMessageEvent" && echo.payload?.content === REPLY, { timeout: 5000 }, ), send("ChatChannelMessageRequest", { channel: "GLOBAL", content: REPLY }), ]); botInfo("Reply delivered"); } catch (error) { // An error that escapes a handler stops the whole script. botWarn("Reply not delivered:", 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 handler is async so that it can await. Handlers don't wait for each other: while this one waits for the echo, the bot keeps handling other messages, the echo included, which is how wait() gets to see it. The try/catch keeps a failed reply from ending the script, and String(error) turns the error into text the console can print. Anyone who says !hello on a channel your character receives gets an answer in the general channel, so stop the script when you're done testing. Test it Run the package on your MITM bot. Say !hello in the Dofus chat. The bot answers in the general channel, and its Console shows Reply delivered. If the console shows Reply not delivered: followed by a reason, select ChatChannelMessageRequest and ChatChannelMessageEvent in the Network tool's Settings, try again, and check in Traffic whether the request went out and whether the line came back. Next, Add a setting lets people change the trigger and the reply without editing your code. Waiting covers every way to wait for a message.
-
React to messages
Most of a script's work is reacting to what the game says. Here you'll build the smallest useful example: a script that writes every chat line the bot receives to the bot's console. Test it on a MITM bot (man-in-the-middle: the bot relays the game session of the Dofus client you play). A Full socket bot doesn't get into the game until a connection script takes it there, and chat only starts once a character is in game. Your first bot sets up a MITM bot. Find the message Before you write a handler, you need the message's name and the names of its fields. The Network tool shows both. On your MITM bot's page, open Tools > Network > Settings. Type Chat in the filter box and tick ChatChannelMessageEvent. Click Apply. A toast says Now mirroring 1 message type(s). Open Traffic, then say something in the Dofus chat. A ChatChannelMessageEvent row appears: the server sends your own line back to you, as it sends everyone else's. Click the row to see its fields. The fields have the names a script reads, in camelCase: content is the text, senderName the speaker's character name, and channel the channel it was said on, as a name such as GLOBAL. There's one difference: 64-bit numbers such as senderCharacterId show in quotes here, while a script receives them as BigInt values. The Dofus protocol reference says what every field means. The Live chat switch on the bot's Console tab turns on this same message: both switches are one setting. Write the handler Replace the contents of index.js with: import { session, botInfo } from "asterobot:bot"; import { on, send } from "asterobot:protocol"; 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; const { channel, senderName, content } = traffic.payload; botInfo(`[${channel}] ${senderName}: ${content}`); }); // 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("ChatChannelMessageEvent", handler) registers handler for every inbound message with that name. It returns immediately, and the handler then runs each time such a message arrives, until the script stops. That's why the default export can finish right after registering it. The handler receives each message as a traffic object, and traffic.payload holds its fields. The check at the top of the handler matters: a handler that throws an error stops the whole script, and a message Asterobot couldn't decode has no payload to read. The handler is registered before the identification on purpose. Registering a handler sends nothing, and while send() is being awaited, the messages that arrive already reach the handlers registered before it. on() also takes "*" to receive every inbound message, or a function that decides which messages to keep. Listening covers those options, the order handlers run in, and off(). Test it In the editor, pick your MITM bot and click Run. On the bot's page, open Console. In Sources, keep only Script. Say something in the Dofus chat. A line such as [GLOBAL] Airelle: hello appears in the console. With Live chat on and Chat in Sources, the same line also shows as a chat line: one comes from Asteroboard's live chat, the other from your script. Your script receives chat lines either way, because what you select in the Network tool only changes what Asteroboard shows. Next, Send messages makes the script answer. Traffic lists every property of a traffic object.
-
Identify and run
A package does nothing until a bot plays it. Start by running the starter package on a bot, then send its output to the bot's page and add the lines that identify a Full socket bot on the game server. Before you start You need a bot whose header shows Connected to game server. The easiest is a MITM bot (man-in-the-middle: the bot relays the game session of the Dofus client you play). There's no Ankama account to store, and you can watch the result in your own game. Your first bot sets one up, and it shows Connected to game server once your Dofus client has connected through it. A Full socket bot works too: see Full socket mode. Run the package Open your package in the editor. In the bot picker, choose your bot. Click Run. Asteroboard saves the package, stops whatever the bot was playing, loads your package on it and starts it. A toast says Running on followed by the bot's name. Run also makes this package the bot's package. The bot's header now shows my-first-script@1.0.0, and Play on the bot's page starts it again later. If the package doesn't start, the toast says Couldn't run the package instead. When something goes wrong explains where to look. Where the output goes The starter code writes with info() from asterobot:console, so its line lands in Server > Console: started (resume) on a MITM bot, or started (initial) the first time it starts on a Full socket bot's connection. Scripts have two sets of output functions, and they write to different places: Functions Module Where the lines appear botDebug(), botInfo(), botWarn(), botError() asterobot:bot The bot's Console tab, as Script lines at that level. Lines of every level show there. debug(), log(), info(), warn(), error(), also available as console.debug(), console.log() and so on asterobot:console Server > Console, and the terminal Asterobot runs in. The lines don't say which bot wrote them. By default debug lines are left out: set Server level to DEBUG on that page to see them there. That console is an object exported by asterobot:console: scripts have no global console, so it has to be imported like the rest. While you write a script, the bot's console is almost always the one you want. Change index.js so its output lands there: import { botInfo } from "asterobot:bot"; export default async function behavior(launch) { botInfo("Started, launch reason:", launch.reason); } Click Run again, then open the bot's Console tab: the line is there. When you pass several values, the console joins them with spaces. Identify a Full socket bot In Full socket mode, Asterobot signs in, picks the game server and opens the game connection, then leaves the rest to the script. The first message on a new game connection has to be IdentificationRequest, carrying the ticket the login server issued for that connection. These are the lines every sample in these docs uses: import { session } from "asterobot:bot"; import { send } from "asterobot:protocol"; 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, }); } } Field Value What it is ticketKey session.gameToken The ticket the login server issued for this game connection, which Asterobot kept while signing in. languageCode session.language The language code the script was started with. The game server uses it for the texts it sends back. Right now that's always fr for a script started from Asteroboard, whatever language the bot itself is set to: nothing Play, Run, Load a new package or Load a package and play it send changes it. The Dofus protocol reference writes these fields ticket_key and language_code. A script can send either spelling, and these docs use camelCase because that's how scripts read fields back. When a script has message handlers, register them above this block, as the next pages do. Registering a handler sends nothing, so the identification is still the first message out, and handlers registered earlier already receive what arrives while send() is awaited. Caution Never log session.gameToken or show it to anyone. It's the ticket to the bot's game session, and a stranger who has it could take that session over. A field the message doesn't have makes send() reject. Sending token instead of ticketKey, for example, fails with IdentificationRequest.token: unknown protobuf field, and the script stops with that error. Identifying doesn't take the bot any further. Choosing a character and entering the game are up to scripts too: Asterobot doesn't do them for a Full socket bot. Note Coming soon. Ready-made basic and full connection scripts will take a Full socket bot into the game. Until they're available, you can learn what the game expects by watching your own Dofus client, as described below. Watch what your Dofus client sends A MITM bot shows the messages your client exchanges with the game server, in the order they happen. To see what logging in to a character takes: Before you launch Dofus, open your MITM bot's page, then Tools > Network > Settings. Tick the messages you want to watch. The select button next to the filter ticks every message the filter shows. Click Apply. Open Traffic, then launch Dofus and log in to a character. Read the list from the top. Out marks what your client sent and In what the server sent. Click a message to see its fields. If Settings then shows a badge such as "12 message(s) dropped - the stream couldn't keep up", select fewer messages and log in again. Why the check is harmless in MITM mode The same package runs on both kinds of bot. On a MITM bot, your Dofus client identifies the session before any script starts, so Asterobot reports the script's first start as "resume", never "initial", and the if skips the identification. Even a script without the check can't identify a relayed session a second time. Asterobot refuses to forward the message and writes Refused to forward a message that would re-authenticate a relayed session in the bot's console. Launch reasons launch.reason is "initial" on a Full socket bot's new game connection, until a script has started successfully on it. Every later start on that connection is "resume", and so is every start on a MITM bot. The entry function lists every reason and what each one asks of your script. A start counts as successful once the default export's function has finished. If a script sends IdentificationRequest and its function then fails before finishing, the next start on that connection is "initial" again, and the script identifies a second time. Handle the errors that can happen after the identification so the first start succeeds. Stop and play again The bot's header has the controls for its package: Control What it does Play Starts the bot's package. It's disabled while the bot isn't connected, with the tooltip Connect the bot to a game before playing a behavior. When the bot has no package yet, it opens the Load a behavior selection dialog instead. Stop Takes the place of Play while a script runs. It stops the script and nothing else: the bot stays connected to the game. The menu next to them Load a new package, Load a package and play it and Unload package. There's no Reload button. To restart a script with your latest code, click Run in the editor, or Stop and then Play on the bot's page: both start the code you last saved. Play, stop and reload explains what's kept between two starts and what starts over. When something goes wrong What you see What it means A number on the editor's error badge The editor found errors, such as a misspelled function or a field a message doesn't have. It doesn't stop you from saving or running, but the script will probably fail. Couldn't run the package after Run, or Couldn't start the behavior after Play The script didn't start, and the toast gives no reason. Open the bot's Settings tab: when the code can't even be loaded, for example because of a syntax error or an import that doesn't resolve, Package settings shows Couldn't read this package's settings with the reason. The editor's error badge usually points at the line too. The behavior stopped with an error, at the top of the bot's page The script started, then threw an error or had an await reject without a catch, while starting or later. The alert shows the complete error text, with Copy and Dismiss. In Bot Manager, the bot's Behavior column says Failed. The error text starts with where the script failed. 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 Dismiss hides the alert. A different error still shows up, even after you've dismissed the previous one. Errors explains what stops a script and what doesn't, and Error messages lists the texts you can meet. Next, React to messages.
-
Create a package
Every script lives in a package. You create one in Library Manager, and Asteroboard opens it in the editor with a few lines of starter code that already run. Create the package In the sidebar, open Library Manager. Click New package. The New local package dialog opens. In Name, type a name for your package, for example my-first-script. Leave Version at 1.0.0. Click Create and edit. The dialog closes and the editor opens on your package, showing index.js. Back in Library Manager, the package has its own row with the Local badge. Field Default Rules Name Empty Required. A package you create is local, so its name can't start with @ and can't contain : or @ anywhere. Version 1.0.0 Required. Names and versions explains how to number versions. The dialog tells you when something is wrong: Message What to do A new package is local, so its name can't start with '@' - those are assigned by the marketplace. Remove the @. Names that start with @ belong to packages installed from the marketplace. A local package name can't contain ':' or '@'. Remove the : or the @. my-first-script 1.0.0 already exists and would be replaced. Your library already has a package with that name and version. Create and edit stays disabled until you change one of them. Couldn't create the package Asterobot didn't create the package. This toast gives no reason. A tour of the editor The top bar Item What it does Box icon Opens the package's own page, with its details and its manifest. Name and version The package you're editing, such as my-first-script @1.0.0. Unsaved Appears while some changes aren't saved. Incompatible Appears when the Asterobot versions the package says it works with don't include the one you run. See Compatibility. Error count A badge such as 2 error(s) when the editor finds errors. It counts every file of the package, not only the open one. IntelliSense Hover it to see how many game messages the editor can complete. If it says Couldn't load the typings - editing works, completion doesn't., you can keep editing, without completion. Bot picker Chooses the bot that Run starts the package on. It only appears when at least one bot is connected to a game. Game Opens a panel next to the code with the map viewer (Atlas) and the game data browser (Datacenter). Save Saves every file of the package. Ctrl+S does the same. The button is disabled while nothing has changed. Run Saves, then starts the package on the bot in the picker. It's disabled when no bot is connected to a game, with the tooltip No bot is connected to a game. Files The Files column lists the package's modules, index.js first. A dot after a file's name means it has unsaved changes, and a number next to it counts its errors. The plus button adds a file. The Add a file dialog asks for a Path, such as lib/helper.js. The path must end in .js or .mjs, stay inside the package and not be taken already, and the file only exists once you save. The trash icon, shown when you hover a file, removes it from the list, and saving deletes it from the package. index.js has no trash icon: it's where every script starts. Saving replaces the package's files with exactly what the editor holds. A bot that's already playing the package keeps running the code it started with until it starts again, which is what Run does. The starter code A new package's index.js contains: import { info } from "asterobot:console"; export default async function behavior(launch) { info(`started (${launch.reason})`); } Line by line: import { info } from "asterobot:console"; brings in the info() function from the built-in asterobot:console module. Everything Asterobot gives scripts comes from an asterobot: module imported this way. export default async function behavior(launch) { declares the function Asterobot calls when a bot starts the script. It has to be the default export of index.js, and it has to be async. Its name doesn't matter. launch is an object Asterobot passes in. launch.reason says why the script is starting: "initial", "resume" or "reload". The next page explains what each value means for your script. info(`started (${launch.reason})`); writes a line such as started (initial) to Asterobot's own log. That log is in Server > Console, not in the bot's Console tab, so don't look for it on the bot's page. The closing } ends the function. Returning from it doesn't stop the script: anything the function set up, such as handlers for game messages, keeps working until the script is stopped. This one sets nothing up, so it has nothing left to do. The starter code doesn't identify the bot on the game server. On a Full socket bot it runs, but nothing happens in the game. Identify and run fixes that, and moves the output to the bot's page.