Aller au contenu
Afficher dans l'application

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

Asterobot

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

Pour installer cette application sur iOS et iPadOS
  1. Appuyez sur Icône de partage dans Safari
  2. Faites défiler le menu et appuyez sur Ajouter à l'écran d'accueil.
  3. Appuyez sur Ajouter dans le coin supérieur droit.
Pour installer cette application sur Android
  1. Appuyez sur le menu à trois points (⋮) dans le coin supérieur droit du navigateur.
  2. Appuyez sur Ajouter à l'écran d'accueil ou Installer l'application.
  3. Confirmez en appuyant sur Installer.
What session tells a script, how Full socket and MITM bots differ for a script, sharing a character with the person playing, and what disconnect() does.
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. connectionId BigInt Which of the bot's game connections this is: one more each time the bot gets a new one. 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.
What belongs to one connection
A script started again on the same connection, after Stop then Play, finds the game where it left it: the character is already chosen and on a map, and the server won't answer a second IdentificationRequest. Only the script's own variables are gone. To carry on rather than start over, keep what it learned in the bot's memory, with the connection it belongs to:
import { memory, session } from "asterobot:bot"; import { send } from "asterobot:protocol"; const KEY = "@alice:hello/connection"; export default async function behavior(launch) { const stored = memory.get(KEY); const state = stored?.id === session.connectionId ? stored : { id: session.connectionId }; // Recorded before sending: a script stopped right after the send must never // identify a second time on this connection. A MITM bot's Dofus client has // already identified the session. if (!state.identified && !session.shared) { memory.set(KEY, { ...state, identified: true }); await send("IdentificationRequest", { ticketKey: session.gameToken, languageCode: session.language, }); } } A value stored for an earlier connection has another id, so the script knows it's out of date. Bot memory builds this into a state object that follows every step of a connection. launch.reason isn't enough to decide: when a start fails half way, the next one is still "initial" on the same connection.
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. Who answers the server's proof of identity and polls, pings, and says the character is ready on each map Your script, or @asterobot:base The Dofus client. Don't answer them too: the server would get every answer twice. 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 a MITM bot, a script that would have been given a skipped message stops instead, with game traffic lost: Asterobot fell behind the game and missed messages, rather than carry on with a hole in what it saw. On either kind of bot, a script with more than 8 MB of game messages waiting for it stops with the script fell too far behind the game: 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 the messages it decides about wait for your script, however long it takes: 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 first handles every message that arrived before the end, such as the server's reason for closing, then stops: its waits and requests 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.
  • 91 vues
What changes for scripts when Ankama patches Dofus, what messages look like while Asterobot doesn't know the new version, and how to write scripts that keep working.
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.
The last page of this chapter, Fights, covers how scripts and Asterobot's fight module share a bot's fights.
  • 95 vues
What game data a script can read, which game version and language it comes from, when it's available, and the limits every lookup shares.
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 5 s limit of the code calling them, described in Async and timing, and thousands of calls in a row keep the bot from handling its messages meanwhile.
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 the SQL of a query() 10 seconds, counted once the query's turn has come 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 request() and movements The call rejects with behavior resource limit exceeded: maximum pending operations reached. A table(), find(), search() or query() call has no time limit of its own besides that: it ends when its work is done, however busy the PC or the other bots are. | 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.
  • 106 vues
Read game texts, records and tables with text(), record(), table(), find() and search(), what each one returns, and the errors each one can raise.
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.
  • 77 vues
Run read-only SQL on a game version's data with query(), pass parameters, read the result, stay within the limits, and try queries in the SQL tab first.
Some questions need a join, a count or a sort that the lookups can't do. query() runs SQL on the bot's game data and hands back the result. Queries are written in SQLite's dialect of SQL, the same one the game version's SQL tab runs.
query()
query(sql, params, options) resolves to an object with three properties:
Property What it holds columns The name of each column, in the order the query selects them. A name can appear twice, as in SELECT a._id, b._id. rows One array per row, holding the values in the same order as columns truncated true when the query had more rows than the limit let through Option Default What it does limit 200 How many rows to return, 5,000 at most Rows are arrays rather than objects because column names can repeat, so read the values by position:
const result = await query( "SELECT language, name FROM _names WHERE record_table = ? AND record_id = ? ORDER BY language", ["monsters", 147], ); for (const [language, name] of result?.rows ?? []) { botInfo(language, name); } This query lists the names of one monster in every language the game data holds, which is also how a script reads a language other than its own. query() resolves to undefined when the bot's game version has no game data, hence result?.rows.
Read-only
The game data is opened read-only. INSERT, UPDATE, DELETE, CREATE and every other statement that writes fail, and ATTACH is refused too, so a query can't reach any other database file.
Every statement in the string runs, and query() returns the result of the last one: SELECT 1; SELECT 2 returns 2. Since nothing can be written, the statements before the last one only cost time.
Parameters
Write ? wherever a value goes, and pass the values in the params array, in the same order. A parameter keeps its type, so a number is compared as a number, whereas a value written into the SQL between quotes, like '147', is text.
params has to be an array: anything else rejects with gamedata.query params must be an array. Pass numbers and strings, and convert BigInt ids with Number() first.
Writing the SQL
Put text between single quotes, as in 'monsters'. Double quotes only name a table or a column, so "monsters" is never a string, and a mistyped column name gives an error instead of a wrong result. Names go through the _names table, with the columns record_table, record_id, name_id, name, language and ordinal. A record can have several names, and ordinal 0 is its main one. The texts of one language are in the table text_ followed by the language code, such as text_fr, with an id and a text column. In a derived table, whose name contains two underscores, each row points to its parent row with _parent_id. _names_fts is a full-text index of the names, much faster than LIKE '%...%'. The SQL tab's Examples show how to use it. Values come back the way they're stored: numbers as regular numbers, never BigInt, text as strings, and an empty value as null. A column holding nested data comes as an array or an object. A computed column, such as COUNT(*), comes back as the database computed it.
Limits and errors
Limit Value Past it Rows 200 unless you pass a limit, 5,000 at most The other rows are left out, and truncated is true. Time 10 seconds for the query, once its turn has come The query is interrupted and the promise rejects. Length of the SQL 64 KiB The promise rejects with a text such as datacenter: query is 70000 bytes, over the 65536 byte limit. Size of one value the query builds 16 MiB The query fails. Queries at once 4 on one game version, shared by every bot and every open SQL tab Other queries wait for their turn, in the order they came, and the wait doesn't count toward their 10 seconds. Calls in progress 32 for a script, counted together with request(), movements and the other game data calls The call rejects with behavior resource limit exceeded: maximum pending operations reached. Problem The promise rejects with sql is empty or isn't a string gamedata.query sql must be a non-empty string sql only holds spaces datacenter: empty query params isn't an array gamedata.query params must be an array A mistake in the SQL A text starting with datacenter: that names the problem, such as no such column: nmae Try it in the SQL tab first
The game version's SQL tab runs queries on the same data, with the same limits, and shows the result as a table. It's the quickest way to get a query right before putting it in a script.
In Game Manager, open the bot's game version, go to its Game data tab, then to SQL. Type a query, or pick one from Examples. They're grouped by topic: explore, names, monsters, items, harvesting, navigation, crafting, maps, quests and text. Click Run, or press Ctrl+Enter. Next to the buttons, the tab shows the number of rows, stopped at the limit when some were left out, and how long the query took. When the query returns what you want, copy it into your script, and replace the values you typed with ? and params. The tab has no parameters, so write the values into the SQL while you try a query there. It also shows up to 500 rows, where a script gets 200 unless it passes a limit. When a query fails, the tab shows the same text a script would get. SQL describes the tab in full.
A complete example
This package adds a button that lists the sub-areas where a monster lives, with a join the lookups can't do:
import { session, botInfo } from "asterobot:bot"; import { query } from "asterobot:gamedata"; import { send } from "asterobot:protocol"; const SUB_AREAS_OF_MONSTER = ` SELECT DISTINCT area.name FROM _names monster JOIN monsters__subareas link ON link._parent_id = monster.record_id JOIN _names area ON area.record_table = 'sub_areas' AND area.record_id = link.value AND area.language = monster.language WHERE monster.record_table = 'monsters' AND monster.language = ? AND monster.name = ?`; export const actions = { subAreas: { label: "Find where a monster lives", args: { monster: { type: "string", label: "Monster name" }, }, async run({ monster }) { const result = await query(SUB_AREAS_OF_MONSTER, [session.language, monster]); if (result === undefined) throw new Error("No game data for this bot's game version"); const areas = result.rows.map(([name]) => name); botInfo(`${monster}:`, areas.length > 0 ? areas.join(", ") : "no sub-area found"); }, }, }; export default async function behavior(launch) { // A new game connection starts with this message. MITM bots never launch // with "initial": the Dofus client has already identified their session. if (launch.reason === "initial") { await send("IdentificationRequest", { ticketKey: session.gameToken, languageCode: session.language, }); } } The query takes the script's language as its first parameter, so type the monster's French name for now. The button appears under Package settings on the bot's Settings tab once the package runs, as in Lookups.
  • 85 vues
Declare the settings your package shows on a bot's Settings tab, with every field, the six types, choices, bounds, defaults, the errors Asterobot reports and editor typing.
Settings let the people who run your package change what it does without touching its code. Add a setting showed the basics, and Declarations keeps the compact schema with the full list of error texts.
Where the declaration goes
A package declares its settings in one place: an object exported as parameters from its index.js. Each key is the name of a setting, the name your script reads later in values, and each value describes the setting.
/** @type {BehaviorParameters} */ export const parameters = { enabled: { type: "bool", label: "Answer in chat", default: true, }, delay: { type: "double", label: "Wait before answering", unit: "seconds", default: 1.5, min: 0, max: 10, }, maxAnswers: { type: "int", label: "Answers per player", description: "The bot stops answering a player after this many replies.", default: 3, min: 1, }, tone: { type: "string", label: "Tone", choices: [ { value: "polite", label: "Polite" }, { value: "short", label: "Short" }, ], default: "polite", }, ignored: { type: "array", of: "string", label: "Ignored players", description: "Character names the bot never answers.", }, replies: { type: "map", of: "string", label: "Replies", description: "What a player says on the left, what the bot answers on the right.", default: { "!hello": "Hello!", "!help": "Ask me anything." }, }, }; A few rules decide what Asterobot picks up:
Only an export of index.js counts. An export const parameters in another file of the package is ignored. Settings appear on the bot's page in the order you write them. The one exception comes from JavaScript itself: keys that look like whole numbers, such as "2", are always listed first. The object is ordinary JavaScript. A default can be a constant from another file of the package, or a value computed when the module runs. Asterobot reads the object by running the top level of your modules, without calling your default export and without a bot or a game session. At that moment values is empty and session holds empty values, so a declaration can't depend on either. The entry function lists what else behaves differently while Asterobot reads it. Each package declares its own settings, dependencies included. See Settings in dependencies. The key is also the name Asterobot saves the value under for each bot. Renaming a setting in a later version loses the values people saved, as Read settings explains, so pick keys you won't want to change. An empty key is refused.
Fields
Field Used by What it does type Every setting Required. "bool", "string", "int", "double", "array" or "map". of array and map, where it's required The type of the list's items, or of the map's values: "bool", "string", "int" or "double". Refused on every other type. label Every setting The name shown with the input. Without it, the key is shown, such as maxAnswers. description Every setting A line of help shown with the input. placeholder string, int and double Grey text shown while the box is empty. unit int and double A short word shown at the end of the number box, such as seconds or kamas. default Every setting The value until someone applies another one. See Defaults. choices string and int Turns the input into a drop-down list of allowed values. See Choices. min and max int and double The smallest and the largest value allowed. A value equal to a bound is allowed. ref int, and array of int The game data table the numbers are keys of, such as monsters. See Ask for game data with ref. Asterobot is lenient with what it doesn't understand, which cuts both ways. A field it doesn't know is ignored without an error, so a typo such as lable silently does nothing. A label, description, placeholder or unit that isn't a string is ignored too, and so is a min or max that isn't a number. The JSDoc type described below catches most of these mistakes in the editor.
Some combinations have no effect either:
min and max don't apply to a setting with choices, nor to the items of an array or a map. placeholder and unit aren't shown on a drop-down list, nor on the items of a list or a map. min greater than max is refused. On the bot's page, min and max also limit the arrows of the number box. Asterobot checks the bounds again when someone clicks Apply, whatever was typed.
The six types
type The script reads On the bot's page Without a default bool true or false A switch false string A string A text box "" int A whole number, as a regular number A number box whose arrows step by 1 0 double A number A number box 0 array An array of values of the of type One row per item, numbered from 0, each with a remove button, and Add under the rows [] map An object whose keys are strings and whose values have the of type One row per entry with a Key box, the value and a remove button, and Add an entry under the rows {} What each type accepts, for a default or for a value applied on the bot's page:
bool takes true or false, nothing else. string takes a string. The number 5 isn't turned into "5". int takes a whole, finite number. 3.0 fits, and 3.5 is refused rather than rounded. The script reads it as a regular number, not as a BigInt like the 64-bit fields of game messages. double takes a finite number, so not NaN and not Infinity. array takes an array whose every item fits of. map takes an object whose every value fits of. Lists and maps only hold the four scalar types: a list of lists can't be declared. On the bot's page, a new item starts at false, "" or 0, depending on of, and a map row whose key is left empty isn't saved. Clearing a number box doesn't store an empty value: the box keeps the last number typed in it.
Choices
choices turns a string or int setting into a drop-down list. Each choice is either a bare value, which is then its own label, or an object with a value and a label:
/** @type {BehaviorParameters} */ export const parameters = { slot: { type: "int", label: "Spell slot", choices: [1, 2, 3], default: 2, }, tone: { type: "string", label: "Tone", choices: [ { value: "polite", label: "Polite" }, { value: "short", label: "Short" }, ], default: "polite", }, }; The script reads the value, never the label: values.tone is "polite", not "Polite". The rules:
Only string and int settings can have choices. Every value must fit the setting's type. "2" isn't a valid choice for an int. A choice object without a value is refused. A choice without a label shows its value. The default must be one of the values. Without a default, the setting starts at the first choice. Defaults
The default is what a bot reads until someone applies another value.
Without a default, or with default: null, a setting starts at its type's value from the last column of the table above, or at its first choice. The default has to fit the declaration exactly like a value typed on the bot's page. Asterobot never converts it: default: "20" on an int is refused, and so is a default below min or above max. A default can be any expression, as long as it doesn't need a setting's value or the session. A default only matters while a bot has no saved value for the setting. Asteroboard saves every setting of a package at once, the untouched ones included, when someone clicks Apply and when a bot is added with the package, so a default you change in a later version doesn't reach those bots. Read settings goes through what happens to saved values.
When Asterobot checks the declaration
Asterobot reads and checks the declaration each time it needs it: when a bot's Settings tab shows Package settings, when someone picks the package in the Add a bot dialog, and at every start of the script. Saving in the editor checks nothing. After changing a declaration, save, then open Package settings on a bot set to your package and click Refresh.
Problems fall into two groups:
Problem Examples What happens A setting breaks a rule An unknown type, a default that doesn't fit, choices on a bool, min above max, a ref that isn't a string Package settings and the Add a bot dialog show Couldn't read this package's settings with the reason. The script still starts, but with no settings at all: every read from values gives undefined. The export itself has the wrong shape parameters isn't an object, or one of its settings isn't an object The same message on Package settings, and the script doesn't start: the Problems alert shows a text starting with read declarations of. The reason names the package, then the setting, then what's wrong. With default: "friendly" on the tone setting above, Package settings shows:
chat-helper@1.0.0: read declarations: package "chat-helper" parameter "tone": parameter "tone" default: friendly is not one of polite, short Declarations lists every text you can get.
Typing with JSDoc
Put /** @type {BehaviorParameters} */ on the line above export const parameters, as in the examples on this page. The editor then completes the field names, and marks as errors a field that doesn't exist and a type or of that isn't one of the allowed names: mistakes Asterobot would ignore silently, or only report once it reads the declaration. It doesn't check that a default fits its type, so Asterobot's own check still matters.
These types need no import:
Type What it describes BehaviorParameters The whole parameters object BehaviorParameter One setting, or one argument of an action BehaviorParameterType "bool", "string", "int", "double", "array" or "map" BehaviorScalarType "bool", "string", "int" or "double", the types of accepts BehaviorChoice A choice written as an object, with a value and an optional label Next, Read settings shows how the script uses the values. The arguments of actions, and of steps and conditions, are declared with the same fields.
  • 71 vues
Read your package's current settings with values, react when someone applies new ones with onChange and offChange, and know what happens to saved values when your package changes.
Once your package declares settings, its script reads their current values from asterobot:parameters. It can also react the moment someone applies a change. asterobot:parameters is the reference for the module.
values
import { values } from "asterobot:parameters"; values has one property per setting your package declares, named by its key: values.maxAnswers for a setting declared as maxAnswers. Each value has the type of its setting: a boolean for bool, a string for string, a regular number for int and double, an array for array and an object for map.
The values are the ones saved for the bot, laid over the defaults of your declaration. A setting nobody changed reads as its default.
values is live. After someone applies new settings, the next read returns the new value, without restarting the script. So read a setting at the moment you need it, in a handler or at each turn of a loop, instead of copying it into a variable when the script starts. When a value has to stay the same for a whole exchange, copy it at the start of that exchange, as the chat script in Add a setting does with its reply.
A few more things about values:
Object.keys(values) lists the settings, and "delay" in values tells whether one exists. It's read-only. Assigning to one of its properties throws a TypeError, and so does deleting one. asterobot:parameters quotes both texts. It only holds your own package's settings. In a dependency, values holds the dependency's settings: see Settings in dependencies. When Asterobot can't read the declaration, values is empty and every read gives undefined. Declare settings says when that happens. An inline script can declare settings, but they never appear on the bot's Settings tab, so it always reads its defaults. When new values arrive
Here is what happens when someone changes settings under Package settings on the bot's Settings tab, then clicks Apply:
Asteroboard sends every setting of that package's card: the ones that changed and the ones that didn't. Asterobot checks each value against your declaration. If one doesn't fit, nothing is saved, and Couldn't apply the settings appears with the reason. Asterobot saves the values for the bot. If a script is running, Asterobot hands it the new values. values returns them from then on, then the onChange() handlers run for each setting whose value changed. All the values of one Apply reach the script together, before any handler runs, so a script never sees half an edit.
When no script is running, the values are only saved, and the next start reads them. The same thing happens in the rare case where the running script already has 32 operations waiting when someone clicks Apply: see Limits.
React to a change
onChange() registers a function that runs when an applied value differs from the previous one. Use it when a change should do something right away. When the script only needs the current value the next time it looks, reading values is enough.
This script writes a warning on the bot's console when the character's pods go over a limit taken from the settings. When someone changes the limit, onChange() checks the last pods the game reported against the new limit at once, instead of waiting for the game to report them again:
import { session, botInfo, botWarn } from "asterobot:bot"; import { values, onChange } from "asterobot:parameters"; import { on, send } from "asterobot:protocol"; /** @type {BehaviorParameters} */ export const parameters = { warnAt: { type: "int", label: "Warn when pods reach", unit: "%", default: 90, min: 1, max: 100, }, }; // Kept so the pods can be checked again when the limit changes. let lastWeight; function checkWeight() { if (!lastWeight || lastWeight.weightMax <= 0) return; const percent = Math.floor((lastWeight.inventoryWeight * 100) / lastWeight.weightMax); if (percent >= values.warnAt) { botWarn(`Pods at ${percent}%, the limit is ${values.warnAt}%`); } } export default async function behavior(launch) { on("InventoryWeightEvent", (traffic) => { // A message Asterobot couldn't decode has no payload, and an error // thrown in a handler stops the whole script. if (!traffic.payload) return; lastWeight = traffic.payload; checkWeight(); }); onChange(({ name, value }) => { botInfo(`Setting ${name} is now ${value}`); if (name === "warnAt") checkWeight(); }); // A new game connection starts with this message. MITM bots never launch // with "initial": the Dofus client has already identified their session. if (launch.reason === "initial") { await send("IdentificationRequest", { ticketKey: session.gameToken, languageCode: session.language, }); } } To try it, Run the package on a MITM bot (man-in-the-middle: the bot relays the game session of the Dofus client you play). On the bot's Settings tab, under Package settings, set Warn when pods reach to 1 and click Apply. The bot's Console shows Setting warnAt is now 1, and a warning as soon as the game has reported the character's pods since the script started.
The handler receives an object with the setting's name and its new value, of the setting's type. Some details:
It runs once for each setting whose value changed, and only for your own package's settings. It doesn't run when the script starts, and an Apply that changes nothing doesn't call it. A list or a map counts as changed only when its content differs. With several handlers, the order they run in isn't fixed. Don't write one that depends on another having run first. A handler that throws stops the whole script, like a message handler. The Problems alert then starts with parameters.onChange handler, or with unhandled Promise rejection: when an async handler's promise rejects. Put anything that can fail in try/catch. onChange() returns a handle. Pass it to offChange() when the script no longer needs to hear about changes:
const handle = onChange(({ name }) => { botInfo(`${name} changed`); }); // Once the script doesn't need it any more. offChange(handle); A script can have up to 256 onChange() handlers registered at once, counted apart from its message handlers. See Limits.
Saved values
Settings are saved per bot, under the package's name, whatever its version. For you as the author, that means:
Two bots playing your package each have their own values. A bot moved to another version of your package keeps its values. Asteroboard saves every setting of a package's card at once, the untouched ones included, when someone clicks Apply, and when a bot is added with your package picked in the Add a bot dialog. From then on, the bot has a saved value for each of those settings, and the defaults of your later versions don't change them. A saved value is used only while it fits the declaration of the version the bot plays. When it doesn't fit, the default applies, but the saved value isn't deleted: it comes back if the bot returns to a version where it fits, unless someone applied a new value to that setting in the meantime. Here is what a bot sees after it moves to a new version of your package:
In the new version, you A bot with a saved value reads A bot without one reads Add a setting Its default Its default Change a default Its saved value The new default Rename a setting The default, under the new name. The value saved under the old name isn't used. The default Change a setting's type Its saved value if it still fits, such as a whole number saved for a setting now declared double, otherwise the default The default Remove a choice, or narrow min and max Its saved value if it's still allowed, otherwise the default The default Remove a setting Nothing: the setting is gone Nothing So once people use your package, keep the names of its settings. And when a setting should mean something else, give it a new name: an old value that still fits the type would otherwise be read with the new meaning.
The settings of a dependency are saved under the dependency's own name. Settings in dependencies explains what follows from that.
What a script can't do with settings
Change a setting. values is read-only, and the next Apply would send the values shown on the page anyway. Read the settings of another package, whether it's the package that uses it or one of its dependencies. Keep data for its next run. Each start begins again from scratch: see Play, stop and reload. Next, Declare actions.
  • 65 vues
Put buttons on a bot's Settings tab with export const actions, with their fields, the run function and its arguments, what the person clicking sees, and when a button can be used.
An action is a button your package adds to the bot's page. Clicking it calls a function of the running script, with values the person can fill in first. Actions suit one-off things someone decides while the bot plays: report something, say something, move on to the next step. For a choice that should last, declare a setting instead.
Declare an action
Actions are declared next to settings, as an object exported as actions from index.js. Each key is the action's name, and each value describes the button and holds the function it runs.
Warning
On a MITM bot (man-in-the-middle: the bot relays the game session of the Dofus client you play), the Say in chat action below sends a real chat message as your character.
import { session, botInfo } from "asterobot:bot"; import { on, send } from "asterobot:protocol"; // The last pods the game reported, for the report action. let lastWeight; /** @type {BehaviorActions} */ export const actions = { reportPods: { label: "Report pods", description: "Writes the character's pods to the bot's console.", run() { if (!lastWeight) { botInfo("The game hasn't reported the pods since the script started."); return; } botInfo(`Pods: ${lastWeight.inventoryWeight} / ${lastWeight.weightMax}`); }, }, say: { label: "Say in chat", description: "Sends a chat message as the character.", args: { text: { type: "string", label: "Message", placeholder: "Hello!" }, channel: { type: "string", label: "Channel", choices: [ { value: "GLOBAL", label: "General" }, { value: "GUILD", label: "Guild" }, { value: "PARTY", label: "Party" }, ], }, }, async run({ text, channel }) { // A failed action is reported to the person who clicked, and the script // keeps running, so there's no need to catch errors here. if (text === "") throw new Error("Type a message first"); await send("ChatChannelMessageRequest", { channel, content: text }); }, }, }; export default async function behavior(launch) { on("InventoryWeightEvent", (traffic) => { // A message Asterobot couldn't decode has no payload, and an error // thrown in a handler stops the whole script. if (traffic.payload) lastWeight = traffic.payload; }); // A new game connection starts with this message. MITM bots never launch // with "initial": the Dofus client has already identified their session. if (launch.reason === "initial") { await send("IdentificationRequest", { ticketKey: session.gameToken, languageCode: session.language, }); } } Actions follow the same rules as settings about where they're declared: only the actions export of index.js counts, the buttons keep the order you write them in, and /** @type {BehaviorActions} */ gives completion and checks on the fields. A package can export parameters, actions or both.
Fields
Field Required What it does label No The button's text. Without it, the button shows the key, such as reportPods. description No Shown when the pointer rests on the button, and at the top of the dialog that asks for the arguments. Without it, that dialog says "Fill in what this action needs, then run it." args No The values the bot's page asks for before running the action. Each one is declared exactly like a setting. run Yes The function the button calls. Other fields are ignored. An action without run, a run that isn't a function, or args that isn't an object of objects is a mistake in the shape of the export: the script doesn't start at all, and the Problems alert shows a text starting with read declarations of. Declarations lists those texts.
run and its arguments
run receives a single object holding every argument you declared:
The value from the dialog, checked against the argument's declaration with the same rules as a setting: the type, the choices, min and max. The argument's default, for an argument that wasn't sent. An argument declared without a default gets its type's zero value or its first choice, which is why channel above starts at GLOBAL. An action without args receives {}. Arguments are never saved. The dialog opens with the declared defaults every time, whatever was typed the last time.
run can be async. Asterobot reports the result once its promise settles, not when the call returns. Whatever run returns is ignored.
run runs inside the script like a message handler: it shares the script's variables, can use every function of the built-in modules, and waits for its turn while the script handles other events. The code of run that runs without awaiting has the same 5 s limit as a handler. Past it, the action fails with JavaScript execution deadline exceeded, and the script keeps running.
Asterobot checks the arguments against the declaration of the package the bot is set to play, as it's saved in your library. If that declaration can't be read, the values go to run unchecked.
What the person clicking sees
The buttons are under Actions, in your package's card on the bot's Settings tab, in the Package settings section. Each one has a lightning bolt. For an action with arguments, a dialog opens, titled with the action's label, with one field per argument, and Cancel and Run buttons. Once the action is handed to the script, a toast says Action started. When run returns, or its promise settles, a second toast gives the action's key followed by finished, or followed by failed with the error. A failure toast stays until someone closes it. The toasts name the action by its key, such as say failed, not by its label. Choose keys a person can recognize.
An error in run never stops the script. When the action can't even be handed to the script, Couldn't start the action appears instead, with the reason. The two you'll meet most while writing a package are an argument that doesn't fit, such as count: 0 is below the minimum 1, and an action the running script doesn't have. Error messages lists them all.
When actions can be used
Only while a script runs on the bot. Otherwise the buttons are disabled, and resting the pointer on one shows The bot has to be playing a behavior before an action can run. A button calls the action of the script that's running, while the card describes the package the bot is set to play, as saved in your library. After you add, rename or change an action in the editor, save and start the script again, with Run for example. Until then, a new action fails with the running behavior declares no action "<action>" on package "<package>", and a changed run still runs the old code. Asterobot finds the actions when the script loads, before it calls your default export. The buttons work while the entry function is still awaiting something, and after it has returned. The actions of a dependency are in the dependency's card, and run the dependency's own functions. See Settings in dependencies. The actions of an inline script can't be used: the Settings tab never shows its declarations. When a setting or an argument breaks a rule, Package settings shows Couldn't read this package's settings instead of the cards, so no button can be clicked until the declaration is fixed. Declare settings explains those errors.
Next, Settings in dependencies.
  • 63 vues
How the settings and actions of the packages your package depends on are scoped, shown on the bot's page and saved, and how to write a package that others can configure.
A package can use other packages of the library, its dependencies, and each of them can declare settings and actions of its own. A script only reads the settings of its own package, and a bot's Settings tab shows each package's settings apart. Dependencies covers declaring and importing them.
Each package reads its own settings
Every package gets its own copy of asterobot:parameters. Whichever file imports it, values holds the settings declared in the index.js of the package that file belongs to:
In your package, values holds your settings, never a dependency's. In a dependency, values holds the dependency's settings, never yours. An onChange() handler registered by a dependency only hears about the dependency's settings. No package can read or change the settings of another one. A dependency declares its settings the way Declare settings describes, in its own index.js.
On the bot's page
Under Package settings, on the bot's Settings tab, there's one card per package of the bot's script that declares at least one setting or action: the bot's own package and each of its dependencies, down to the dependencies of dependencies. A card's title gives the package's name, its version and a count such as "2 setting(s) · 1 action(s)".
A card only appears for a package whose code the script loads. A dependency listed in the manifest that no file imports declares nothing, so it has no card. A package that declares no setting and no action has no card either. The first card is open and the others are closed. The cards don't come in a fixed order, so the bot's own package isn't necessarily the first one. Apply and Reset work card by card: applying one card leaves the others untouched. A dependency's actions are in its own card, and run the dependency's own run functions. The Add a bot dialog shows the same cards, without Apply and without actions, as soon as a library package is picked.
Saved under the dependency's name
A bot saves a dependency's settings under the dependency's name, the same way it saves yours. So, on one bot:
Every package that uses a given dependency reads the same values for it. When the bot moves from one of these packages to another, the dependency's settings come along. The values carry over between versions of the dependency, under the rules in Read settings. Another bot has values of its own. Write a package for others to use
A package meant to be a dependency is an ordinary package. What sets it apart is what its index.js exports: functions and values for the packages that import it, and usually no default export.
Here is a small one, chat-tools, with one setting and one function:
import { values } from "asterobot:parameters"; /** @type {BehaviorParameters} */ export const parameters = { showChannel: { type: "bool", label: "Show the channel", default: true, }, }; export function formatChat(payload) { const line = `${payload.senderName}: ${payload.content}`; // Read at each call, so an applied change counts from the next line. return values.showChannel ? `[${payload.channel}] ${line}` : line; } A package named chat-logger uses it. Its manifest declares the dependency, as Dependencies shows:
{ "dependencies": { "chat-tools": { "version": "1.0.0" } } } And its index.js imports formatChat() by the name it declared:
import { session, botInfo } from "asterobot:bot"; import { on, send } from "asterobot:protocol"; import { formatChat } from "chat-tools"; export default async function behavior(launch) { on("ChatChannelMessageEvent", (traffic) => { // A message Asterobot couldn't decode has no payload, and an error // thrown in a handler stops the whole script. if (!traffic.payload) return; botInfo(formatChat(traffic.payload)); }); // A new game connection starts with this message. MITM bots never launch // with "initial": the Dofus client has already identified their session. if (launch.reason === "initial") { await send("IdentificationRequest", { ticketKey: session.gameToken, languageCode: session.language, }); } } A bot set to chat-logger shows a single card, chat-tools, with Show the channel: chat-logger declares nothing itself. Turning the switch off and clicking Apply changes the next chat line the bot's console shows.
Some advice for packages like chat-tools:
Decide who chooses each value. What the person running the bot decides belongs in your settings. What the package using yours decides belongs in the arguments of the functions you export, since that package can't change your settings. Label your settings clearly. They appear under your package's name on every bot whose package uses yours, and the person reading them may never have heard of your package. Several packages on one bot share your settings. Don't design a setting whose right value depends on which of them uses it. Leave out the default export if the package isn't meant to run on its own. It's still listed in the library and can still be picked for a bot, but its start then fails with an error containing has no unambiguous default export. Say what the package is for in its description when you publish it. Keep the top level of index.js to imports, declarations and function definitions. It runs at every start of every package that uses yours, and each time Asterobot reads their settings. Next, Steps and conditions shows how a package gives its own functions to the Studio.
  • 61 vues
What asterobot.json holds, which fields you write and which ones Asterobot adds, how to edit them on the Manifest card, and why Asterobot refuses a manifest.
asterobot.json is the one file of a package that isn't code. It says which other packages the package depends on, which Asterobot versions it works with and what it declares it uses.
Where it lives
At the root of the package, next to index.js. It's optional. A new package has none, and Asterobot only writes one when there's something to put in it. The editor doesn't show it. The Files list only holds modules, and saving code never changes the manifest. It doesn't hold the package's name or version. Those come from where the package is installed: the Name and Version typed when the package was created, copied or imported, or the file it came from on asterobot.net. See Names and versions. In Asteroboard, you edit the manifest on the package's own page, in its Manifest card. A manifest can also come with the package: inside a .zip you import, or in a folder you install with library install, as Zip files and the command line explains.
An example
This is the manifest of a package installed from the marketplace, as Asterobot writes it:
{ "dependencies": { "@alice:chat-tools": { "url": "https://asterobot.net/files/file/12-chat-tools/", "version": "1.2.0" } }, "compatibleVersion": ">=1.5.0 <2.0.0", "integrity": "sha256-9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "permissions": [ "gamedata" ], "sourceId": 34 } The author wrote dependencies, compatibleVersion and permissions. Asterobot added integrity and sourceId when it installed the package from asterobot.net.
Fields
Key Type Written by What it holds dependencies Object You The library packages this package imports. Each key is the name the code imports, and each value is an object with a version and a url, both optional. See Dependencies. compatibleVersion String You The range of Asterobot versions the package works with, such as >=1.5.0 <2.0.0. See Compatibility. permissions Array of strings You What the package says it uses. Asterobot doesn't enforce it yet. See Permissions. sourceId Number Asterobot The ID of the asterobot.net file the package was installed from. Check for updates compares the package with that file, and it's how a marketplace package keeps its name when its file is renamed. integrity String Asterobot A checksum of the package's .js and .mjs files and its fight profiles, starting with sha256-, recorded when the package is installed from the marketplace. Asterobot compares the files with it each time it loads the package, and refuses them with package source integrity mismatch when they changed. And inside each entry of dependencies:
Key What it holds version An exact version such as 1.2.0, a range such as ^1.2.0, or latest. url The address of the dependency's file on asterobot.net, or its ID. Without it, the dependency has to be in the library already. Asterobot keeps only these keys. Anything else in the file, such as a name or a description, is dropped as soon as the package is installed, because Asterobot writes the file again from what it understood. The file has to be valid JSON: a package whose asterobot.json doesn't parse can't be imported or installed, and if the file breaks after installation, the package disappears from Library Manager and doesn't start.
What Asterobot does with sourceId and integrity
When sourceId integrity Installing or updating from the marketplace Set to the file's ID Computed from the files just downloaded Import a .zip Removed Removed Copy to a new version Removed Removed Save in the editor Kept Removed, since the code changed Save manifest Kept Kept The library install command Kept as they are in the folder's file Kept as they are in the folder's file So whatever you write in these two keys is replaced or removed, except by library install. Leave them out of a package you're working on, which matters most when you start from a package you exported: a leftover integrity stops your package from starting as soon as you change a file, and a leftover sourceId ties it to a file on asterobot.net it didn't come from.
The Manifest card
To open a package's page, click its name in Library Manager, or click the box icon before the name at the top of the editor. The Manifest card is under the details card.
For a local package, the card is a form:
Part What it does Dependencies One row per dependency, with three boxes and a remove button. Empty, the boxes read Package name, as imported, Version and asterobot.net file URL (optional). Without any row, the card says "No dependencies." Add an installed package Lists every other package in your library as name@version. Picking one adds a row with its name and version, and no URL. A row that already had that name is replaced. Add by URL Adds an empty row for you to fill in. Permissions One badge per permission, each with a remove button, or "No permissions declared.". Type a new one in the box that reads e.g. gamedata, then click Add or press Enter. Compatible with A single box for the range of Asterobot versions, reading e.g. >=1.5.0 <2.0.0 while empty. Save manifest and Reset Both stay disabled until something changes. Reset puts the card back as the manifest was last saved. When you click Save manifest, spaces around each value are removed, a dependency row with an empty name is left out, and an empty or repeated permission isn't added. What you save replaces the dependencies, permissions and range the manifest had. A toast says Manifest saved. A script that's already running keeps what it loaded, and the next start uses the new manifest.
For a package with any other name, the card is read-only and says "Published by someone else, so this is read-only". It lists each dependency with its version, or latest when it has none, then the permissions and the range, or "No constraint declared." when there's no range.
When Asterobot refuses a manifest
When a save fails, the card shows Couldn't save the manifest with the reason under it.
Before saving, Asterobot checks each dependency that has no URL: it needs an exact version, and that version has to be in your library. Then it checks the versions of every dependency.
Reason What to do dependency "<name>" needs <name>@<version> installed, or a URL to fetch it from Install that version of the package, or correct the name or the version dependency "<name>" needs an exact version Fill in Version invalid package manifest: dependency "<name>": invalid version "<version>": <details> Write an exact version, a range or latest Error messages lists every reason, including those about a version written in the dependency's name.
Two fields aren't checked when you save. A compatibleVersion that isn't a valid range is accepted, and only fails when the package loads: see Compatibility. An empty or repeated permission, which the card doesn't let you add but a file can contain, stops the package from starting with empty package permission or duplicate package permission "<permission>".
A manifest inside a .zip or a folder goes through the same version checks when the package is installed. The Import a .zip dialog then only says Couldn't import the package, while library install prints the reason.
Next, Names and versions.
  • 77 vues
The rules for package names and version numbers, local and marketplace names, why semantic versions matter, and how several versions of a package live side by side.
A package is identified by its name and its version together: my-bot 1.0.0 and my-bot 1.0.1 are two packages in your library. The way you number versions matters once other people use your package.
Where a name and a version come from
Neither is written in the package's files. A package gets them from how it was installed:
How Name and version New package, Import a .zip, Copy to a new version The Name and Version typed in the dialog The library install command The --name and --version flags Installing from the marketplace Built from the file on asterobot.net. See How your package is named. Asterobot stores each version in its own folder of the library, named after the package and the version, which is why the two follow the rules below. You can't rename a package or change its version in place. Copy it to a new name or version, or export it and import it again.
Local names
A local name is a plain name, such as my-bot. Only local packages can be edited in Asteroboard.
It can't contain @ or :, since both mean something in marketplace names and in imports. It can't be empty, . or .., and can't contain / or \. Everything else is allowed, dots included, even at the start as in .dotted. Lowercase words joined by hyphens, such as chat-logger, read best in lists and in import statements. Marketplace names
A marketplace name looks like @publisher:name, such as @alice:chat-tools. Asterobot gives one to every package installed from the marketplace.
The name starts with @, then a publisher, a : and a name, and contains no other @ or :. A publisher of asterobot makes an Official package. Any other publisher makes a Community package. The Import a .zip dialog and library install also accept a name of this shape. The package then shows Community or Official without coming from the marketplace, and it can't be edited or copied in Asteroboard. Name Accepted Why my-bot Yes A local name .dotted Yes A local name @alice:chat-tools Yes A community name @asterobot:core Yes An official name my@bot No Reads as the package my at version bot local:bot No A : introduces a name after a publisher, and there's no @ @alice, @alice:, @:chat-tools No The publisher or the name is missing @alice:chat:tools, @alice:chat@tools No A second : or @ The dialogs refuse a wrong name as you type:
Message Where A local package name can't contain ':' or '@'. New package, Import a .zip, Copy to a new version A new package is local, so its name can't start with '@' - those are assigned by the marketplace. New package and Copy to a new version, which only create local packages A marketplace name looks like @publisher:name. Import a .zip A marketplace name has exactly one ':' and one leading '@'. Import a .zip The command line gives Asterobot's own wording, such as local package name "my@bot" must not contain ":" or "@", marketplace package name "@alice" must be "@publisher:name" or marketplace package name "@alice:chat:tools" must contain exactly one ":" and one leading "@".
Version numbers
A version is required, and Asterobot accepts almost any text: it can't be . or .., and can't contain / or \. New packages start at 1.0.0. A version breaking these rules is refused with invalid version "<version>": contains a reserved character, or empty or reserved path component. Copy to a new version shows that reason, while New package and Import a .zip only show a toast.
Use semantic versions
Write versions as three numbers separated by dots, such as 1.4.2: a semantic version. Asterobot accepts other text, but several features only understand semantic versions:
Feature With semantic versions With other text Check for updates Offers a version only when it's higher than the installed one Offers any different version, even an older one Version ranges such as ^1.2.0 or latest, in imports and in dependencies Consider this version Skip this version, as if it weren't installed Declaring the package as a dependency Works Save manifest refuses the version Copy to a new version Suggests the next version: 1.4.2 becomes 1.4.3 Raises the last number in the text, or suggests the same version when there's no number The usual meaning of the three numbers helps the people who use your package, especially other authors who depend on it with a range such as ^1.2.0, which accepts any 1.x.y from 1.2.0 on:
Raise the last number for a fix that changes nothing else: 1.4.2 to 1.4.3. Raise the middle number when you add something without breaking what exists, and reset the last one: 1.4.3 to 1.5.0. Raise the first number when something that worked before stops working: a setting renamed, an exported function removed or changed. Reset the other two: 1.5.0 to 2.0.0. Some details of how Asterobot reads semantic versions:
1, 1.4 and v1.4.2 are understood too, but they're different text from 1.4.2. An exact version is always compared as text, so a dependency locked to 1.4 doesn't match an installed 1.4.0. Write three numbers, without v, everywhere. Numbers can't have leading zeros: 1.04.0 isn't a semantic version. A suffix after a hyphen, as in 2.0.0-rc.1, makes a version that comes before 2.0.0. Check for updates offers 2.0.0 to someone on 2.0.0-rc.1, never the reverse, and ranges only match such versions when the range itself has a suffix. A suffix after +, as in 1.4.2+build.7, doesn't change the order. Versions side by side
Each version of a package is installed on its own, and stays until someone removes it. A bot is set to one exact version, so installing a new version never changes what a bot plays. Update packages describes this from the player's side.
For you as the author:
To start a new version, use Copy to a new version on the current one, as Share your package shows. The old version stays as it was. Give every change you share a new version. Two packages with the same name and version can't be told apart, and the second one installed replaces the first. Settings are saved under the package's name, so a bot moved to a new version keeps its values, and a package copied under a new name starts from its defaults on every bot. See Read settings. A script can load several versions of the same package at once, when its dependencies lock different versions. See Dependencies. Next, Dependencies.
  • 60 vues
Use another package's code from yours: declare the dependency, choose its version, make it installable for others, import it, and fix what Asterobot reports.
A package can use the code of other packages in the library, its dependencies. That's how you share helpers between your own packages, or build on a package someone published.
How dependencies work
A package declares its dependencies in the dependencies field of its manifest. The key of each dependency is the exact name your code imports. An import gives you what the dependency's index.js exports, and nothing else. Each dependency is locked to one exact version, and that version has to be in the library when the script starts. Starting a script never downloads anything. @asterobot:base is the exception: your package always loads the newest version of base in the library that the running Asterobot can run, whatever version it locked, and Asterobot keeps that version up to date by itself. See Base is always the newest. A dependency's code runs in the same script as your package, on the same bot. Its settings and actions show on the bot's page next to yours: see Settings in dependencies. The modules of your dependencies count toward the module limits of your script. See Limits. Depend on a package from your library
Make sure the package you need is in your library, at the version you want: install it from the marketplace, as Install from the marketplace shows, or create it yourself. Open your own package's page: click its name in Library Manager. In the Manifest card, open Add an installed package and pick the package, such as chat-tools@1.0.0. A row appears with its name and version. Click Save manifest. A toast says Manifest saved. Your manifest now holds:
{ "dependencies": { "chat-tools": { "version": "1.0.0" } } } And your code can import it by that name:
import { formatChat } from "chat-tools"; Settings in dependencies has a complete example of both packages. When you save, Asterobot checks that a dependency without a URL is installed at that exact version, and refuses the manifest otherwise.
Make it installable for others
When someone installs your package from the marketplace, Asterobot also installs its dependencies, but only those with a URL. A dependency without a URL has to be in their library already, at exactly that version, or the installation fails. So every dependency of a package you publish needs a URL, which means it has to be published too.
If the dependency is one of your own packages, publish it first: see Publish on asterobot.net. Install it from the marketplace on your own Asterobot. It gets a marketplace name, shown as Will install as before you install, such as @alice:chat-tools@1.2.0. In your package's Manifest card, add it with Add an installed package, then paste the address of its file page in the third box of that row, the one that reads asterobot.net file URL (optional). If a row with its local name remains, remove it. Change your imports to the marketplace name: import { formatChat } from "@alice:chat-tools";. Click Save manifest, then start your package on a bot to check that it still works. About the URL:
It's the address of the file's page on asterobot.net, such as https://asterobot.net/files/file/12-chat-tools/. The file's ID alone, 12, works too. The name of the dependency has to match the file. The part before the : must be the publisher Asterobot derives from the file: its author's name, or asterobot for an official file. When they don't match, the installation of your package fails. So a dependency with a local name, such as chat-tools, can't be fetched from a URL. Asterobot only uses the URL when your package is installed or updated from the marketplace. Starting your package, importing it from a .zip or installing it with library install never downloads its dependencies. Choose the version
Version On your Asterobot When someone installs your package from the marketplace An exact version, such as 1.2.0 Loads that version, which has to be installed Uses that version if they have it. Otherwise Asterobot downloads it, from the file's current version or from the file's version history on asterobot.net. A range, such as ^1.2.0, ~1.2.0 or >=1.2.0 <2.0.0 Doesn't start Uses the highest version of the file that matches, among its current version and its history, installing it if they don't have it latest, or nothing, for a dependency with a URL Doesn't start Uses the file's current version, installing it if they don't have it Once Asterobot has chosen a version during an installation, it writes that exact version in the installed copy's manifest. The package keeps using it from then on, even when the dependency publishes a newer version, except base: see Base is always the newest. A dependency without a URL always needs an exact version.
A range or latest in your own package's manifest stops it from starting on your Asterobot, because Asterobot only loads exact versions. The start fails with a reason starting with resolve package <name>@<version>: dependency "<name>" locked to version "^1.2.0": read package, which Package settings on the bot's Settings tab shows. So use exact versions. Your package then runs for you exactly as it will for the people who install it, and you choose when to move to a newer dependency, by publishing an update of your package.
Base is always the newest
Asterobot installs @asterobot:base by itself and keeps it up to date: at startup, every hour, after the person signs in to asterobot.net and after base is removed, it downloads base's current version when the library doesn't have it. So every package plays the same base, its newest:
A dependency named @asterobot:base loads the newest version of base in the library whose compatibleVersion accepts the running Asterobot, whatever version the manifest locks. The locked version only counts when no version of base can run, and the start then fails with its reason. A bot set to play @asterobot:base itself plays that same version, whatever version it was set to, and its settings are that version's. The Studio's palette shows that version's blocks too. Installing a package from the marketplace, or saving a manifest, accepts whatever version of base the library has: an older locked version is never downloaded, and is never required to be installed. A dependency whose name carries its own version, such as @asterobot:base@0.0.3, loads exactly that version, as described below. Still declare base in your dependencies, with an exact version and base's URL, https://asterobot.net/files/file/5-base/: that's what makes your code import it. A script loads its packages when it starts, so a running script keeps the base it started with until it's stopped and played again. After each check, Asterobot deletes the versions of base older than the one that runs, except a version a package imports by its exact version (see below). A newer version this Asterobot can't run stays, for the update that will run it. Base keeps the exports it has, so a package written against an older version runs on the newer one.
A version in the name
A dependency's name can end with @ and an exact version, such as chat-tools@1.0.0. That's how one package uses two versions of the same package at once:
{ "dependencies": { "chat-tools@1.0.0": { "version": "1.0.0" }, "chat-tools@2.0.0": { "version": "2.0.0" } } } import { formatChat as formatChatV1 } from "chat-tools@1.0.0"; import { formatChat } from "chat-tools@2.0.0"; Write the same version in Version as in the name. Asterobot loads the version written in Version: a dependency whose version is only in its name doesn't start. A version in the name that differs from Version is refused when you save, and so is one that isn't an exact version.
Both versions read the same settings, since settings are saved under the package's name. An import can also pick a version with a range, such as chat-tools@^1.0.0, among the versions the script already loads: see Modules and imports.
Dependencies of dependencies
A dependency declares its own dependencies in its own manifest. They're loaded the same way, and installed with it from the marketplace. A package needed by several packages at the same version is loaded once, and its top-level code runs once. Different versions of the same package can be loaded side by side, each locked by the package that declared it. Every package loaded is checked: an incompatible compatibility range or a checksum that no longer matches, in any dependency, stops the whole script from starting. When something's wrong
What you see Cause Fix dependency "<name>" needs <name>@<version> installed, or a URL to fetch it from, on the Manifest card That version of the dependency isn't in your library Install it, or correct the version dependency "<name>" needs an exact version, on the Manifest card A dependency without a URL has no version Fill in Version A reason containing read package <name>@<version> sources, on Package settings The locked version isn't in the library: it was removed, or the version is a range or latest Install that version, or lock an installed version A reason containing has no locked dependency for "<name>" An import uses a name that isn't a key of dependencies, often a typo or an old name Make the import and the key identical A reason containing dependency "<name>" targets package named The version in the dependency's name differs from its Version Write the same version in both Library Manager lets you remove a package other packages depend on, after a warning listing them. Those packages then fail to start until the version they lock is installed again. Error messages lists the other texts.
Reuse a connection package
Every package that runs on a Full socket bot has to identify the bot on the game server, as Identify and run shows. You can already put that part in a package of your own and depend on it from your other packages.
Note
Coming soon. Ready-made basic and full connection scripts will be published for you to run and reuse, so your package can depend on one instead of doing the identification itself. Until then, the identification stays in code you write: in your package, or in a package of yours that it depends on.
Next, Compatibility.
  • 61 vues
Declare which Asterobot versions your package works with in compatibleVersion, write the range correctly, and know where incompatibility shows and what it blocks.
compatibleVersion says which versions of Asterobot your package works with. When someone runs an Asterobot outside that range, the package is marked Incompatible and doesn't start. It's about Asterobot's version, not the game's: a Dofus update changes nothing here. After a game update covers what a game update changes for scripts.
Declare it
On your package's page, type the range in Compatible with, in the Manifest card, then click Save manifest. In the file, it's the compatibleVersion key:
{ "compatibleVersion": ">=1.5.0 <2.0.0" } Without a range, the package works with every version of Asterobot and nothing is checked. To find the version you run, see Updating, or run asterobot --version.
Write the range
A range is one or more conditions on the version of Asterobot:
Range Matches >=1.5.0 1.5.0 and every later version >=1.5.0 <2.0.0 From 1.5.0 up to 2.0.0, without 2.0.0. A space or a comma between two conditions means both must hold. ^1.5.0 The same as >=1.5.0 <2.0.0: this version and the later ones with the same first number ^0.4.0 From 0.4.0 up to 0.5.0, without 0.5.0. When the first number is 0, the second one has to stay the same. ~1.5.0 From 1.5.0 up to 1.6.0, without 1.6.0 1.5.x Every 1.5 version 1.5.0 - 1.8.0 From 1.5.0 to 1.8.0, both included. The hyphen needs a space on each side. <1.6.0 || >=1.7.0 Either condition: here, every version except the 1.6 ones !=1.6.2 Every version except 1.6.2 1.5.0 Only 1.5.0 A version with a suffix after a hyphen, such as 1.6.0-rc.1, only matches a range that has such a suffix itself, as in >=1.6.0-0. Without one, the range leaves those versions out, even when their numbers are inside it.
Where it shows
Where What you see Library Manager, Compatibility column Compatible or Incompatible, only for a package that declares a range The package's page The same badge next to the provenance badge, and a Compatible with row with the range The top bar of the editor Incompatible, when the Asterobot you run isn't in the range Package pickers, in Add a bot, Load a behavior selection and the bot's Settings tab (incompatible) after the package's name and version. The package can still be picked. The bot's header The package's name in orange, and incompatible versions in orange in its version menu A script packages.info(name).compatibleVersion, described in asterobot:runtime What it blocks
Asterobot checks the range each time it loads the package: at every start, whether from Play, Run or Load a package and play it, and each time it reads the package's settings. When the Asterobot you run isn't in the range:
The script doesn't start, and the toast gives no reason. Package settings, on the bot's Settings tab, and the Add a bot dialog show Couldn't read this package's settings with the reason: resolve package my-bot@1.0.0: package "my-bot@1.0.0": package "my-bot" requires asterobot >=1.5.0 <2.0.0, running 1.4.2 A package that depends on an incompatible package doesn't start either, even when its own range is fine. The reason then names the dependency. Nothing else is blocked. Installing from the marketplace, importing, copying, editing and saving all work on an incompatible package, and it stays in the library. After Asterobot is updated and started again, the badges and the checks follow the new version.
Not checked when you save
Asterobot doesn't check that a range is written correctly when you click Save manifest, nor when it installs a package. A range it can't read is saved as it is, and then:
The package shows Incompatible everywhere, whatever version you run. Every start fails, and Package settings shows a reason containing has an invalid compatibleVersion constraint, followed by the range and what's wrong with it. So after saving a range, look at the badge at the top of the package's page. If it says Incompatible while the Asterobot you run should match, read the range again: >=1.5.0 and <2.0.0 or 1.5.0+, for example, aren't ranges Asterobot can read.
When to declare a range
Declare a lower bound when your package needs something a given Asterobot version brought: a function, a message name, a fix. Use the first version that has it. Add an upper bound only when you know a later version breaks your package. A bound that's too tight marks your package Incompatible for people whose Asterobot would have run it fine. A new range reaches the people who installed your package only with a new version of it: see Publish an update. Next, Permissions.
  • 71 vues
List what your package uses in its manifest, see where the list shows and how a script reads it, and know that Asterobot doesn't enforce it yet.
A package can list permissions in its manifest: short names for what it uses. For now the list is information for the people who install your package, and Asterobot doesn't limit anything based on it.
Note
Coming soon. Asterobot will enforce the permissions a package declares. Until then, any package can use every function of every built-in module, whatever it lists.
Declare them
Open your package's page and find the Manifest card. Under Permissions, type a name in the box that reads e.g. gamedata, then click Add or press Enter. The name appears as a badge. Add the other permissions the same way. The button on a badge removes it. Click Save manifest. In the file, the list is the permissions key:
{ "permissions": [ "gamedata" ] } The rules:
Each permission is a string. Asterobot doesn't define permission names yet, so it accepts any text and shows it as you wrote it. The card leaves out empty names and names already in the list. In a file, an empty entry or a name listed twice stops the package from starting, with empty package permission or duplicate package permission "<permission>". The list comes along when the package is copied, exported or imported. Where they show
On the package's page, the Manifest card shows each permission as a badge, or "No permissions declared." when there's none. For a package installed from the marketplace, the card is read-only, so the people who install yours read your list there.
A script reads the list with packages.info(), from asterobot:runtime, for its own package and for each of its dependencies. This one writes its own list to the bot's console when it starts, assuming the package is named my-bot:
import { session, botInfo } from "asterobot:bot"; import { send } from "asterobot:protocol"; import { packages } from "asterobot:runtime"; export default async function behavior(launch) { const self = packages.info("my-bot"); botInfo(`${self.name} ${self.version} declares: ${self.permissions.join(", ") || "no permission"}`); // 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, }); } } permissions is always an array, empty when the package lists nothing. asterobot:runtime describes the other properties, and the errors packages.info() throws for a name the script doesn't load.
What to list
Since nothing checks the list today, it's worth exactly as much as it's honest. Name what your package really does, with words people understand, such as gamedata for a package that reads game data, the example the card itself gives. The names Asterobot will check aren't defined yet: expect to update your list, in a new version of your package, once they are.
Whatever the list says, people deciding whether to run your package should know what any package can do with their bot. Trusting a package explains it from their side.
Next, The editor.
  • 66 vues
Every part of the package editor, with its files and plan files, saving and unsaved changes, Run, the Game panel, completion and error checking, and why marketplace packages can't be edited.
The editor is where you write a package's code, in Asteroboard. Create a package gave a first tour of it. Some of its parts aren't obvious: saving replaces the package's files, Run does four things in one click, and a clean error badge doesn't mean the script will run.
Open it
In Library Manager, open a package's menu, or right-click its row, and choose Edit code. On a package's page, click Edit code at the top. New package and Copy to a new version open the editor on the package they create. Edit code is only offered for local packages. When the editor can't load a package's files, for example because the package was removed in the meantime, it shows Couldn't load this package's code instead of the files and the code.
The top bar
Item What it does Box icon Opens the package's page, where its manifest is edited. Name and version The package open in the editor. Unsaved Shown while the files differ from what was last saved. Incompatible Shown when the package's range of Asterobot versions doesn't include the one you run. See Compatibility. Error count A badge such as 2 error(s), shown when the editor finds errors. Resting the pointer on it says "Across every file in this package, not only the open one." IntelliSense Grey while the editor loads its typings, green once it completes and checks the asterobot: modules and the game's messages, orange when the typings couldn't be loaded. Resting the pointer on it says how many game messages it knows, or Couldn't load the typings - editing works, completion doesn't. Bot picker The bot that Run starts the package on. It lists the bots connected to a game server, and only appears when there's at least one. Game Opens or closes the Game panel next to the code. Save Saves every file. Disabled while nothing has changed. Run Saves, then starts the package on the bot in the picker. 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 files: index.js first, then the others in alphabetical order, each with its folders in its path, such as lib/format.js. A package's fight profiles, the .json files of its fightprofiles folder, are listed too, and open as JSON.
Click a file to show it. Each file keeps its own cursor, scroll position, selection, folded blocks and undo history while you move between files. A dot after a file's name marks unsaved changes in it, and a number next to it counts its errors. The plus button at the top of the column opens Add a file. Type a Path, such as lib/helper.js, then click Add a file or press Enter. The dialog refuses a path that is neither a module ending in .js or .mjs nor a profile such as fightprofiles/careful.json or chatprofiles/wheat-ads.json, one that starts with / or contains .. or \, and one already used. It shows A file must be a .js or .mjs module, or a fight profile in fightprofiles/ or a chat profile in chatprofiles/, as a .json file. for the first. The new file is empty, opens right away, and only exists in the package once you save. Package layout gives every path rule, including one the dialog doesn't check. Resting the pointer on a file shows a trash icon. It removes the file from the list at once, without asking, and saving then deletes it from the package. index.js has no trash icon. The other files see a new file as soon as you add it, before you save: an import of it completes and isn't marked as an error. Jumping to a function's definition, or renaming it, works across the files of the package the same way.
Plan files
A plan is a file of the package made in Asteroboard's Studio: places on the world map, the order the bot goes round them, and what it does at each. What makes a file a plan is its first line, wherever the file is:
// asterobot-studio-plan v1 The Studio writes the whole file. It holds the plan as JSON between /* studio:plan */ and /* studio:end */, and generates everything around it: the imports of the packages and plans it uses, and its exports. The second line says so: a change made by hand is lost the next time the Studio saves the plan. It opens in the Studio. A file whose first line is the marker gets a plan icon in Files, and opens in the Studio instead of the code. View as code shows the file itself. It needs @asterobot:base. Base's run() plays it, so creating a plan adds base to the package's dependencies. A script plays a plan by importing its default export and passing it to run(). This is the index.js Start with the Studio creates:
import { run } from "@asterobot:base"; import main from "./plans/main.js"; export { parameters } from "./plans/main.js"; export default async function behavior() { void run(main); } run() resolves when the plan ends and rejects when it fails, and stop() stops it cleanly. Base's README describes how a plan plays, and the plan format.
A plan can declare settings of its own, which its file exports as parameters. They're the package's settings, on the bot's Settings tab, only once index.js re-exports them, as the line above does. A package has a single parameters export, so the settings of several plans are merged into one.
The Studio's Add it button writes this for every plan, as long as index.js only gathers plans' settings. When index.js has no settings yet, it adds the line above, after the imports. For a second plan, it turns that line into the merge of both:
import { parameters as mainSettings } from "./plans/main.js"; import { parameters as bankSettings } from "./plans/bank.js"; export const parameters = { ...mainSettings, ...bankSettings }; A third plan adds its own import and its own spread to that merge. Each name comes from the plan's file name, so plans/farm-run.js gives farmRunSettings, with a number added when the name is already used in index.js.
When index.js declares settings of its own, the Studio leaves them alone: instead of Add it, it shows the lines to write, and you merge them by hand the same way:
import { parameters as mainSettings } from "./plans/main.js"; export const parameters = { ...mainSettings, maxFights: { type: "int", default: 20 } }; A plan whose settings aren't re-exported uses their defaults, and the bot's Console says so.
Saving
Click Save, or press Ctrl+S (Cmd+S on a Mac).
Saving replaces the package's files with exactly the files the editor holds. A file you removed from the list is deleted, and so is any file that isn't in the list, such as one added to the package from somewhere else while the editor was open. The manifest isn't touched.
What else to know about saving:
Unsaved also goes away when you undo your changes back to what was last saved. Removing a file counts as a change, even when every other file is unchanged. Nothing asks you to save when you leave the editor: unsaved changes are lost. The editor reads the files when it opens. If the package changes elsewhere in the meantime, in another tab or from the command line, the editor doesn't show it, and saving from the editor overwrites that change. With the same package open in two tabs, the last save wins. A script that's already running keeps the code it started with. The next start uses what you saved: see Play, stop and reload. Saving doesn't check the declarations of settings and actions. Package settings, on a bot set to the package, does: see Declare settings. When saving fails, a toast says Couldn't save, without a reason. The usual causes are a file path Asterobot refuses, such as ./lib/helper.js, a package that was removed in the meantime, or a package that isn't local.
Run
Run does in one click what you'd otherwise do in four steps:
It saves the package. If saving fails, it stops there, with Couldn't save. It stops the script the picked bot is running, if there's one. It sets the bot's package to this package, at this version. It starts the script. A toast then says Running on followed by the bot's name, or Couldn't run the package when a step failed. The bot keeps this package afterwards, so Play on its page starts it again. Identify and run shows where the output goes and what to check when the script doesn't start.
The Game panel
Game opens a panel next to the code, with two tabs: Atlas, the map viewer, and Datacenter, the game data browser. Drag the line between the code and the panel to share the width differently. Closing the panel only hides it: when you open it again, each tab is where you left it.
The panel shows one game version:
The game version Asterobot is tested against, when it's downloaded and its game data extracted. A badge shows the version number, and resting the pointer on it explains that Atlas and Datacenter answer from that version, the one bots meet. Otherwise, the most recent game version with extracted game data. The badge then reads Incompatible version, and resting the pointer on it explains the difference: fine for writing a script, since most IDs carry over between neighbouring versions, but not the version Asterobot connects bots with, and map IDs, table columns and game texts can differ. When no game version has extracted game data, the panel says No extracted game version. Extract one, as Extract game data shows, then open the panel again. Names and texts in the panel use the language picked for game data on the game's page. Map viewer and Game data describe both tools.
Completion and error checking
The editor loads its typings from the Asterobot it's connected to: the asterobot: modules, and a type for each game message that version of Asterobot knows. With them, it completes functions, message names and payload fields, and checks your code the way TypeScript checks JavaScript. JavaScript support describes that check, the warnings for the three constructs scripts don't support, and the JSDoc types you can use.
The error badge and the numbers in Files count errors only, in every file, open or not. Warnings aren't counted. Neither errors nor warnings stop you from saving or running.
Two mistakes are worth looking for in the editor before anywhere else: a syntax error, and an import of a file that doesn't exist. When a script can't even load because of one of them, Run only says Couldn't run the package, while the editor shows the line, and Package settings, on the bot's Settings tab, gives the full reason. On the other hand, code with no error in the editor can still fail when it runs: the editor knows the shape of messages, not what the game will send.
Marketplace packages are read-only
A package installed from the marketplace, or any package with a @publisher:name name, can't be edited. Library Manager doesn't offer Edit code or Copy to a new version for it, Asterobot refuses to save its files, so Couldn't save appears if you reach the editor for it anyway, and its Manifest card is read-only. The package stays exactly as it was installed, which the integrity check described in The manifest relies on.
When you need another author's code in your package, depend on their package, as Dependencies explains. When you really need to change it, export it and import the .zip under a local name: the imported package is yours to edit. It has no link to the marketplace any more, so no updates and no Source row, and bots save its settings under its new name. See Zip files and the command line.
Next, Zip files and the command line.
  • 69 vues
How a package archive is laid out, its size limit, Export and Import in detail, the library install, library list and library remove commands, and working on a package with your own code editor.
A package leaves Asterobot and comes back as a .zip file: that's what Export produces, what Import a .zip reads, and what you upload to publish on asterobot.net. The command line installs packages from a folder instead, which lets you write them in the code editor you already know.
What goes in a .zip
The simplest archive, and the one Export produces, has the package's files at its root:
index.js asterobot.json lib/format.js The rules:
index.js has to be at the root of the archive. Only .js and .mjs files are read, in any subfolder, following the paths described in Package layout, and the profiles, the .json files directly in fightprofiles/ and chatprofiles/. Other files, such as a README or images, are left out and not kept. asterobot.json is optional, next to index.js. Its sourceId and integrity are removed at import, as The manifest explains. An archive can't be larger than 32 MiB, whether it's imported or installed from the marketplace. The archive doesn't carry the package's name and version. They come from the import dialog, or from the file on asterobot.net. Asterobot also accepts an archive whose files all sit in a single folder, such as my-bot/index.js, which is what compressing a folder produces. That only works when the archive lists nothing but the folder's contents. With a file or a second folder next to it, or with an entry for the folder itself, which some archiving tools add, Asterobot doesn't find index.js and the import fails. To avoid surprises, compress the files of the package rather than the folder that holds them.
Export
In Library Manager, open a package's menu, or right-click its row, and choose Export. The package's page has the same menu. Your browser downloads the archive.
The file is named after the package and its version, with the @ removed and the : turned into -: version 1.2.0 of @alice:chat-tools gives alice-chat-tools-1.2.0.zip, and my-bot 1.0.0 gives my-bot-1.0.0.zip. The archive holds every file of the package at its root: its modules, and asterobot.json when it has one. The manifest is exported as it is. For a package installed from the marketplace, that includes its sourceId and integrity, which an import removes but library install keeps. Every package can be exported, local or not. Import a .zip
Share your package walks through the dialog, and Manage your library lists its messages. The details that matter to an author:
Version fills in by itself when the file's name ends with a hyphen and a version starting with a digit, as in my-bot-1.2.0.zip, and only while Version is empty. A version with its own hyphen defeats it: my-bot-1.2.0-rc.1.zip suggests nothing. Any valid name works, local or @publisher:name. Only a local name gives a package you can edit. An installed package with the same name and version is replaced. The dialog warns you, but, unlike New package, it doesn't disable the button. Importing never installs the package's dependencies. Install them first, or the package won't start. When the import fails, the toast Couldn't import the package gives no reason. Unzip the archive and install the folder with library install: the command prints the reason for most failures. The command line
Three commands of the library group manage the library from a terminal. Command line explains how to run Asterobot's commands on each system.
library install
asterobot library install --path ./my-bot --name my-bot --version 1.0.1 Flag Required What it takes --path Yes The folder holding the package's .js and .mjs files, its asterobot.json if it has one, and its fightprofiles folder if it ships fight profiles --name Yes The package's name: a local name, or @publisher:name --version Yes The package's version What it does:
It copies every .js and .mjs file of the folder and of all its subfolders, whether index.js imports it or not, the .json files directly in fightprofiles/ and chatprofiles/, and asterobot.json from the root of the folder. Other files are ignored. Keep the folder to your package's own files. It replaces the package if that name and version are already installed. It refuses a folder that contains a symbolic link anywhere, with module source "<path>" is a symbolic link. It keeps the sourceId and integrity of the folder's asterobot.json, when the file has them. It doesn't download dependencies. On success, it logs Package my-bot@1.0.1 installed. On failure, the message contains failed to install package: followed by the reason, such as package my-bot@1.0.1 entry "index.js" is missing from its sources.
library list
asterobot library list Prints a table with one row per installed version, sorted by name, then by version: the name, the version, the provenance and the publisher, which is empty for a local package.
NAME VERSION PROVENANCE PUBLISHER @alice:chat-tools 1.2.0 community alice my-bot 1.0.0 local my-bot 1.0.1 local library remove
asterobot library remove --name my-bot --version 1.0.0 Both flags are required. The command removes that version and logs Package removed. Unlike Library Manager, it doesn't warn about the packages that depend on it or the bots set to it. A version that isn't installed fails with a message containing package my-bot@1.0.0: package is not installed.
Asteroboard shows what these commands changed the next time it loads the library, for example after you reload its page.
Work with your own code editor
Only Asteroboard's editor completes the asterobot: modules and the game's messages. If you'd still rather write the code in the editor you're used to, keep the package in a folder of your own and install it with library install after each change:
Create a folder with index.js, your other modules and, if the package needs one, its asterobot.json without sourceId or integrity. To start from a package you already have, export it, unzip it, and remove those two keys from its manifest if they're there. Edit the files. Install the folder, under the same name and version for as long as you work on that version: asterobot library install --path ./my-bot --name my-bot --version 1.0.1 The first time, set a bot to that package and version with Load a new package, reloading Asteroboard's page first if the package isn't listed. Then click Play, and Stop then Play after each new install: every start reads the files installed at that moment. Go back to step 2. While you work this way, don't save that package from Asteroboard's editor: saving replaces the installed files with what the editor holds, and Run saves first. When the version is done, move on to a new --version for the next changes.
You can also edit the files of a local package directly in the library, inside Asterobot's data folder, and the next start reads them. The same caution about Asteroboard's editor applies. Never change the files of a package installed from the marketplace there: its integrity check then refuses it. Files and folders says where the data folder is.
Next, the Publishing chapter starts with what to check before you publish.
  • 83 vues
A checklist to go through before you upload a package to asterobot.net, so that it installs, starts and keeps working for the people who download it.
Once your package is on asterobot.net, people install it on their own Asterobot, with their own bots and their own settings, and a version they installed stays as it is. So go through this list before you upload anything. Each item links to the page that explains it.
It runs on both kinds of bot
The script identifies a Full socket bot on its initial launch, with the lines from Identify and run. Without them, a Full socket bot never gets into the game. You tested it on a MITM bot (man-in-the-middle: the bot relays the game session of a Dofus client someone plays), and on a Full socket bot when what the package does allows it. Every await inside a handler sits in try/catch, and every wait() has a timeout. An error that escapes a handler stops the script: see Errors. The editor's error badge shows no error. The script says so on the bot's console when something it expects doesn't happen, and never writes session.gameToken anywhere. Settings and actions
Package settings, on a bot set to your package, shows your cards and not Couldn't read this package's settings. Every setting has a label, a sensible default, a description when the label isn't enough, and a unit for a number that has one. See Declare settings. The keys of your settings are the ones you'll keep. Renaming a setting in a later version loses the values people saved: see Read settings. Each action works while the script runs, and its key reads well in the toasts that end with finished and failed. Dependencies and manifest
Every dependency is published, has the URL of its file on asterobot.net, its marketplace name and an exact version. See Dependencies. If the package declares a compatibleVersion, its page shows Compatible on your Asterobot. See Compatibility. The permissions list what the package really uses. See Permissions. The manifest has no sourceId and no integrity. A package created, copied or imported in Asteroboard never has them. Version and name
The version is a semantic version, three numbers such as 1.0.0. See Names and versions. You never published different code under this version. See Publish an update. The title you'll give the file, and your asterobot.net name, contain letters from a to z or digits, since the package's name is built from them. See How your package is named. The archive
You exported the version you tested, with Export in Library Manager. See Zip files and the command line. The archive weighs less than 32 MiB. You imported the exported .zip under another local name, such as my-bot-check, and that package starts on a bot. That's the code people will install. Remove the check package afterwards. Next, Publish on asterobot.net.
  • 65 vues
Submit your package as a file in the Library category of asterobot.net, fill in what Asterobot reads from it, check that it installs, and share the link.
The marketplace is the Library category of asterobot.net, and any asterobot.net member can publish a package there. Publishing means submitting a file on the website with your exported .zip attached.
You need an asterobot.net account, as Create an asterobot.net account explains, and the .zip of your package, exported once you've gone through Before you publish.
What Asterobot reads from your file
When someone pastes the link of your file in Asteroboard, Asterobot reads a few things from the file on asterobot.net:
On asterobot.net What Asterobot does with it The file's title Turns it into the second part of the package's name, in lowercase with hyphens: Chat Tools gives chat-tools. Your asterobot.net name, as the file's author Turns it into the first part of the name, the publisher: @alice:chat-tools. The official mark, set by the Asterobot team Replaces the publisher with asterobot, and shows Official instead of Community. The file's version Uses it as the package's version. The file's attachment Installs it as the package. The file must have exactly one attachment: your .zip. Who can download the file Decides who can install the package. Asterobot doesn't read the description, the screenshots or anything else on the file's page, and doesn't check which category the file is in. Those are for the people who look for packages, and they look in the Library category. How your package is named goes into the naming rules.
Submit the file
Sign in on asterobot.net and open the Library category: https://asterobot.net/files/category/2-librairie/. Start submitting a new file in that category. Attach your exported .zip, and nothing else. Give the file its title. Choose it with care: it becomes part of the package's name, and a title changed later gives new users a different name. Set the file's version to your package's version, written the same way, such as 1.0.0. A file without a version can't be installed. Write the description: what the package does, whether it works on Full socket bots, MITM bots or both, its settings and actions, the packages it depends on and the permissions it declares. Submit the file. Anything else you fill in on the website is for the people reading your file's page: it doesn't change how Asterobot installs the package.
Check that it installs
Install your package on your own Asterobot, the way anyone would:
Copy the address of your file's page. In Library Manager, click Download package, paste the address in File ID or URL, then click Preview. Check the card: your title, the Community badge, your name after Author, and after Will install as, the name and version you expect, such as @alice:chat-tools@1.0.0. Click Install. Package installed appears, and the package has its own row in Library Manager. Start it on a bot. Remove it from your library afterwards if you don't need it. If Preview says File not found, or you don't have permission to view it., check that your file is visible on asterobot.net, that your Asterobot is signed in, and that the title and your asterobot.net name each contain at least one letter from a to z or a digit.
When the installation itself fails, Asteroboard only says Couldn't install the package. The causes on your side:
Cause Fix The file has more than one attachment, or none Keep your .zip as the file's only attachment Asterobot doesn't find index.js in the archive Export the package again from Library Manager and attach that archive. See Zip files and the command line. The archive is larger than 32 MiB Leave out what the package doesn't need The file has no version, or its version contains / or \ Set a version such as 1.0.0 The archive's asterobot.json isn't valid JSON, or a dependency's version isn't valid Fix the manifest, export again and upload the new archive as a new version A dependency has no URL, its URL points to a file whose author doesn't match the dependency's name, or the version it locks isn't available See Dependencies When a dependency is what failed, your package itself may already be installed, without that dependency. Remove it before trying again.
Share the link
The address of your file's page, such as https://asterobot.net/files/file/12-chat-tools/, is all people need. They paste it in Download package, or in Download one wherever they choose a package for a bot, as Install from the marketplace shows. The number at the start of the last part of the address, 12 here, is the file's ID, which works on its own too. Asterobot only reads that number, so an address that still has an old title in it keeps working after you rename the file.
Other authors use the same address as the URL of a dependency on your package.
Who can install it
Asterobot downloads the file with the asterobot.net account it's signed in with. When that account isn't allowed to download your file, the preview says You don't have permission to download this file. and Install stays disabled. Who can download a file is decided on asterobot.net, not in Asterobot. The marketplace describes the website's side.
Next, when you change your package, Publish an update.
  • 63 vues
Publish a new version of your package on the same asterobot.net file, understand how people and the packages that depend on yours get it, and why a version number must never be reused.
A package on asterobot.net changes by getting new versions on the same file. The people who installed it see the update the next time they check for updates, and choose when to move their bots to it.
Prepare the new version
In Library Manager, open your package's menu and choose Copy to a new version, with a higher version such as 1.0.1 or 1.1.0. Names and versions says which number to raise. Make your changes in the copy, and test it on your bots. Check what your changes do to the settings people saved: a renamed setting loses its values, and a new default doesn't reach the bots that already saved one. See Read settings. Go through Before you publish again. Export the new version, which gives a file such as my-bot-1.0.1.zip. Upload it to the same file
On your file's page on asterobot.net, upload a new version of the file:
Replace the attachment with the new .zip, so the file still has exactly one attachment. Set the file's version to the new version, written exactly as in your library. Say what changed, especially anything people have to do, such as checking a setting. Keep the title, since a new title gives new users a different package name: see How your package is named. Always update the existing file, never submit a new one for a new version. Asterobot checks for updates against the file a package was installed from, so people who have your package never see a new file as an update. And since a package's name comes from its author and title, a second file with the same title installs under the same name as the first, which only adds confusion.
How people get the update
Nothing changes on anyone's Asterobot by itself. When someone clicks Check for updates in Library Manager, Asterobot compares each package they installed from the marketplace with the current version of its file:
The file's current version is They see Higher than theirs, both being semantic versions Update to followed by the version Lower than theirs Nothing: Asterobot never offers to go back The same as theirs Nothing Different, when either version isn't a semantic version Update to, even for an older version Update to installs the new version next to the old one. Their bots stay on the old version until they move them, one by one, and keep the settings they saved. The old version stays until they remove it. Update packages describes their side. Packages that were installed as dependencies of another package are checked the same way.
Packages that depend on yours
A package that depends on yours locks one exact version of it. Which version it gets depends on what its author declared, at the moment someone installs that package:
The dependency's version The version installed An exact version, such as 1.0.0 That version: your file's current version when it's the same, otherwise the same version from your file's history A range, such as ^1.0.0 The highest matching version, among your file's current version and its history latest Your file's current version Afterwards, the installed package keeps the version it got. Your update doesn't change it, even when the same person installs your new version with Update to. It moves to a newer version of yours when its author publishes an update of their own package, or when someone installs that package again and its declaration picks your newer version.
Keep older versions available
Asterobot fetches an older version of your package from your file's version history on asterobot.net. A package that locks your 1.0.0, while your file's current version is 1.1.0, can only be installed as long as 1.0.0 is still in that history. When it isn't, installing that package fails. So keep your previous versions in the file's history for as long as other packages may lock them.
Never reuse a version number
Once a version is published, never publish different code under the same number, not even to fix a mistake a minute later. Asterobot tells versions apart by their number alone:
People who already installed that version aren't offered the new code, since Check for updates sees the same version. They only get it if they happen to install that version again. People who install it afterwards get the new code under the same number. Two people with 1.0.1 then run different code, and nobody can tell from the version which one they have. The same goes for packages that lock that exact version: installed before, they keep the old code, installed after, they get the new one. Asterobot can't notice it. It records a checksum of a package's files when it installs the package from the marketplace, and refuses the package if those files change on that computer afterwards. That protects what was downloaded, but the checksum comes from the download itself, so it can't tell that the file on asterobot.net changed. Publish the fix as a new version, such as 1.0.2, instead. That one reaches everyone through Check for updates.
When a version goes wrong
If you publish a broken version, don't set the file back to an older version number: the people who installed the broken one wouldn't be offered the older one, since Asterobot never offers to go back. Publish a fixed version with a higher number, and say in the description which version to avoid.
Next, How your package is named.
  • 63 vues
How Asterobot builds the @author:title name of a package installed from asterobot.net, why the name stays after a rename, what happens when the author or the official mark changes, and how official packages are named.
Every package installed from the marketplace gets a name of the form @publisher:name, built from its file on asterobot.net. You don't type that name anywhere, but you choose what it's built from.
The name at first install
The first time someone installs a file, Asterobot builds the package's name from @, the file author's asterobot.net name, :, then the file's title. Both parts go through the same conversion:
Letters are put in lowercase. Every run of characters other than the letters a to z and the digits 0 to 9 becomes a single hyphen, and hyphens at the start or at the end are dropped. Spaces, punctuation and accented letters all count as other characters. Author Title Package name Alice Chat Tools @alice:chat-tools Some_Author Auto Harvest 2! @some-author:auto-harvest-2 Alice Récolte auto @alice:r-colte-auto The version is the file's version on asterobot.net. In Asteroboard, the preview shows the full name and version after Will install as, before anything is installed.
A title with no letter from a to z and no digit leaves nothing to build a name from, and neither does an author name written only with other characters. Such a file can't be previewed or installed. And since people type your package's name in their import statements when they depend on it, a title that converts to a readable name, such as Harvest Helper, serves them better than one full of accents or symbols.
Official packages
A file the Asterobot team marks as official on asterobot.net installs under @asterobot: followed by its converted title, whoever uploaded it. It shows the Official badge, and the preview's Author line says asterobot.
A package imported from a .zip, or installed with library install, takes the name typed for it, which can start with @asterobot: too. It then shows Official without coming from the marketplace. Only a package installed from the marketplace has a Source row on its page: see Trusting a package.
The name sticks
Asterobot remembers which file each marketplace package came from. When someone installs or updates that file again, the package keeps the name it already has on their Asterobot, even if you've changed the title since. Updates keep reaching them.
The name is only built again for someone who didn't have the package yet. So after you rename the file, new users get your package under the new name, while earlier users keep the old one. Bots, saved settings and dependencies all refer to a package by its name, so for Asterobot these are two different packages, and a package that depends on yours by one of the names gets its own copy under that name. The same happens with two files that have the same author and title: they install under the same name. Choose the title before you publish, keep it, and publish every new version on the same file.
When the author or the official mark changes
Each time Asterobot reads the file again, it checks the publisher part of the name: the file's current author, or asterobot when the file is marked official. When it no longer matches the name a package has:
Someone who installs or updates the file again gets it as a new package, under a name built from the new author or mark. Their bots and saved settings stay with the package under the old name. A package that depends on yours under the old name can't be installed any more, because Asterobot refuses to fetch a dependency whose file now has another publisher. A change of your asterobot.net name has the same effect, since it's the publisher part of every name built from your files. A change of title alone is never refused.
Names typed by hand
Only an installation from the marketplace builds a name from a file. Import a .zip and library install use the name typed for them, which can have the same @publisher:name shape. Such a package isn't tied to any file: it has no Source row, Check for updates skips it, and Asterobot records no checksum of its files. Names and versions gives the rules every name follows.
The marketplace describes the marketplace from the website's side.
  • 68 vues
Every function of asterobot:protocol with its parameters, return value and exact errors, plus the traffic object, payload values and interception decisions.
asterobot:protocol is how a script exchanges messages with the game: it sends them, waits for them, reacts to them, and changes or blocks them on their way through. Listening, Waiting, Sending and Intercepting explain how to use each part.
import { send, request, wait, on, onTraffic, intercept, off } from "asterobot:protocol"; These are the module's only exports. There's no protocol object to import.
Export What it does send() Sends a message, and resolves once it's in line to go out request() Sends a request, and resolves with the response matched to it wait() Resolves with the next inbound message that matches on() Calls a handler for every inbound message that matches onTraffic() Calls a handler for every message, in both directions intercept() Decides whether a message goes on unchanged, is dropped or is replaced off() Removes a handler registered with on(), onTraffic() or intercept() Message names and fields are the ones in the Dofus protocol reference.
send()
send(type, payload, options?) returns Promise<void>.
Parameter Type Description type string The message's name, such as "ChatChannelMessageRequest" payload object The message's fields. Pass {} for a message you send without fields: leaving payload out is an error. options.timeout number Accepted and ignored: send() has nothing to wait for. options.to "server" or "client" Where the message goes. Default: "server". options.correlated boolean Gives the message a uid of its own instead of -1. Default: false. send() builds the message and puts it in line to be written. The promise resolves at once: it doesn't wait for an answer, so use wait() when you need to know what the game did. Messages leave in the order send() and request() were called, awaited or not, and a wait() started right after await send() always gets the answer. A message in line still leaves if the script stops before it's written, whether by Stop, a reload or an error.
With correlated: true, the message goes out as a request carrying the next uid, as request() would send it, and the promise still resolves once it's in line. The Dofus client sends a few messages that way and nothing ever answers them: ClientIdRequest, ClientChallengeInitRequest, ClientChallengeProofRequest and BasicLatencyStatsRequest. request() would wait for its whole timeout on those. A response that does come back with that uid reaches on(), onTraffic() and wait() like any other message.
With to: "server", the message goes to the game server as if the Dofus client had sent it. onTraffic() handlers see it go out, and no interceptor is asked about it.
With to: "client", the message is delivered as an event from the server:
On a MITM bot, Asterobot writes it to the Dofus client, which acts on it as if the server had sent it. On a Full socket bot there's no Dofus client, so the message only reaches the script's own on(), onTraffic() and wait(). Either way it arrives as an inbound message, so the same script works on both kinds of bot.
On a MITM bot, an IdentificationRequest sent to the server is never forwarded, because the Dofus client has already identified the session. The promise resolves anyway, and the bot's Console shows the warning Refused to forward a message that would re-authenticate a relayed session.
The promise rejects with:
Error Cause protocol type must be a non-empty semantic message name type is missing, empty or not a string semantic protobuf message is not mapped: <type> Asterobot has no message with that name. Check the spelling, and see After a game update. <type>: expected an object payload is missing, null or undefined <type>.<field>: unknown protobuf field The message has no field with that name, for example IdentificationRequest.token: unknown protobuf field Another text starting with <type> A field's value doesn't fit. Payload errors lists them all. protocol options must be an object options isn't an object, for example send(type, payload, 5000) unknown protocol option "<name>" options has a key other than timeout, to and correlated timeout/milliseconds must be a finite positive Number timeout isn't a number greater than 0 timeout/milliseconds is too large timeout is too big to be a duration send option "to" must be "server" or "client" to isn't a string unknown send target "<value>": expected "server" or "client" to is a string other than those two send option "correlated" must be a boolean correlated isn't true or false send option "correlated" cannot be combined with to: "client": ... A message toward the client is delivered as an event, which has no uid behavior resource limit exceeded: more than 8 MB are waiting to be sent, so the connection isn't taking them The connection has stopped taking messages. See Limits. A message that passes these checks but can't be written later stops the script: with Game session closed when the connection closed, or with send <type>: <reason> otherwise.
await send("ChatChannelMessageRequest", { channel: "GLOBAL", content: "Hello" }); request()
request(type, payload, options?) returns Promise<ProtocolTraffic>.
Parameter Type Description type string The request's name payload object Its fields, as for send() options.timeout number Milliseconds from the call before the promise rejects. Default: 30000. options.to and options.correlated are accepted and ignored: a request always goes to the server, with a uid.
request() sends the message as a request carrying a new uid, and resolves with the response the server sends back with the same uid, as a traffic object whose kind is "response".
Only a few Dofus requests get such a response. The game answers most of them with an event, or not at all, and request() then waits for its whole timeout before rejecting. For those, send the message with send() and wait() for the event.
The promise rejects with the same argument, payload and limit errors as send(), apart from the ones about to and correlated, and with:
Error Cause behavior resource limit exceeded: maximum pending operations reached 32 operations are already pending. See Limits. context deadline exceeded No response came back before the timeout mitm relay refuses to identify: this Game session was already authenticated by the real DOFUS client The script sent IdentificationRequest on a MITM bot. The request isn't forwarded. wait()
wait(selector, options?) returns Promise<ProtocolTraffic>.
Parameter Type Description selector string or function A message name, "*" for any message, or a function that receives a traffic object and returns true for the message you want options.timeout number Milliseconds before the promise rejects. There's no default: without a timeout, wait() waits until the script stops. options.to and options.correlated are accepted and ignored.
wait() looks at the inbound messages the bot handles after the call, and resolves with the first one that matches. When several waits match the same message, they all resolve with it. For each message, the handlers registered with on() and onTraffic() run before the waits are checked.
It doesn't look back, but nothing can slip between await send() and a wait() started right after it: send() resolves before any answer is handled. Waiting shows how.
A selector function runs for every inbound message until the wait settles. If it throws, the whole script stops, with evaluate protocol wait selector: <error>.
The promise rejects with:
Error Cause protocol wait timed out The timeout passed before a message matched protocol selector cannot be empty selector is "" protocol selector must be a semantic type, '*', or predicate selector is neither a string nor a function protocol options must be an object, unknown protocol option "<name>", timeout/milliseconds must be a finite positive Number, timeout/milliseconds is too large The options are wrong, as for send() behavior resource limit exceeded: maximum waits reached 128 waits are already open const echo = await wait( (traffic) => traffic.type === "ChatChannelMessageEvent" && traffic.payload?.content === "Hello", { timeout: 5000 }, ); on()
on(selector, handler) returns a handle, { id }, where id is a BigInt.
Parameter Type Description selector string or function A message name, "*" for any message, or a function that returns true for the messages to handle handler function Called with the traffic object of each matching inbound message. It may be async. on() sends nothing and returns right away. From then on, handler runs for every inbound message that matches, until you pass the handle to off() or the script stops. Outbound messages never reach it, whatever the selector.
Handlers run one message at a time, in the order they were registered, onTraffic() handlers included. A handler registered while a message is being handled starts with the next message. An async handler returns at its first await, and the next handler doesn't wait for it to finish.
An error in a handler stops the script:
What happened Text in the Problems alert The handler threw Game traffic handler failed: <error> An async handler's promise rejected unhandled Promise rejection: <error> The selector function threw evaluate protocol handler selector: <error> on() throws a TypeError at once, instead of returning, when:
Error Cause protocol selector cannot be empty selector is "" protocol selector must be a semantic type, '*', or predicate selector is neither a string nor a function protocol.on handler must be callable handler isn't a function maximum behavior handlers reached 256 handlers from on(), onTraffic() and intercept() are already registered If the script is already stopping, on() throws the reason it's stopping.
const handle = on("ChatChannelMessageEvent", (traffic) => { if (!traffic.payload) return; botInfo(`${traffic.payload.senderName}: ${traffic.payload.content}`); }); onTraffic()
onTraffic(handler) returns a handle, { id }.
It works like on("*", handler), except that it receives messages in both directions:
what the game server sends; what the Dofus client sends, on a MITM bot; what the script sends with send() and request(); what someone sends from the bot's Network tool. A message dropped by an interceptor never reaches it.
onTraffic() throws a TypeError with protocol.onTraffic handler must be callable when handler isn't a function, and with maximum behavior handlers reached past the limit. Errors in the handler stop the script, with the same texts as for on().
intercept()
intercept(selector, handler) or intercept(handler) returns a handle, { id }.
Parameter Type Description selector string or function Optional. A message name, "*", or a function that returns true for the messages to decide about. With only one argument, handler is asked about every message. handler function Called with the traffic object, and returns a decision. It must answer synchronously. An interceptor is asked about the messages the bot carries: every inbound message from the game server, and on a MITM bot every outbound message from the Dofus client. It's never asked about:
a message the script sends, nor one sent from the bot's Network tool, although both still reach onTraffic(); on a MITM bot, the Dofus client's first message, and the responses to the script's own request() calls; a message Asterobot can't read at all. A MITM bot forwards such a message without showing it to the script, and a Full socket bot closes its game connection when it receives one. For each message, interceptors are asked in the order they were registered. One whose selector doesn't match, or that returns no decision, leaves the message to the next. The first decision wins, and the interceptors after it aren't asked.
A message waits for the script only when an interceptor names it, or when one was registered with a function, "*" or no selector, which makes every message wait. Until the package has loaded, every message waits, so an interceptor registered as the package loads decides every message from Play on. Remove an interceptor with off() once you no longer need it.
Return value What happens undefined, null, or any value not listed below No decision. The next interceptor is asked, and when none decides, the message goes on unchanged. "drop" The message stops here. On a MITM bot the other side never receives it, and on every bot on(), onTraffic() and wait() never see it. On a Full socket bot, dropping the response to a request() makes that request() wait for its timeout. { payload } The message goes on, rebuilt from these fields. It keeps its name, its kind and its uid. A field you leave out of the object is left out of the message, and the fields Asterobot has no name for are lost. { raw } The message is replaced by these bytes, a Uint8Array or an ArrayBuffer. They stand for the whole message as it travels on the connection, envelope included, not for traffic.rawAny. raw is used when both raw and payload are present. After a replacement, the script's handlers and waits see the new message. A MITM bot forwards raw bytes even when Asterobot can't read them, and its handlers then see the original message. On a Full socket bot, bytes Asterobot can't read change nothing.
An async handler returns a promise, and a promise isn't a decision: the message goes on before the promise settles. If that promise rejects, nothing catches it, and the script stops with unhandled Promise rejection: <error>.
The message waits for a decision however long the script takes to reach it, including while the script is busy with other work: a decision is never skipped because the PC is slow. Like any code, an interceptor that runs for more than 5 s is stopped, and the message goes on unchanged. On a MITM bot, the person playing feels a slow interceptor, or a busy script, as lag. Intercepting explains these rules with examples.
When a handler throws, or its selector function throws, the script doesn't stop. The message goes to the next interceptor, or on unchanged, and the bot's Console gets an error line for that message. When several interceptors fail on the same message, the line is written for the first failure only, and again at the tenth, ending with (10 times).
Console line Cause interceptor failed: <error> The handler threw, or ran for more than 5 s interceptor selector failed: <error> The selector function threw build replacement payload for <type>: <error> The payload doesn't fit the message. <error> is one of the payload errors. re-encode <type>: <error> Asterobot couldn't rebuild the message with the new fields cannot replace the payload of <wire name>: this build has no name for it, so there is nothing to build a replacement from - use { raw } instead { payload } was returned for a message without a readable name intercept() throws a TypeError, like on(), for a bad selector, with protocol.intercept handler must be callable when handler isn't a function, and with maximum behavior handlers reached past the limit.
intercept("ChatChannelMessageRequest", (traffic) => { if (!traffic.payload) return; return { payload: { ...traffic.payload, content: traffic.payload.content.toUpperCase() } }; }); On a MITM bot, this turns everything the player types in chat into capital letters before the server receives it.
off()
off(handle) returns undefined.
It removes a handler registered with on(), onTraffic() or intercept(), given the handle they returned. A handler removed while a message is being handled doesn't run for that message if its turn hasn't come yet. off() never throws: a handle that was already removed, or anything else, is ignored. It doesn't remove what onChange() from asterobot:parameters registered: use offChange() for that.
The traffic object
Handlers, selector functions and interceptors receive a traffic object, and wait() and request() resolve with one. Traffic explains its properties in depth.
Property Type Value sequence BigInt The message's position in the bot's traffic. A dropped message leaves a gap. On a MITM bot, the object request() resolves with has 0n. direction string "inbound" from the server towards the client, "outbound" from the client towards the server kind string "request", "event" or "response". The type also lists "unknown", which scripts never receive, because a message Asterobot can't read isn't delivered. uid number The number that pairs a request with its response. -1 on events, and on messages sent with send(). type string The message's readable name, or "" when Asterobot has no name for it wireType string The name the message has on the connection, which changes between game versions typeUrl string The full type identifier of the message's content payload object, or absent The message's fields, when Asterobot could read them unknown boolean true when there's no payload decodeError string, or absent What went wrong while reading the message's content. It can be there along with a payload. rawAny Uint8Array The message's content as bytes, without its envelope. Always present, possibly empty. Payload values
A payload's field names are the camelCase names, such as senderName. When sending, the names from the protocol definitions, such as sender_name, work too, but not both for the same field. Payloads covers every case.
Field type A script reads A script can send 64-bit integer A BigInt A BigInt, a string of digits, or a whole number no bigger than Number.MAX_SAFE_INTEGER either way 32-bit integer A number A whole number within the field's range Floating point A number A number Boolean A boolean A boolean String A string A string Bytes A Uint8Array A Uint8Array or an ArrayBuffer Enum The value's name, such as "GLOBAL", or a number for a value without a name The name or the number List An array An array Map An object with string keys A plain object Nested message An object An object, or null to leave it out Group of alternatives (oneof) Only the field that's set One field of the group at most A nested message, an optional field or an alternative that the message doesn't set is absent from payload. Other fields are always there, with 0, "", false or an empty array when they're not set.
When sending, a field you leave out isn't sent. Setting a field to undefined is an error, and null is only accepted for a nested message.
The editor completes message names and payload fields, and knows the types ProtocolTraffic, ProtocolOptions, ProtocolSelector, ProtocolSubscription and ProtocolDecision.
  • 121 vues
The launch object, the session a script runs on, the map its character stands on, the memory its scripts share, disconnect(), onStopRequest(), and the four functions that write to the bot's console.
asterobot:bot gives a script what belongs to its own bot: the facts about the game session, the map its character stands on, a memory every script on the bot shares, a way to close the game connection, a way to hear that the bot's schedule wants it to stop, and the bot's console. The launch argument of the entry function is described on this page too, because session repeats its two values.
import { session, currentMap, memory, disconnect, onStopRequest, offStopRequest, botDebug, botInfo, botWarn, botError } from "asterobot:bot"; These are the module's only exports. There's no bot object to import.
The launch object
Asterobot starts a script by calling the default export of the package's index.js with one argument, launch:
import { session, botInfo } from "asterobot:bot"; import { send } from "asterobot:protocol"; export default async function behavior(launch) { botInfo("Launch:", launch.reason, launch.generation); // 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 the first start of a Full socket bot, the console shows Launch: initial 1.
Property Type Value reason string "initial", "resume" or "reload", as described below generation BigInt Which start this is on the current game connection: 1n for the first, then one more for each start. A start whose script fails while starting still counts. A start that fails before any code runs, such as one with a syntax error, doesn't. A new game connection counts from 1n again. reason When a script starts with it "initial" On a Full socket bot's game connection, as long as no script has started successfully on it yet. A start is successful once the default export's function has finished. "resume" Every later start on the same connection, such as Play after Stop. 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. The default export must be a function that returns a promise, usually an async function. The entry function explains what to do for each reason.
session
session is a frozen object. Its values are set when the script starts and don't change while it runs, except scheduleEndsAt.
Property Type Value gameToken string The ticket the login server issued for this game connection. A Full socket script sends it as ticketKey in IdentificationRequest. serverId number The game server chosen when signing in, fixed for the whole connection. On a MITM bot, the server the Dofus client connected to. It says nothing about where the character is. language string The language code the script was started with. Right now every start made from Asteroboard, with Play, Run or a load from the bot's menu, gives fr, whatever language the bot itself is set to. text() and search() from asterobot:gamedata use this language too. launchReason string The same value as launch.reason behaviorGeneration BigInt The same value as launch.generation connectionId BigInt Which of the bot's game connections the script runs on: one more each time the bot gets a new one, signing in or relaying a Dofus client. Store it next to what you keep in memory for one connection. shared boolean true on a MITM bot, where a person plays the same character through their Dofus client. false on a Full socket bot. scheduleEndsAt Date or undefined When the bot's schedule will ask the script to stop: the end of the current time slot, or the moment it hands over to a time slot that plays another package. undefined outside a time slot, when the bot has no schedule or it's switched off, and when the bot is always connected with one package. It's read again each time you access it, so a schedule saved while the script runs shows up immediately. Every function behaves the same whatever shared says. It exists so that a script that acts on its own, moving or fighting, can decide to stay passive while someone else is playing.
scheduleEndsAt lets a script avoid starting something long just before it's asked to stop:
const end = session.scheduleEndsAt; if (end && end.getTime() - Date.now() < 30 * 60_000) { botInfo("Less than 30 minutes left in this time slot: no dungeon now"); } 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.
While Asterobot reads a package's declarations, there's no session yet. session then holds empty values: "" for the strings, 0 for serverId, "initial", 1n, 0n for connectionId, false and undefined for scheduleEndsAt. See Declarations.
currentMap()
currentMap() returns the map the bot's character stands on, or undefined until the character has entered a map.
import { currentMap, botInfo } from "asterobot:bot"; const map = currentMap(); if (map?.cellId !== undefined) { botInfo("Map", map.mapId, "cell", map.cellId, "actors", map.actors.length); } For a character on cell 300 of map 154010883, next to a monster group, the console shows Map 154010883 cell 300 actors 2.
Property Type Value mapId BigInt The map's id characterId BigInt The id of the bot's character, the one MapMovementEvent gives in characterId when the character walks. Absent until the character is selected. cellId number The cell the character stands on. Absent while the map doesn't place the character yet. actors array Every actor the server placed on the map, the character included, sorted by id. Each one is an object with id, a BigInt, and cellId, a number. A monster group also has monsters: its monsters, the main creature the map shows first, each { id, grade, level }, id being a key of the game data's monsters table. elements array Every interactive element on the map (doors, zaaps, resources, workshops, exits), sorted by id. Each one is an object with id, the number useElement() takes, and skills, what it offers right now: objects with id, the skill id (a row of the game data's skills table), and instanceUid, which offer of that skill on this element. An element whose only skill is 339, Point Out an Exit, is an exit to the next map. Asterobot follows the map itself, for the whole game session, from the messages the bot sends and receives: MapCurrentEvent when the server names the map the character is now on, or MapCurrentInstanceEvent for an instanced map, MapComplementaryInformationEvent for the actors already there, then GameRolePlayShowActorsEvent, MapMovementEvent and ContextRemoveElementEvent for the actors that come, walk and leave, and MapMovementRefusedEvent for the cell the server keeps the character on when it refuses a walk. The interactive elements come with MapComplementaryInformationEvent too, and InteractiveMapUpdateEvent and InteractiveElementUpdatedEvent keep them current. So a script started or reloaded in the middle of a session reads where the character is, not only what it saw since it started. Each call returns a new object holding the values of that moment.
On a map change, the server names the new map with MapCurrentEvent first, or with MapCurrentInstanceEvent for an instanced map, whose mapId plays the same part, and sends none of its contents until the client has said it's ready, with ContextReadyRequest and MapInformationRequest. From then on, currentMap() describes the new map, with no actors and no cellId: the character has left the previous map, and nothing of the new one has arrived. MapComplementaryInformationEvent then places the actors and the character. On a Full socket bot, sending the two requests is up to a script: @asterobot:base does it for every map. After a map change made by changeMap(), an exit's useElement() or travel(), Asterobot sends them itself unless a script said it does with answerMapLoads(), as base does. A character is in the world, ready to act, once cellId is set. A move() asked for before that rejects with move: the character's map and cell aren't known yet.
An actor's cellId is the cell its walk ends on from the moment the server grants the walk, before the actor gets there. When the character's own walk stops early, for example because the player clicked elsewhere on a MITM bot, the MapMovementCancelRequest sent for it moves the character to the cell where it stopped. Fights aren't followed.
memory
memory is the bot's shared memory: keys that name values, which every script the bot runs can read and write, and which outlive each of them.
import { memory, session, botInfo } from "asterobot:bot"; import { on, send } from "asterobot:protocol"; const KEY = "@alice:chat-count/count"; export default async function behavior(launch) { on("ChatChannelMessageEvent", () => { // Only a count stored on this game connection goes on: after a // reconnection, it starts over. const stored = memory.get(KEY); const count = (stored?.connection === session.connectionId ? stored.count : 0) + 1; memory.set(KEY, { connection: session.connectionId, count }); botInfo(`${count} chat messages on this connection`); }); // 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, }); } } Stop then Play doesn't reset the count: the next chat message on the same connection shows 4 chat messages on this connection after three. Call the functions from the entry function or a handler, not from the top level of a module: see the last paragraph of this section.
A script's own variables are gone after Stop, a reload or Run in the editor, while the bot and its game connection carry on. What the script needs to find again goes here. Bot memory explains how to use it, with how long it lasts and how to keep a connection's state.
Function Returns What it does memory.get(key) The value, or undefined Reads a copy of the value stored under key. memory.set(key, value) undefined Stores a copy of value under key, replacing what was there. memory.delete(key) boolean Removes key. Returns whether it was there. memory.keys() array of strings Every stored key, sorted. It belongs to the bot. It survives Stop, Play, reloads and reconnections. It's empty again when Asterobot restarts, when the bot is deleted, and when changing the bot's account or game version rebuilds it. One memory per bot, shared by every package it runs. A value one package stores is there for any other to read. Start your keys with your package's name, as in @alice:farm/progress, so two packages never pick the same key by accident. Nothing enforces it. Values are plain data: null, booleans, numbers, strings, BigInts, arrays and plain objects, nested at most 32 levels. Anything else throws a TypeError, naming where it is in the value: undefined (use delete() instead), functions, NaN and infinities, Map, Date, symbols and instances of a class. Values are copies. Changing an object you read back changes nothing until you set() it again. An object's keys come back sorted by name. For one connection only, store session.connectionId next to the value, as the example does, and ignore a value whose id isn't the current one. launch.reason can't tell you: a start that fails half way leaves the next one "initial" on the same connection. It's bounded: 1024 keys of up to 256 bytes, 64 KiB per value, 1 MiB per bot. Past a limit, set() throws an Error that names it, such as memory: the value is larger than 64 KiB. All four functions are synchronous: the memory is in Asterobot's own memory, not on disk. While Asterobot reads a package's declarations there's no bot, so get() returns undefined, keys() an empty array, and set() and delete() throw.
The bot's page shows the memory under Tools > Memory, and refreshes it when a script changes it.
disconnect()
disconnect() returns undefined.
It closes the bot's game connection once the messages the script sent before it have left, and returns right away, without waiting for the connection to close. The script stops as a result, and the bot's page shows The behavior stopped with an error with the text behavior requested Game disconnect. Calling disconnect() again, or while the script is already stopping, does nothing.
Warning
On a MITM bot, the connection is the game session of the person playing. disconnect() closes it for their Dofus client too. Check session.shared first if that matters to your package.
Asterobot doesn't connect the bot again by itself, unless the bot's schedule is in one of its time slots. Disconnect, delete and restart describes what happens next from the player's side. To end a session when a time slot of the schedule ends, return from an onStopRequest() handler instead: the bot is then stopped without an error.
onStopRequest()
onStopRequest(handler) returns a handle, { id }, where id is a BigInt.
Argument Type Value handler function Called with { reason, deadline } when Asterobot wants the script to stop. It may be async. Property Type Value reason string Why: "schedule", meaning a time slot of the bot's schedule ended, or handed over to one that plays another package. It's the only reason for now. deadline Date When Asterobot stops the script anyway. When a time slot of the bot's schedule ends, or hands over to a time slot that plays another package, Asterobot calls each handler registered at that moment, once. Meanwhile the script carries on as usual: its handlers, sends, trips and actions all keep working. The script counts as done once every handler has returned, or its returned promise has settled. Asterobot then stops the script and disconnects the bot, or plays the next package in the game the bot is still in. It does so at deadline at the latest. The time between the request and deadline is the schedule's Time to finish, from 0 to 30 minutes.
A script that registered no handler is stopped immediately, and so is any script when the time to finish is 0. A handler registered after the request isn't called for it.
import { onStopRequest, session, botInfo, botWarn } from "asterobot:bot"; import { send } from "asterobot:protocol"; import { sleep } from "asterobot:timers"; let stopping = false; let round = Promise.resolve(); onStopRequest(async ({ deadline }) => { stopping = true; botInfo(`Asked to stop by ${deadline.toLocaleTimeString()}`); // An error escaping a handler stops the script. try { // The round under way finishes, and no new one starts. await round; } catch (error) { botWarn(String(error)); } }); async function loop() { while (!stopping) { // Stands for one round of work: a fight, a harvest, a trip. round = sleep(60_000); await round; } } 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, }); } void loop(); } Return from the handler rather than calling disconnect(): the bot is then stopped and disconnected without an error, as with the Disconnect button.
An error thrown by a handler stops the script, as in any other handler, and the bot is disconnected immediately. If the promise a handler returned rejects, the error is written to the bot's console starting with onStopRequest handler: , and the handler counts as done.
Problem What happens handler isn't a function onStopRequest() throws a TypeError with onStopRequest handler must be callable 256 handlers are already registered with onStopRequest() onStopRequest() throws a TypeError with maximum behavior handlers reached While Asterobot reads a package's declarations, onStopRequest() returns a handle as usual, and the handler is never called.
offStopRequest()
offStopRequest(handle) returns undefined. It removes the handler registered with the onStopRequest() call that returned handle. A handle that's already removed, or anything else, does nothing.
botDebug(), botInfo(), botWarn() and botError()
Each takes any number of values and returns undefined:
botInfo("Kamas:", traffic.payload?.kamas); botWarn("Reply not delivered:", String(error)); The line goes to the bot's Console tab as a Script line, at the level the function names: debug, info, warn or error. Lines of every level show there, debug included. The values are joined with spaces. These functions never throw.
Asterobot turns each value into text on its own, and only strings, numbers and booleans come out the way JavaScript would print them:
Value Printed as A string As it is A number 42, 1.5 A BigInt Its digits, without the n: 1500 A boolean true, false undefined or null <nil> An array Items separated by spaces, in brackets: [1 2 3] An object map[ then its properties sorted by name: map[kamas:1500 name:Airelle] An Error map[], so log String(error) instead To print an object readably, build the text yourself, for example with a template string. JSON.stringify() throws on any value that holds a BigInt, with the message Do not know how to serialize a BigInt, and message payloads often do.
When you open a bot's Console tab, it starts with the last 200 lines Asterobot kept for that bot.
Code at the top level of a module also runs while Asterobot reads the package's declarations. What these functions write at that moment is thrown away.
To write to Asterobot's own log instead, use asterobot:console. Logging and debugging compares the two and suggests a way to track down a problem.
  • 121 vues
sleep(), the only timer scripts have, with its argument, how it rejects, and how to repeat a task on a schedule.
asterobot:timers has a single export, sleep(). Scripts have no setTimeout() or setInterval(), so pauses, delays and repeated tasks are all built with it.
import { sleep } from "asterobot:timers"; sleep()
sleep(milliseconds) returns Promise<void>.
Parameter Type Description milliseconds number How long to wait. 0 and fractions such as 2.5 are accepted. The promise resolves once the time has passed and the script gets to it: when other events are waiting to be handled, it resolves after them. While a script awaits sleep(), its message handlers keep running.
The promise rejects with:
Error Cause timeout/milliseconds must be a finite positive Number milliseconds is negative, NaN or Infinity, or isn't a number at all. A BigInt such as 1000n and a string such as "1000" are refused too. timeout/milliseconds is too large milliseconds is too big to be a duration, more than about 292 years behavior resource limit exceeded: maximum timers reached 128 calls to sleep() are already pending. See Limits. When the script stops while a sleep() is pending, its promise rejects with the reason the script stopped, such as behavior runtime closed after Stop.
Repeating a task
Start the loop without awaiting it, so the entry function can finish. A start only counts as successful once that function has returned, which matters for launch.reason.
import { session, botInfo, botWarn } from "asterobot:bot"; import { send } from "asterobot:protocol"; import { sleep } from "asterobot:timers"; async function everyMinute() { while (true) { await sleep(60000); botInfo("Still running"); } } export default async function behavior(launch) { // Not awaited, and caught: a rejection nobody catches stops the script. everyMinute().catch((error) => botWarn("Timer loop stopped:", 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, }); } } When you click Stop, the pending sleep() rejects, so the console shows Timer loop stopped: behavior runtime closed as the script ends.
Don't poll in a tight loop to wait for a message: await wait(selector, { timeout }) from asterobot:protocol resolves as soon as the message arrives. And never keep the script busy without an await: messages wait meanwhile, and code that runs for more than 5 s in one go stops the script. Async and timing explains why.
  • 63 vues
text, record, table, find, search and query, which read the game's texts and data from a script, with every option, return shape, limit and error.
asterobot:gamedata reads the texts and the data of the game version the bot uses: items, monsters, maps, spells and everything else Asterobot extracts from the game files. It only reads. Game data overview, Lookups and SQL show how to use it.
import { text, record, table, find, search, query } from "asterobot:gamedata"; These are the module's only exports. There's no gamedata object to import.
Export Returns What it reads text() A string or undefined, right away One game text, by id or key record() An object or undefined, right away One row of a table, by id table() A promise of an array The rows of a table, a page at a time find() A promise of an array The rows whose columns equal given values search() A promise of an array Records found by their name query() A promise of a result The answer to a read-only SQL query What the data is
The data comes from the game version the bot is set to. text() and search() answer in the language the script was started with, session.language, which is fr for every start made from Asteroboard today.
Asterobot looks for the data when the script starts. When that game version has no extracted game data at that moment, record() returns undefined, and table(), find(), search() and query() resolve with undefined rather than an array, until the script starts again after the data is extracted. When Asterobot can't load the game's texts in that language, text() returns undefined for every key. Extract game data explains how to get the data.
Table and column names, such as monsters or name_id, are the ones of the extracted data, and they can change between game versions. Browse them on the Game page's game data (see Game data), and try queries in the SQL tab.
In the rows these functions return, numbers are regular JavaScript numbers, not BigInt. A packed column arrives as an array of objects, and a JSON column as the value it holds. record(), table() and find() leave empty columns out of their objects.
table(), find(), search() and query() share these rules:
They reject after 30 seconds, and there's no option to change that. Each call counts as one of the 32 operations a script can have pending at once, along with send() and request(). Past that, the promise rejects with behavior resource limit exceeded: maximum pending operations reached. limit defaults to 200 rows and can't go above 5000. A limit or offset that isn't a whole number greater than 0 is ignored. text()
text(key) returns a string, or undefined.
Parameter Type Description key number or string A text id, as found in columns such as name_id, or a text key such as "ui.common.classic" It returns the text in the script's language, or undefined when no text has that id or key.
When the game's texts are loaded, text() throws a TypeError with gamedata.text key must be a string or integer for any other key: a fraction, a number outside the 32-bit range, a BigInt, undefined or an object. Check a value exists before passing it.
record()
record(table, id) returns an object, or undefined.
Parameter Type Description table string The table's name, such as "monsters" id number The record's id: a whole number, not a BigInt It returns the row with that id, or undefined when there's none.
Error Type Cause gamedata.record table must be a non-empty string TypeError table is missing, empty or not a string gamedata.record id must be an integer TypeError id isn't a whole number, is too big to be exact in JavaScript, or is a BigInt or a string datacenter: no such table: "<table>" Error This game version has no table with that name. String(error) gives GoError: datacenter: no such table: "<table>". const monster = record("monsters", 31); if (monster) botInfo("Monster 31 is", text(monster.name_id)); table()
table(name, options?) returns Promise<object[]>.
Parameter Type Description name string The table's name options.limit number How many rows to return. Default 200, at most 5000. options.offset number How many rows to skip first. Default 0. It resolves with the rows, in the order of their ids. A call without options returns the first 200 rows, not the whole table.
The promise rejects with gamedata.table name must be a non-empty string when name isn't a non-empty string, and with datacenter: no such table: "<name>" for a table this game version doesn't have.
const page = await table("items", { limit: 50, offset: 100 }); find()
find(name, filter, options?) returns Promise<object[]>.
Parameter Type Description name string The table's name filter object Column names and the values they must equal. A row matches when every column equals its value. options.limit number How many rows to return. Default 200, at most 5000. offset is ignored. Error Cause gamedata.find table must be a non-empty string name isn't a non-empty string gamedata.find filter must be a non-empty object filter is missing, empty, an array, or not an object datacenter: no such table: "<name>" This game version has no table with that name datacenter: no such column: table "<name>" has no column "<column>" A filter column doesn't exist in the table const spots = await find("maps__interactive_elements", { gfx_id: 1018 }); search()
search(query, options?) returns Promise<Match[]>.
Parameter Type Description query string The name, or part of it options.table string Only search this table's records, such as "monsters". Without it, every named record is searched. options.limit number How many matches to return. Default 200, at most 5000. Each word of query matches the beginning of a word, so a partial name works. Shorter names come first. Without options.table, the results take one match from each table in turn, so the first results show every kind of thing that has that name. A query made only of spaces resolves with an empty array.
Error Cause gamedata.search query must be a non-empty string query isn't a non-empty string gamedata.search options.table must be a string options.table is there but isn't a string datacenter: no such table: "<table>" This game version has no table with that name datacenter: no text for this language: "<language>" The extracted data has no names in the script's language const [hit] = await search("Bouftou Royal", { table: "monsters" }); if (hit) botInfo(hit.name, "is monster", hit.id); query()
query(sql, params?, options?) returns Promise<QueryResult>.
Parameter Type Description sql string One or more SQL statements params array Values for the ? placeholders, in order options.limit number How many rows to return. Default 200, at most 5000. The game data is opened read-only, so any statement that writes fails, and attaching another database is refused. Every statement in sql runs, and the result is the last one's: "SELECT 1; SELECT 2" answers 2. A query runs for at most 10 seconds, and its text can't be longer than 65,536 bytes.
Pass values through params instead of writing them into the SQL: a number written into the text arrives as text, and doesn't equal the number stored in a column.
In rows, an empty value is null.
Error Cause gamedata.query sql must be a non-empty string sql isn't a non-empty string gamedata.query params must be an array params is there but isn't an array datacenter: empty query sql holds only spaces datacenter: query is <size> bytes, over the 65536 byte limit The SQL text is too long datacenter: followed by the database's message The SQL is wrong, names a table or column that doesn't exist, or tries to write, in which case the message mentions attempt to write a readonly database The promise also rejects when the query runs for more than 10 seconds, counted once its turn has come: the wait while other queries run doesn't count.
const { columns, rows, truncated } = await query( "SELECT _id, name_id FROM monsters WHERE _id = ?", [31], ); Types
Type Shape Row An object whose keys are the table's column names Window { limit?: number, offset?: number }, the options of table() and find() SearchOptions { table?: string, limit?: number } QueryOptions { limit?: number } Match { table: string, id: number, nameId: number, name: string }: the record's table and id, to pass to record(), the id of the text its name comes from, and the name in the script's language QueryResult { columns: string[], rows: unknown[][], truncated: boolean }. columns lists the column names in order, and they can repeat, which is why each row is an array aligned with them. truncated is true when the query had more rows than limit.
  • 82 vues
The functions that write to Asterobot's own log instead of the bot's console, where their lines appear, and how values print.
asterobot:console writes to Asterobot's own log, the one shown in Server > Console and in the terminal Asterobot runs in. It's for a script's diagnostics. To show something to the person watching a bot, use botInfo() and the other functions of asterobot:bot instead: they write to that bot's own console.
import { console, debug, log, info, warn, error } from "asterobot:console"; Scripts have no global console. The console here is an object this module exports, so it has to be imported like the rest.
Exports
Export Level of the line debug(...values) debug log(...values) info. log is the same function as info. info(...values) info warn(...values) warn error(...values) error console A frozen object with the same five functions: console.debug(), console.log(), console.info(), console.warn() and console.error() Each function takes any number of values, writes them as one line joined with spaces, and returns undefined. They never throw.
info("Route computed in", elapsed, "ms"); console.warn("No zaap found for map", mapId); Where the lines appear
Lines go to Server > Console and to the terminal. By default, debug lines are left out of both: set Server level to DEBUG on Server > Console to see them there.
In Server > Console, a line doesn't say which bot wrote it. When several bots run scripts, write the bot's name or something that identifies it into the text yourself.
How values print
Values are turned into text the same way as for botInfo(): strings, numbers and booleans print as JavaScript would, a BigInt prints without its n, undefined and null print <nil>, an array prints as [1 2 3], an object as map[name:value], and an Error as map[]. Log String(error) for errors. The table on asterobot:bot has every case.
At a module's top level
Code at the top level of a module also runs while Asterobot reads the package's declarations, and what these functions write at that moment is thrown away. The same code runs again when the script starts, and its lines appear then.
Logging and debugging compares the two consoles and when to use each.
  • 56 vues

Compte

Navigation

Recherche

Recherche

Configurer les notifications push du navigateur

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