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.
packages.info(), which tells a script the name, version, origin, integrity, permissions and compatibility of its own package, its dependencies and the built-in modules.
asterobot:runtime lets a script read facts about the packages it's made of: its own package, the packages it depends on, and the built-in asterobot: modules. Nothing in it can change a package.
import { packages } from "asterobot:runtime"; packages is the module's only export. It's a frozen object with one function, info(). There's no flat info export, so it can't be confused with info() from asterobot:console.
packages.info()
packages.info(name) returns a frozen PackageInfo object.
Parameter Type Description name string A package name, such as "my-first-script" or "@alice:pathfinder", or the name of a built-in module, such as "asterobot:protocol" The names it knows are:
every package in the running script: the bot's package and, all the way down, the packages its dependencies lock; the built-in modules asterobot:protocol, asterobot:bot, asterobot:timers, asterobot:gamedata, asterobot:console, asterobot:runtime, asterobot:pathfinding, asterobot:movement, asterobot:verification, asterobot:character, asterobot:fight, asterobot:chat and asterobot:alarms. asterobot:parameters isn't one of them: each package has its own copy of that module, and asking for it throws package "asterobot:parameters" is not installed.
An inline script, loaded as code rather than as a library package, is the package api-script with the version local.
packages.info() throws when:
Error Type Cause packages.info requires a module or package name TypeError name is missing, empty or not a string. Despite the wording, a module path isn't accepted. package "<name>" is not installed Error No package in the running script has that name package "<name>" is ambiguous across <count> locked versions Error The script's dependencies lock more than one version of that package, so the name alone doesn't say which const self = packages.info("my-first-script"); botInfo(`${self.name} ${self.version}, ${self.provenance}`); On a local package, this writes my-first-script 1.0.0, local to the bot's console.
packages.info() also answers while Asterobot reads the package's declarations, so a declaration can use it.
PackageInfo
Property Type Value name string The package's name version string The package's version. "embedded" for a built-in module. publisher string The part before the colon in @publisher:name: "asterobot" for official packages and built-in modules, "" for a local package provenance string "local", "official", "community", or "builtin" for a built-in module integrity string The checksum Asterobot recorded when it installed the package from the marketplace, starting with sha256-. "" when there's none, as for a local package, and "embedded" for a built-in module. permissions array of strings The permissions listed in the package's asterobot.json. An empty array when it lists none, and always for a built-in module. compatibleVersion string The Asterobot versions the package declared it works with, such as ">=1.5.0 <2.0.0", or "" The type in the editor also lists "builtin-native" as a possible provenance. Asterobot never returns it.
A package's provenance comes from its name: a name starting with @asterobot: is official, another name starting with @ is community, and a name without @ is local. Names and versions explains the rules, The manifest describes asterobot.json, and Compatibility covers compatibleVersion.
Permissions
permissions only reports what a package declares. Asterobot doesn't check it today: any package can use every function of every built-in module, whatever it lists.
Note
Coming soon. Asterobot will enforce the permissions a package declares. Until then, treat the list as information for the people who install your package. Permissions has the details.
  • 56 vues
values, onChange and offChange, which give a script its package's current settings, with the value types and when changes arrive.
asterobot:parameters gives a script the current values of its package's settings: the ones declared with export const parameters and shown on the bot's Settings tab, under Package settings. Declarations describes how to declare them, and Read settings shows how to use them.
import { values, onChange, offChange } from "asterobot:parameters"; Whose settings
Each package gets its own copy of this module. Any file of a package that imports it reads that package's settings, declared in that package's index.js, and nothing else. A dependency reads its own settings, never those of the package that uses it, and no package can read another package's settings. Settings in dependencies explains how that looks on the bot's page.
values
values is a read-only object with one property per declared setting.
Declared type Value in values bool A boolean string A string int A whole number. It's a regular number, not a BigInt, unlike 64-bit fields in game messages. double A number array An array of values of the of type map An object whose keys are strings and whose values have the of type The values are the ones saved for this bot, on top of the declared defaults. Settings are saved under the package's name, whatever its version, so they carry over when the bot moves to a new version. A saved value that no longer fits the declaration, because the setting was renamed, removed or given another type, is ignored, and the default applies.
values is live. When someone clicks Apply on the bot's page, the running script reads the new values from its next read on, without restarting. A package's settings are applied all at once, so a script never reads half of an edit.
Don't assign to values or delete from it. Package code always runs as a module, which JavaScript makes strict, so Asterobot's refusal throws: assigning a property throws a TypeError with 'Set' on a dynamic object returned false, and deleting one throws Could not delete property "<name>" of a dynamic object. Either way the setting keeps the value set on the bot's page.
Object.keys(values) lists the settings, in no particular order, and "name" in values tells whether a setting exists.
When Asterobot can't read the package's declarations, the bot's page shows Couldn't read this package's settings, and the script starts with no settings at all: values is empty, and every read returns undefined.
While Asterobot reads the declarations themselves, values is an empty object too, so a declaration can't depend on a setting's value.
onChange()
onChange(handler) returns a handle, { id }, where id is a BigInt.
Parameter Type Description handler function Called with { name, value } for each setting whose value changed When settings of this package are applied to the running script, handler runs once for each setting whose value actually changed, with the setting's name and its new value, of the type shown in the table above. Settings applied with the value they already had don't call it, and arrays and maps count as changed only when their content differs.
It doesn't run when the script starts, and it never runs for another package's settings. With several handlers, the order they run in isn't defined. Settings applied while no script runs are saved, and the next start reads them from values.
In the rare case where the script already has 32 operations pending when someone applies settings, the settings are saved, but the running script only gets them at its next start. See Limits.
An error in a handler stops the script:
What happened Text in the Problems alert The handler threw parameters.onChange handler <number>: <error> An async handler's promise rejected unhandled Promise rejection: <error> onChange() throws a TypeError at once when:
Error Cause parameters.onChange handler must be callable handler isn't a function maximum behavior handlers reached 256 onChange() handlers are already registered. This count is separate from the message handlers of asterobot:protocol. If the script is already stopping, onChange() throws the reason it's stopping.
onChange(({ name, value }) => { botInfo(`Setting ${name} is now ${value}`); }); offChange()
offChange(handle) returns undefined.
It removes the handler that onChange() registered, given the handle it returned. It never throws: a handle that was already removed, or anything else, is ignored. It doesn't remove handlers registered with on(), onTraffic() or intercept(): use off() for those.
Types
Type Definition ParameterScalar A boolean, a string or a number ParameterValue A ParameterScalar, a read-only array of them, or a read-only object of them ParameterChange { name: string, value: ParameterValue }, what onChange() handlers receive ParameterSubscription { id: bigint }, the handle onChange() returns When a value applied from the bot's page doesn't fit its declaration, nothing is saved and the page shows Couldn't apply the settings with the reason. Error messages lists those reasons.
  • 57 vues
findPath, cellToPoint, pointToCell, distance and direction, which find the route the Dofus client would walk on a map and work with cell positions, with every option, return shape and error.
asterobot:pathfinding computes moves on a map the way the Dofus client does. findPath() gives the route the client would walk from one cell to another, the key cells to send for it and how long the walk lasts. The other functions work with cell positions: where a cell sits on the map's grid, and the distance and the direction between two cells.
It only computes. To walk, call move() from asterobot:movement, or send the walk yourself as Walk along a path shows. To go to the next map or use an element of the map, call changeMap() or useElement().
import { findPath, cellToPoint, pointToCell, distance, direction } from "asterobot:pathfinding"; These are the module's only exports. There's no pathfinding object to import.
Export Returns What it does findPath() A path or undefined, right away Finds the route from one cell of a map to another cellToPoint() A point, right away Gives a cell's position on the grid pointToCell() A cell or undefined, right away Gives the cell at a grid position distance() A number, right away Measures the grid distance between two cells direction() A direction or undefined, right away Gives the direction from one cell to another None of them returns a promise. Like the functions of asterobot:gamedata, they throw when the top level of a module calls them while Asterobot reads the package's declarations: see Declarations.
Everything here is about moving outside fights. Moves in a fight follow other rules, and this module doesn't cover them.
Cells and directions
A map has 560 cells, numbered 0 to 559. Every function takes cell ids as regular whole numbers: a BigInt or a string is refused.
A direction is a number from 0 to 7:
Number Direction on screen 0 East 1 South-east 2 South 3 South-west 4 West 5 North-west 6 North 7 North-east findPath()
findPath(mapId, from, to, options?) returns a Path, or undefined.
Parameter Type Description mapId BigInt, number or string The map's id: a BigInt such as currentMap().mapId, a whole number up to Number.MAX_SAFE_INTEGER, or the id written in digits, such as "154010883". Any map of the bot's game version works, not only the one the character stands on. from number The cell the walk starts from to number The cell to walk to options.occupied number[] The cells other actors stand on, as described below. Default none. options.cautious boolean The cautious flag the walk will be sent with. It changes how the character moves, and so the walk's times. Default false. options.disableDiagonals boolean true only for a character whose look has no diagonal walking animations. Player characters have them, so leave it false. It reads the map from the game data of the bot's game version and returns the route the Dofus client would take. When to can't be reached, the route ends on the reachable cell closest to it instead, and end tells you which. findPath() returns undefined only when the Dofus client itself would find no route at all, which doesn't happen on the game's maps.
Occupied cells
The Dofus client's router makes a cell with an actor on it more expensive to walk onto, so its routes tend to go around actors. To get the route the client would walk, pass in occupied the cell of every actor the server shows on the map except the bot's own character: other players, monster groups, NPCs, merchants, tax collectors, prisms and the like. Items on the ground, marks and glyphs don't count. currentMap() lists the actors:
import { currentMap } from "asterobot:bot"; import { findPath } from "asterobot:pathfinding"; const map = currentMap(); if (map?.cellId !== undefined) { const others = map.actors.filter((actor) => actor.id !== map.characterId); const path = findPath(map.mapId, map.cellId, 301, { occupied: others.map((actor) => actor.cellId), }); } The path
Property Type Value start number The cell the route starts from: from end number The cell the route ends on: to when the router reached it, otherwise the reachable cell closest to to steps array The route's steps, in walking order. Each one is an object with cell, the cell the character leaves, and direction, the direction it walks from there. keyCells number[] What MapMovementRequest carries in keyCells for this route. Send them as they are. Empty when end is start: there's nowhere to walk, and the Dofus client sends nothing. gait string "walk" or "run": how the character moves along the route duration number How long the walk lasts, in milliseconds: from its start to the moment the Dofus client sends MapMovementConfirmRequest. 0 when end is start. stepEnds number[] When each step ends, in milliseconds from the start of the walk. The last one is duration. A character walks a route of 3 cells or fewer, the start included, and runs a longer one. With cautious: true it always walks.
duration is the earliest moment a Dofus client can confirm the walk. A real client only ends a step when it draws its next frame, so it confirms a little later: in walks recorded from the Dofus client, the confirmation left 10 to 48 ms after duration. The times are those of a character on foot. A mounted character runs at a speed Asterobot doesn't cover yet, so its duration can be wrong.
Errors
Error Type Cause pathfinding.findPath mapId must be an integer Number, BigInt or decimal string TypeError mapId is a fraction, a number above Number.MAX_SAFE_INTEGER, a string that isn't a whole number written in digits, or another type pathfinding.findPath from must be an integer cell id, and the same with to TypeError The cell isn't a whole number in the 32-bit range, or is a BigInt or a string pathfinding.findPath options must be an object TypeError options is set, not to null or undefined, and isn't an object: an array or a number, for example pathfinding.findPath options has no property "<name>" TypeError options has a key other than occupied, cautious and disableDiagonals, such as the misspelled ocupied pathfinding.findPath options.occupied must be an array of cell ids TypeError occupied isn't an array pathfinding.findPath options.occupied[<index>] must be an integer cell id TypeError That item isn't a whole number in the 32-bit range pathfinding.findPath options.cautious must be a boolean, and the same with disableDiagonals TypeError The option is set to something other than true, false, null or undefined pathfinding.findPath: map <mapId>, cell <from> to cell <to>: pathfinding: not a map cell, cell ids go from 0 to 559: <cell> TypeError from or to is a whole number outside 0 to 559. For a cell of occupied, the text ends with occupied cell <cell> instead. failed to load map <mapId> for pathfinding: datacenter: no such record: maps/<mapId> Error The bot's game version has no map with that id failed to open the datacenter: followed by the reason Error Asterobot can't read the game data of the bot's game version, for example because it isn't extracted. See Extract game data. String(error) gives TypeError: or GoError: followed by the text.
cellToPoint()
cellToPoint(cell) returns { x, y }, the cell's position on the map's grid.
cellToPoint(300); // { x: 17, y: -4 } The grid is the map turned 45 degrees, so both numbers change along a row of the screen. One cell to the east adds 1 to x and 1 to y. One cell to the south adds 1 to x and takes 1 from y.
Error Type Cause pathfinding.cellToPoint cell must be an integer cell id TypeError cell isn't a whole number in the 32-bit range, or is a BigInt or a string pathfinding.cellToPoint cell: pathfinding: not a map cell, cell ids go from 0 to 559 TypeError cell is outside 0 to 559 pointToCell()
pointToCell(x, y) returns the cell at that grid position, or undefined when the position is off the map.
pointToCell(18, -3); // 301 pointToCell(0, 1); // undefined It throws a TypeError with pathfinding.pointToCell x and y must be integers when x or y isn't a whole number in the 32-bit range, or is a BigInt or a string.
distance()
distance(a, b) returns the grid distance between two cells, as the Dofus client measures it: how far apart their x are, plus how far apart their y are.
distance(300, 301); // 2 So the next cell to the south-east, south-west, north-east or north-west is 1 away, and the next cell to the east, west, north or south is 2 away.
Error Type Cause pathfinding.distance a must be an integer cell id, and the same with b TypeError The cell isn't a whole number in the 32-bit range, or is a BigInt or a string pathfinding.distance cells: pathfinding: not a map cell, cell ids go from 0 to 559 TypeError A cell is outside 0 to 559 direction()
direction(from, to) returns the direction from one cell to another, or undefined when the two cells aren't on one line.
direction(300, 301); // 0, east direction(300, 316); // undefined Two cells on one line give that line's direction, whatever their distance. The same cell twice gives 1, as it does in the Dofus client.
Error Type Cause pathfinding.direction from must be an integer cell id, and the same with to TypeError The cell isn't a whole number in the 32-bit range, or is a BigInt or a string pathfinding.direction cells: pathfinding: not a map cell, cell ids go from 0 to 559 TypeError A cell is outside 0 to 559 Walk along a path
A walk takes two exchanges with the game server:
The script sends MapMovementRequest with the path's keyCells, the cautious flag and the map's id. The server grants the walk with MapMovementEvent, whose cells list every cell of the walk. The server sends that message for every actor that walks on the map, so compare its characterId with the character's. It can also grant a shorter walk than the one asked for, or refuse the walk with MapMovementRefusedEvent, whose cellX and cellY are the grid position of the cell the character stays on: pointToCell() turns them into its id. Once the walk is over, the character confirms it with MapMovementConfirmRequest, which has no field, and the server answers with MapMovementConfirmResponse. Until then, the server considers the character still walking. Who sends the confirmation depends on the bot:
Bot Who sends MapMovementConfirmRequest Full socket Your script, duration milliseconds after MapMovementEvent arrived. Nothing else will. MITM, where session.shared is true The Dofus client, once it has walked the character on screen. Your script only waits for the server's answer. import { currentMap, session } from "asterobot:bot"; import { findPath } from "asterobot:pathfinding"; import { send, wait } from "asterobot:protocol"; import { sleep } from "asterobot:timers"; async function walkTo(cell) { const map = currentMap(); if (map?.cellId === undefined) return; const others = map.actors.filter((actor) => actor.id !== map.characterId); const path = findPath(map.mapId, map.cellId, cell, { occupied: others.map((actor) => actor.cellId), }); if (!path || path.keyCells.length === 0) return; await send("MapMovementRequest", { keyCells: path.keyCells, cautious: false, mapId: map.mapId }); await wait( (traffic) => traffic.type === "MapMovementEvent" && traffic.payload?.characterId === map.characterId, { timeout: 5000 }, ); if (session.shared) { // The Dofus client confirms the walk once it has walked it. await wait("MapMovementConfirmResponse", { timeout: path.duration + 5000 }); return; } await sleep(path.duration); await send("MapMovementConfirmRequest", {}); await wait("MapMovementConfirmResponse", { timeout: 5000 }); } Each wait() starts right after its send(), so it can't miss the answer, as Send messages explains.
This sample times the confirmation with the route it asked for. When the server grants a shorter walk, time the walk it granted instead, for example with findPath() from the first of its cells to the last. Start one walk at a time: a walk started before the previous one ends would start from a cell the character hasn't reached yet.
To stop a walk before its end, the Dofus client sends MapMovementCancelRequest with the cell the character stopped on, and the server doesn't answer it. stepEnds tells you which step the character has reached at a given moment.
move() does all of this in one call, shorter walks, refusals and both kinds of bots included, and changeMap() and useElement() walk the same way before they change map or use an element.
asterobot:bot describes currentMap(), and the Dofus protocol reference lists the fields of the movement messages.
  • 132 vues
move(), changeMap(), useElement(), travel() and the map finders, which walk the bot's character on its map, take it to the next map by a side, use the doors, zaaps, resources and exits of the map, take it to any map however far and find the maps to go to, on Full socket and MITM bots alike, with every option, error and limit.
asterobot:movement moves the bot's character the way the Dofus client does when the player clicks:
move() walks the character to a cell of the map it stands on; changeMap() takes it to the next map by one side of the map; useElement() uses an interactive element of the map: a door, a zaap, a resource, a workshop, or an exit (the sun drawn on some cells); travel() takes it to another map, however far, map after map; route() tells which way it would take, trip() where a trip stands, and stopTravel() stops one; the map finders pick the map to go to: findMaps(), mapAt(), mapInfo(), nearest(), nearestWith(), mapsWithin() and reachable(); answerMapLoads() tells the bot that the script asks for each new map's contents itself. The same calls work on a Full socket bot and on a MITM bot (man-in-the-middle: the bot relays the game session of a Dofus client that someone plays). They are the same movements as the bot's Map tab in Asteroboard, which calls them through the API.
import { move, changeMap, useElement, travel, route } from "asterobot:movement"; @asterobot:base exports all of them too.
Each function starts once the messages the script sent before it have left, so await send(...) followed by changeMap() sends that message before the character moves.
move()
move(cell, options?) returns Promise<number>.
Parameter Type Description cell number The cell to walk to, a whole number from 0 to 559 options.cautious boolean Walk instead of running, whatever the distance. Default false. options.timeout number How many milliseconds the server gets to grant the walk, and then to acknowledge its end once the walk is over. Default 5000. It resolves with the cell the character stands on, once the server has acknowledged the end of the walk.
try { const cell = await move(301); botInfo("Standing on cell", cell); } catch (error) { botWarn("Couldn't walk:", String(error)); } How it walks
It reads where the character and the other actors stand, as currentMap() does, and plans the route to cell the way findPath() does, around every other actor. When cell can't be reached, the route ends on the reachable cell closest to it. When there's nowhere to walk, move() resolves right away with the character's cell and sends nothing. It sends MapMovementRequest and waits for the server's MapMovementEvent granting the walk. A MapMovementRefusedEvent refuses it instead, and move() rejects at once. On a Full socket bot, it waits as long as the Dofus client takes to walk the cells the server granted, then sends MapMovementConfirmRequest. On a MITM bot the Dofus client walks the character on screen and confirms the walk itself. If it hasn't one second past that time, as when it dropped a walk granted before it showed the character on a new map, Asterobot confirms it. It resolves when the server answers with MapMovementConfirmResponse, or moves the character to another map, as walking onto an exit does. When the server grants a shorter walk than the one asked for, move() times the walk the server granted and resolves with the cell where it ends. On a Full socket bot the confirmation leaves as early as a Dofus client could send it. The path explains how that time is found, and why it can be wrong for a mounted character.
changeMap()
changeMap(side, options?) returns Promise<{ mapId: bigint, cellId: number }>.
Parameter Type Description side string The side of the map to leave by: "top", "bottom", "left" or "right" options.cell number The exit cell to leave from, which must be an exit of that side. Default: the exit of that side the character reaches soonest. options.timeout number How many milliseconds the server gets for each step: to grant and end the walk to the exit, then to change map. Default 5000. The new map's contents get 20 seconds more. It resolves with the map the character stands on and its cell there, once that map's contents have arrived, so a move() can follow at once.
const { mapId, cellId } = await changeMap("top"); botInfo("Now on map", mapId, "cell", cellId); How it changes map
It finds the exits of that side: the cells of that edge of the map whose map data lets the character leave that way (mapChangeData). From those, it takes the one the character walks to soonest, or options.cell. Not every cell of an edge is an exit, and a map split by a river or a wall can have exits the character can't reach. It walks there as move() does. The change goes on only if the walk ends on that exit cell. It sends MapChangeRequest with the map the map data names beyond that side. It waits for MapCurrentEvent, or MapCurrentInstanceEvent for an instanced map, the server putting the character on the next map, then for that map's MapComplementaryInformationEvent, which places the character. The server sends it once ContextReadyRequest and MapInformationRequest have asked for it: see Full socket and MITM bots. Which map the character arrives on is the server's choice, and the promise resolves with that one. MapChangeRequest carries the map the map data names beyond that side, as the Dofus client sends it, but on some maps the server sends the character somewhere else depending on the exit cell. That's what the next section is about.
Choose the exit
On most maps, every exit of a side leads to the same map, and changeMap(side) is all you need. On a few maps, one side leads to different maps depending on where the character leaves it: the top of the right side to one map, the bottom of the right side to another. To go to a given one, leave from one of its cells with options.cell.
The exits of a side are the cells of that edge whose map data lets the character leave that way: the cell's mapChangeData holds one bit per direction. A script reads them from the map's cells in the game data, with the same rule the Dofus client applies when the player clicks towards a side:
import { record } from "asterobot:gamedata"; // The cells a character may leave a map from, towards one side. function exits(mapId, side) { const cells = record("maps", Number(mapId))?.cells_data ?? []; return cells.flatMap((cell, id) => (isExit(id, cell.mapChangeData ?? 0, side) ? [id] : [])); } // The bits are the 8 directions (1 right, 2 bottom right, 4 bottom, 8 bottom // left, 16 left, 32 top left, 64 top, 128 top right). A corner cell also // leaves by the diagonal that points off the map on that side. function isExit(id, bits, side) { switch (side) { case "top": return id < 28 && ((bits & 64) !== 0 || (id < 14 && (bits & (32 | 128)) !== 0)); case "bottom": return id >= 532 && ((bits & 4) !== 0 || (id >= 546 && (bits & (2 | 8)) !== 0)); case "right": return id % 14 === 13 && ((bits & 1) !== 0 || ((id + 1) % 28 === 0 && (bits & (2 | 128)) !== 0)); case "left": return id % 14 === 0 && ((bits & 16) !== 0 || (id % 28 === 0 && (bits & (8 | 32)) !== 0)); default: return false; } } Which exit leads where is in the game's travel graph, the one the Dofus client plans trips with, in the tables world_vertices (one row per walkable part of a map) and world_transitions (one row per way out of it). Walking off a side is type 1 or 2, and direction is the side: 0 right, 2 bottom, 4 left, 6 top. When a side leads to several maps, each of its rows has a criterion that lists the cells it's taken from, such as #(PC=447|PC=475|PC=503|PC=531), where PC is the cell the character stands on.
import { botInfo, currentMap } from "asterobot:bot"; import { query } from "asterobot:gamedata"; const result = await query( `SELECT t.direction, t.criterion, destination.map_id FROM world_transitions t JOIN world_vertices origin ON origin.uid = t.from_uid JOIN world_vertices destination ON destination.uid = t.to_uid WHERE origin.map_id = ? AND t.type IN (1, 2)`, [Number(currentMap().mapId)], ); // undefined when the bot's game version has no game data. for (const [direction, criterion, destination] of result?.rows ?? []) { botInfo("Side", direction, "leads to", destination, criterion || "from every exit"); } On map 112460037 of game version 3.7.11.11, the right side has two rows: one to map 112461062 with #(PC=27|PC=55|PC=83|PC=97|PC=546|PC=41), one to map 112461060 with #(PC=447|PC=475|PC=503|PC=531|PC=517|PC=489|PC=461|PC=433). exits(112460037, "right") gives 27, 41, 55, 69, 83, 223, 237, 251, 265, 279, 475, 489, 503, 517 and 531. So to reach 112461060, leave from an exit that's also in its list, such as 475:
const { mapId } = await changeMap("right", { cell: 475 }); The tables and the rule can change with a game update. In Asteroboard, the same choice is a right-click on an exit cell of the bot's Map tab.
useElement()
useElement(element, options?) returns Promise<{ mapId: bigint, cellId: number, skillId: number, duration: number, canMove: boolean }>.
Parameter Type Description element number The element's id, from currentMap().elements options.skill number The skill id of the action to perform, among those the element offers now. Default: the element's only action. An element that offers several rejects without one. options.timeout number How many milliseconds the server gets for each step: to grant and end the walk to the element, then to answer the use, or to change map for an exit. Default 5000. It resolves once the server has accepted the use, with:
Field Description mapId, cellId Where the character stands: on the same map, or on the next one after an exit skillId The skill id of the action performed. 339, Point Out an Exit, for an exit. duration How many milliseconds the use lasts, such as a harvest. 0 for an instant use. canMove Whether the character may walk while the use lasts const map = currentMap(); const door = map.elements.find((element) => element.skills.some((skill) => skill.id === 184)); if (door) { await useElement(door.id, { skill: 184 }); } What follows the use is yours to wait for: the new map of a door that leads indoors (MapCurrentEvent), the destinations of a zaap, the end of a harvest.
How it uses an element
It finds the element's cell in the map data and picks the action: options.skill, or the element's only action. Directional signs, which are read and not used, are never picked. It reads the distance the element is used from in the game data's skills table, as the Dofus client does: the smallest range among the element's actions, or 1 as soon as one of them has use_range_in_client off. When the character is further away, it walks to the cell the Dofus client would walk to, next to the element on the character's side for a distance of 1. It sends InteractiveUseRequest with the element id and the action's instance uid, and resolves when the server's InteractiveUsedEvent names the character and that element. Exits are walked onto, not used. An element whose only action is Point Out an Exit (skill 339) is an exit to another map, drawn as a sun on its cell. The Dofus client doesn't send InteractiveUseRequest for it: it walks onto the cell, and the server moves the character. useElement() does the same, and resolves on the next map as changeMap() does.
While a timed use lasts (duration above 0), the character can't use another element: useElement() rejects until the server ends that use, with InteractiveUseEndedEvent for that element or a StatedMapUpdateEvent, or for one second at most past its duration.
travel()
travel(mapId, options?) returns Promise<{ mapId: bigint, cellId: number }>.
Parameter Type Description mapId number, BigInt or string The map to go to, such as currentMap().id, a map id from asterobot:gamedata, or one the map finders give options.cell number The cell to walk to once on the map, or the reachable cell closest to it. Default: where the last leg leaves the character. options.risky boolean Legs whose condition Asterobot can't check: true takes them whenever they save more than 10 legs, false never takes them. Default: only when no other way leads there. options.timeout number How many milliseconds the server gets for each step of each leg, as for changeMap() and useElement(). Default 5000. options.onLeg function Called before each leg with the leg, its index and the number of legs. Returning false stops the trip there. It resolves once the character stands on the map, and on options.cell when it names one, with where the character stands.
const bonta = 212600323; await travel(bonta, { onLeg: (leg, index, total) => botInfo(`${index + 1}/${total}: ${leg.kind} to map ${leg.toMapId}`), }); How it travels
The game ships a travel graph of the world, which the Dofus client uses for its mount's autopilot: every map, and every way from one map to another. travel() routes over it, with the fewest map changes, and takes the route leg by leg, each leg one of these:
Leg How it is taken A side of the map As changeMap() does. When a side leads to different maps from different exit cells, the leg leaves from the cells of its map. A cell that leads elsewhere, such as stairs or a sun Walked onto, as useElement() does for an exit An element, such as a door, a ladder or a teleporter Used with the action the graph names, as useElement() does, then the next map is awaited The trip follows its route as long as each leg leaves the character where the route said, and routes again otherwise:
a leg that leaves the character somewhere else is a detour, not a failure; a leg the server refuses, or the map doesn't allow (no exit to walk to, an element missing or offering another action), is left out of the routes that follow, and the bot's Console says so; a fight that starts on the way is waited out: once the character stands on a map again, won or lost, the trip routes from there; at the 8th refused leg, or the 8th leg that ends off the route, the trip gives up. On a map the graph doesn't have, such as a house or a dimension, the trip first leaves it: by an exit drawn as a sun, a side of the map, then a door whose action is Leave, until it stands on a map of the graph.
Conditions
Some legs carry a condition: a quest, an item, a level, a breed. Asterobot checks the ones it knows the answer to, from what the game told the bot about its character:
Code What it checks Known from PL, PG Level, breed The character's selection, its level ups PK Kamas The inventory, kamas updates PO An item carried (id and quantity), E worn The inventory, quantity updates Qa, Qf, Qo A quest under way, a quest finished, an objective of the step under way The quest book, quest steps, objective updates OA An achievement done The achievement list Pj A job above a level Job experience updates CE Energy (a ghost has none) The characteristics SC The server's type The server's settings Pm The map the leg leaves The graph itself A leg whose condition can't be checked yet, because the game never said or because only the server knows, is unsure: the trip takes it only when no other way leads there (see options.risky), and the server decides. Items gained and quests started or finished during the session are only known once the game lists them again.
Estimates
Each leg carries durationMs: the walk across its map, from where the character lands to the leg's exit, cell or element, at the speed the Dofus client walks, plus 1.5 seconds for the map change and half a second for an element. Where the character lands after stairs or a door can't be told, so such a walk counts 3 seconds. A trip of 70 maps comes out at about 5 minutes.
route()
route(mapId, options?) returns Promise<Array<Leg>>: the legs travel() would take now, from where the character stands, without moving. It takes the same options as travel(), but timeout and onLeg, and resolves with no legs when the character already stands on the map. Each leg is:
Field Description kind "side", "cell" or "element" fromMapId, toMapId The map the leg leaves, and the map it leads to, as BigInts side For a side: "top", "bottom", "left" or "right" cells For a side that leads elsewhere from other cells: the exit cells the leg leaves from. Absent for any exit. cellId For a cell: the cell walked onto. For an element: the cell it stands on. elementId, skillId For an element: the element, and the skill id of the action criterion The condition the game puts on the leg, in its own language, such as Qf=2192. Empty for none. unsure Whether Asterobot couldn't check that condition durationMs How long the leg should take, in milliseconds const legs = await route(212600323); const minutes = legs.reduce((total, leg) => total + leg.durationMs, 0) / 60000; botInfo(`Bonta is ${legs.length} maps away, about ${Math.round(minutes)} minutes`); trip() and stopTravel()
trip() returns the bot's trip under way, or its last one, whoever started it (this script, another one, or Asteroboard), and undefined before any:
Field Description mapId, cellId Where the trip goes state "travelling", "fighting" while it waits for a fight to end, then "arrived", "failed" or "stopped" legs The route as planned now: the legs taken, then the ones to come leg The index in legs of the leg under way, or legs.length once arrived refused The legs the server or the map refused error Why the trip failed or stopped stopTravel() stops the trip under way at the end of its leg under way, so the character never stops halfway through a map change, and returns whether a trip was under way. The travel() it stops rejects with move: the trip was stopped.
Find a map
travel() takes a map id. These functions find one; each returns a promise.
Function Resolves with findMaps(query) The ids of the maps a query picks, from the game data. query takes x, y and worldMap (coordinates on a world map, 1 being the world of the Twelve), subArea, area, monster (a monster living in the map's subarea), resource (an item harvested there, such as 289 for wheat) and zaap (true for maps with an active zaap). Every field you give narrows the maps, 0 included: { area: 0 } is Amakna. A field you leave out doesn't. mapAt(x, y, worldMap?) The map at coordinates, on the world of the Twelve by default, or undefined. A map the world graph has comes first, as indoor and underground maps share coordinates. mapInfo(mapId) { mapId, x, y, worldMap, subAreaId, subArea, areaId, area, zaap, inWorldGraph }, names in the bot's language, or undefined for a map the game data doesn't have nearest(mapIds, options?) { mapId, legs, durationMs } for whichever map is the fewest legs away now, or undefined when none can be reached. options.risky as for travel(). nearestWith(query, options?) nearest() over the maps findMaps(query) picks mapsWithin(mapId, legs, options?) The maps at most that many legs away, nearest first, the map itself included reachable(mapId, options?) Whether a trip can get there now // To the nearest zaap. const zaap = await nearestWith({ zaap: true }); if (zaap) await travel(zaap.mapId); // Every map within 3 legs where wheat grows. const wheat = new Set((await findMaps({ resource: 289 })).map(String)); const around = (await mapsWithin(currentMap().id, 3)).filter((id) => wheat.has(String(id))); Full socket and MITM bots
Full socket bot MITM bot Who confirms a walk these functions make asterobot:movement, after the time the Dofus client takes to walk it The Dofus client, or asterobot:movement when the client hasn't one second past that time Who asks for the next map's contents after a map change (ContextReadyRequest and MapInformationRequest) The script that called answerMapLoads(), such as @asterobot:base, however long it takes. Without one, changeMap(), useElement() or travel() sends them itself, 0.4 to 1.2 seconds after the map change, as the Dofus client does. The Dofus client, which loads the map So a map change completes in every case, with or without a script, with or without base, and the two requests are sent once, on every PC: by the script that answers map loads, by Asterobot otherwise.
On a MITM bot, changeMap(), useElement() and each leg of travel() that changes map resolve 0.4 to 1.2 seconds after the new map's MapComplementaryInformationEvent, the time the Dofus client takes to show the character on it. The Dofus client drops a walk granted before, so the next walk waits.
answerMapLoads()
answerMapLoads() returns nothing. Call it once, when the script starts answering MapCurrentEvent and MapCurrentInstanceEvent with ContextReadyRequest and MapInformationRequest: from then on, and until the script stops, the map changes these functions make leave those two requests to the script, however long it takes to send them. If the script stops before sending them, Asterobot sends them itself.
A script that answers map loads without calling it races Asterobot, and on a slow PC both send the two requests. Base calls it for you. On a MITM bot it changes nothing: the Dofus client loads every map.
This only covers the map changes these functions make. A Full socket bot with no script at all is not in the game world: identifying, choosing the character, entering the world and answering the server's polls are a script's, base's when it plays.
One at a time
A bot does one walk, map change, use or trip at a time, whoever asks for it. While one is under way, another move(), changeMap(), useElement() or travel() rejects at once and sends nothing, and so does an action started from the bot's Map tab in Asteroboard. A trip counts as one from its first leg to its last: stop it first with stopTravel() or Asteroboard's Stop. Await each call before starting the next. route(), trip() and the map finders move nothing and can be called at any time.
They also refuse to start while the character is still walking a walk they didn't ask for, such as one the player started on a MITM bot. That walk counts until the server acknowledges its end, or for one second at most past the time the Dofus client takes to walk it.
What the character does belongs to the bot, not to your script. If the script stops in the middle, the promise rejects with the reason the script stopped, and the walk, map change or use still goes on to its end, so the server doesn't keep the character walking. A trip stops at the end of the leg under way.
Doing something else in the meantime
These functions don't hold up the rest of your script. Other code keeps running while the character walks, and on a Full socket bot the confirmation still leaves on time. To send a chat message while walking, start both together:
const [cell] = await Promise.all([ move(301), send("ChatChannelMessageRequest", { channel: "GLOBAL", content: "Hello" }), ]); Don't keep a promise in a variable to await later. If it rejects before your code reaches the await, nothing catches it and the script stops with unhandled Promise rejection:. Await it, put it in a Promise.all, or give it a .catch().
Doing it yourself
A script can make these movements by hand with asterobot:protocol, to do them its own way. Asterobot then takes no part in them: it confirms none of their walks, asks for no map, and doesn't count them in the one-at-a-time rule, so don't mix them with these functions' movements on the same bot. The script sends what the Dofus client would, with findPath() for the routes and currentMap() for the actors and elements:
Movement What to send and wait for Walk Walk along a path: MapMovementRequest, then MapMovementEvent or MapMovementRefusedEvent, then on a Full socket bot MapMovementConfirmRequest and its MapMovementConfirmResponse. Map change by a side Walk onto an exit cell of that side (Choose the exit) and wait for MapMovementConfirmResponse. Then send MapChangeRequest with mapId, the map's top_neighbour_id, bottom_neighbour_id, left_neighbour_id or right_neighbour_id from record("maps", mapId), and wait for MapCurrentEvent. Loading the new map, on a Full socket bot Answer MapCurrentEvent, and MapCurrentInstanceEvent, which the server sends instead for an instanced map, with ContextReadyRequest, then MapInformationRequest, both with its mapId, as @asterobot:base does. The map's contents then arrive with MapComplementaryInformationEvent. On a MITM bot the Dofus client does it. Exit drawn as a sun Walk onto its cell. The server answers the end of the walk with MapCurrentEvent, and the new map loads as above. Use an element Walk within reach, then send InteractiveUseRequest with the element's elementId and the chosen action's skillInstanceUid from currentMap().elements. The server answers with InteractiveUsedEvent or InteractiveUseErrorEvent, and a use with a duration ends with InteractiveUseEndedEvent. The Dofus protocol reference describes every field of these messages.
Errors
A mistake in the arguments rejects the promise with a TypeError, and nothing is sent:
Error Cause movement.move cell must be an integer cell id cell isn't a whole number in the 32-bit range, or is a BigInt or a string movement.move cell: pathfinding: not a map cell, cell ids go from 0 to 559 cell is outside 0 to 559 movement.changeMap side must be "top", "bottom", "left" or "right" side is none of the four movement.changeMap options.cell must be an integer cell id, and movement.changeMap options.cell: pathfinding: not a map cell, cell ids go from 0 to 559 options.cell isn't a cell id movement.useElement element must be an integer element id element isn't a whole number in the 32-bit range movement.useElement options.skill must be an integer skill id options.skill isn't a whole number movement.travel mapId must be an integer Number, BigInt or decimal string, and the same for movement.route mapId isn't a whole number movement.travel options.cell must be an integer cell id, and movement.travel options.cell: pathfinding: not a map cell, cell ids go from 0 to 559, and the same for movement.route options.cell isn't a cell id movement.<function> options must be an object options is set, not to null or undefined, and isn't an object movement.<function> options has no property "<name>" options has a key the function doesn't take movement.move options.cautious must be a boolean cautious is set to something other than true, false, null or undefined movement.travel options.risky must be a boolean, and the same for the other functions that take it risky is set to something other than true, false, null or undefined movement.travel options.onLeg must be a function onLeg is set to something other than a function, null or undefined movement.findMaps query must be an object, movement.findMaps query.<name> must be an integer The query isn't an object, or a field isn't a whole number movement.mapAt x and y must be integers, movement.mapAt worldMap must be an integer A coordinate isn't a whole number movement.nearest mapIds must be an array of map ids mapIds isn't an array movement.mapsWithin legs must be a whole number of legs legs isn't a whole number from 0 movement.<function> options.timeout: timeout/milliseconds must be a finite positive Number timeout isn't a number above 0 A movement that is refused or fails rejects with an error whose message is one of these texts. travel() rejects with them too, for a leg that fails other than by a refusal its route goes round, and "all three" includes it:
Text Returned by Cause move: the bot isn't connected to a game server all three The bot has no game connection. Nothing is sent. move: this bot has no game data to route on all three The bot's game version has no game data. Nothing is sent. move: the character is already moving; await the walk, map change or use in progress first all three Another walk, map change or use of the bot is under way, from a script or from Asteroboard. Nothing is sent. move: the character is still walking all three The character hasn't finished a walk it didn't ask for. Nothing is sent. move: the character's map and cell aren't known yet all three Asterobot hasn't seen the character's map yet, or where the map places the character, for example right after the character enters the game. Nothing is sent. move: the game has no path from cell <cell> to cell <cell> all three The Dofus client itself would find no route. Nothing is sent. move: the server didn't grant the walk in time all three Neither MapMovementEvent nor MapMovementRefusedEvent answered the walk within timeout move: another walk of the character started before this one ended all three A new walk of the character started first, or its walk was cancelled, for example because the player clicked elsewhere on a MITM bot move: the server didn't acknowledge the end of the walk in time all three No MapMovementConfirmResponse came within the walk's time plus timeout. On a MITM bot the timeout counts from Asterobot's own confirmation, one second past the walk's time, when the Dofus client hasn't confirmed by then move: the game session closed during the walk all three The game connection closed first move: the walk didn't end on the cell it was for: it ended on cell <cell>, not on cell <cell> changeMap(), useElement() on an exit The server granted a shorter walk, so the character isn't on the exit. Nothing more is sent. move: the map data names no map beyond that side changeMap() That side leads nowhere. Nothing is sent. move: no exit leads off that side of the map changeMap() That edge has no exit cell. Nothing is sent. move: that cell isn't an exit of that side: cell <cell> changeMap() options.cell isn't one of that side's exits. Nothing is sent. move: no exit of that side can be walked to from here (cell <cell>) changeMap() Every exit of that side is out of the character's reach, for example beyond a river. Nothing is sent. move: the server didn't change map in time; the character is still on map <mapId> changeMap(), useElement() on an exit Neither MapCurrentEvent nor MapCurrentInstanceEvent came within timeout move: the server refused the map change; the character is still on map <mapId> changeMap() The server answered MapChangeRequest with MapMovementRefusedEvent move: the character changed map, but the new map's contents never came changeMap(), useElement() on an exit The character is on the next map, but no MapComplementaryInformationEvent came within 20 seconds of it, although MapInformationRequest was sent move: the server refused the walk move(), changeMap(), useElement() on an exit The server answered the walk with MapMovementRefusedEvent. useElement() on any other element sends the use anyway, from the cell the refusal names, as the Dofus client does. move: the character is still using an element useElement() A timed use of the character's hasn't ended. Nothing is sent. move: no such interactive element on this map: <element> useElement() The element isn't in currentMap().elements. Nothing is sent. move: the game data doesn't place this element on the map: <element> useElement() The map data has no cell for that element. Nothing is sent. move: the element offers no action right now: element <element> useElement() The element offers nothing now, such as a resource already harvested. Nothing is sent. move: the element doesn't offer that action right now: skill <skill>; it offers skills <skill>, <skill> useElement() options.skill isn't among the element's actions now. Nothing is sent. move: the element offers several actions; name the one to use: skills <skill>, <skill> useElement() The element offers several actions and options.skill is missing. Nothing is sent. move: the server refused the use (element <element>) useElement() The server answered with InteractiveUseErrorEvent move: the server didn't answer the use in time (element <element>) useElement() Neither InteractiveUsedEvent nor InteractiveUseErrorEvent answered within timeout travel: the world graph doesn't have this map: map <mapId> travel(), route() The trip goes to a map the travel graph doesn't have, such as a house, a haven bag or an instance, or route() starts from one. Nothing is sent. travel: no known way there (map <mapId> to map <mapId>) travel(), route() No way the trip may take leads there: it needs a zaap, an NPC transport, or a leg whose condition Asterobot can't check without options.risky. For travel(), nothing more is sent. move: too many legs of the trip were refused; the last one: <error> travel() The server or the map refused 8 legs, the last one with that error move: the trip keeps ending up off its route: on map <mapId> travel() 8 legs left the character somewhere their route didn't lead move: the trip was stopped travel() stopTravel(), Asteroboard's Stop, or options.onLeg returning false stopped it move: nothing leads off this map to a map the world graph has (map <mapId>) travel() The trip started on a map the graph doesn't have, and none of its exits, sides or doors that leave worked behavior resource limit exceeded: maximum pending operations reached all 32 operations are pending, and each call counts as one until it ends. Nothing is sent. They also reject with the texts of findPath() when the game data can't be read, such as failed to load map <mapId> for pathfinding: datacenter: no such record: maps/<mapId>. asterobot:pathfinding lists them.
String(error) gives TypeError: or GoError: followed by the text.
Limits
They move the character outside fights, like asterobot:pathfinding. changeMap() goes to the next map only. travel() goes as far as the travel graph leads, without zaaps, zaapis, the haven bag or potions for now. NPC transports, such as statues and boats, are never used: the game doesn't say where they lead. A trip can't yet go round maps whose monsters attack: the game data doesn't say which do. useElement() leaves out the house instance a house door can take, as the Dofus client does on a plain click. Walk along a path shows the messages move() exchanges, for a script that walks by hand. Harvest and move on is a complete script that harvests a route of maps, with recipes for doors, suns, zaaps and exits.
  • 128 vues
The official package that does what the Dofus client does by itself, starting as soon as a script loads it, and the standard library packages build on: moving, the Studio's plans, steps and conditions for gathering, fighting, resting and equipment, and 24 fight profiles.
@asterobot:base is the official package that does what the Dofus client does by itself, so that your package never has to:
it identifies the game connection, picks a character and enters the world; it keeps the connection alive the way the client does, with its pings and its answers to the server's checks and polls; it tells the server the character is ready on every map it enters. It starts itself as soon as a script loads it, and your package carries on from a character standing on its map.
It's also the standard library packages build on:
moving, from asterobot:movement, re-exported; playing the plans drawn in Asteroboard's Studio; steps and conditions for gathering, fighting, resting, eating and equipment, for plans and scripts alike; 24 fight profiles, ready to play. You don't install it: Asterobot downloads it from asterobot.net by itself, keeps its newest version in every library, and loads that version for every package that depends on it, whatever version the package locked. See Base is always the newest.
Add it to a package
Declare @asterobot:base in your package's dependencies, as Dependencies shows, then import it by that name:
import { connect } from "@asterobot:base"; export default async function behavior() { void main(); } async function main() { const map = await connect(); // The character stands on map.mapId: your script starts here. } Importing base is all it takes. It starts as it loads, before your own code runs, and connect() only waits for the world. The entry function doesn't wait for it, so Play returns right away and main() goes on in the background.
A package loads base when one of its files imports it, and so does every package that depends on that one, at any depth. Listing base in dependencies without importing it loads nothing.
@asterobot:base can also be played on its own: it brings a bot into the world and keeps it there. Such a bot plays the newest version of base too, whatever version it was set to.
The four cases
Full socket bot MITM bot Played on its own Identifies, selects a character, enters the world, then answers everything the server asks Sends nothing, and waits for the person's character to be on a map As a dependency The same, as soon as the script loads base The same Base tells the two kinds of bot apart by session.shared, so there's nothing to configure. A package using base is the same code in all four cases.
Base starts at the top level of its index.js. Asterobot also runs that code while it reads a package's settings, without a game session, and base doesn't start then. See Declarations.
Exports
Export Returns What it does connect() Promise of the map Waits until the character stands on its map. latency() number The latency the bot reports to the server, in milliseconds. move(), changeMap(), useElement(), travel(), route(), stopTravel(), trip() Moving, from asterobot:movement, re-exported. After a map change, base asks for the new map, as for every map. findMaps(), mapAt(), mapInfo(), nearest(), nearestWith(), mapsWithin(), reachable() The map finders of asterobot:movement, re-exported: see Find a map. run(), stop(), plan() Play a plan drawn in the Studio, and stop it: see Plans. levelAtLeast(), kamasAtLeast(), lifeBelow(), bagsAbove(), inFight(), hasItem(), jobLevelAtLeast(), onMap(), inSubarea(), inArea(), harvestsAtLeast(), diedInLastFight(), fightsAtLeast(), wears() boolean, or a Promise of one Yes/no questions about the character: see Conditions. gather(), fightMonsters(), fightSettings(), rest(), eat(), equip(), unequip() A Promise, nothing for fightSettings() Gathering, fighting, resting, eating and equipment: see Steps. steps, conditions objects The declarations of every step and condition above, which the Studio offers in every package that depends on base. connect()
connect() resolves with what currentMap() returns once the character is placed on its map, at once if it already is. Base itself calls it as it loads, which starts everything. Every later call returns the same promise, so any number of packages on the bot can call it.
When it fails, the reason goes to the bot's Console as an error line, and the promise rejects with it too. Stopping the script, or losing the connection, rejects it without an error line.
What base does by itself
On a Full socket bot, for as long as the script runs:
Proof of identity. It answers ServerSessionReadyEvent, ServerVerificationEvent and ServerChallengeEvent with the values of asterobot:verification, as soon as each event arrives. The answers go out with a uid, like the client's. It answers a challenge with a value and one without, and both are checked against the real client. Polls. It answers each SequenceNumberEvent with the number of polls received on the connection, and each BasicLatencyStatsEvent with latency(). The server sends the two together, every 4 minutes, and base answers them in the Dofus client's order: the sequence, then the latency. Pings. With the ping setting on active, it sends a PingRequest on about half of the 5-second ticks once the character has loaded, as a client does while someone uses it. Maps. It answers each MapCurrentEvent, and each MapCurrentInstanceEvent, the form the server uses for an instanced map, with ContextReadyRequest and MapInformationRequest carrying the map's mapId, after the time the Dofus client takes to load the map: 1.2 to 2.4 seconds for the first one, then 0.4 to 1.2 seconds. The server sends nothing of the new map until both requests arrive. On the first map it sends AnomalySubareaInformationRequest too. After a changeMap(), an exit's useElement(), or each map of a travel(), these two requests are base's alone: base tells Asterobot so with answerMapLoads(), and those functions never send them too, however long base takes. And once, to get into the world:
Identifying. A refused ticket makes connect() reject. The ticket is then spent: only a new sign-in brings another. Selecting a character. Base picks the character the character setting names, or the first of the list, after a pause of 1.5 to 3.5 seconds. connect() rejects when no character has that name, naming the ones there are, and when the server refuses the selection. Entering the world. Base sends what the client sends once the character has loaded, then waits for the map. latency()
The mean of the bot's last 50 ping round trips, in milliseconds, rounded as the Dofus client rounds it, or 0 before the first. The round trips are kept in the bot's memory, so a script started again goes on from the same ones.
Plans
A plan is a file of a package, drawn in Asteroboard's Studio: places on the world map, the order the bot goes round them, and the steps it runs at each. A script plays one with run(), as the index.js that Start with the Studio writes does:
import { run } from "@asterobot:base"; import main from "./plans/main.js"; export { parameters } from "./plans/main.js"; // the plan's settings export default async function behavior() { void run(main); } Export Returns What it does run(plan, options?) Promise Plays a plan, the default export of a plan file. Resolves when its circuit ends or is stopped, rejects with what failed. options is reserved: nothing reads it yet. stop() boolean Asks the plan being played to stop cleanly. Says whether one was. plan(data, refs) The plan What a plan file calls as it's imported, never by hand. It never throws, since it also runs while Asterobot reads the package's declarations. run() first waits for the world, with connect(). It then checks the whole plan, and every plan it plays through interrupts and plan steps: every field, place, package, function, argument and setting. On any problem it rejects before the bot takes a step, listing every problem, each with its block's label and id. Then it walks the circuit, lap after lap.
One plan at a time. A run() while another plan plays rejects with base: a plan is already running. The plans a plan plays itself don't count. Stopping cleanly. A stop comes from stop(), from a stop step, or from Asterobot asking the script to stop, such as at the end of a schedule's time slot. The step under way finishes, and a long one sees context.stopping turn true. A wait ends at once, a trip ends at the end of its leg, then run() resolves. A failure rejects run(). Nothing catches a void run(main), so a plan that fails stops the script, and the bot's page shows why. The error names the block, then the cause: base: Astrub farm › 2 Astrub fields (k3f9) › Fight (p2x1) failed 3 times: no monster to fight. Where it is. While a plan plays, the runner keeps where it is in the bot's memory, under @asterobot:base/plan: { plan, stop, step, map, lap, connectionId }. The Studio explains what a player draws: the order, the failures and retries, the interrupts, the plan's settings and the console lines. The editor explains the plan files, and base's README describes the plan format.
Conditions
Each condition is a function base exports and declares in its conditions, so the Studio offers it in every package that depends on base. From code, each takes its arguments as one object, as the Studio passes them:
import { bagsAbove, levelAtLeast } from "@asterobot:base"; if (levelAtLeast({ level: 50 }) && bagsAbove({ percent: 90 })) { // … } Condition Arguments True when levelAtLeast level (1 or more) The character's level is at least level. kamasAtLeast kamas (0 or more) It carries at least kamas kamas. lifeBelow percent (1 to 100) Its life is below percent of its maximum. bagsAbove percent (1 to 100) Its bags are fuller than percent of what they can carry. inFight A fight is under way. hasItem item, count (1 by default) It carries at least count of item, worn or not. wears item It wears item. jobLevelAtLeast job, level (1 or more) That job is at least level. A job it doesn't have is level 0. onMap map It stands on that map. inSubarea subarea It stands in that subarea. Returns a Promise. inArea area It stands in that area. Returns a Promise. harvestsAtLeast count (1 or more) It has made at least count harvests with gather() since the script started. fightsAtLeast count (1 or more) It has fought at least count fights since the script started. diedInLastFight It died in its last fight. False before any. Items, jobs, maps, subareas and areas are game data ids, which the Studio searches by name. The conditions read what Asterobot follows: asterobot:character, currentMap() and mapInfo(). The two fight conditions count every fight from the moment base starts, from the fight module's onFight(), whoever started the fight.
A fact the game hasn't told yet is never guessed. The condition throws, naming it, such as the level isn't known yet or the jobs aren't known yet, and in a plan that fails the block it guards. By the time connect() resolves, the game has sent the character, its characteristics and its inventory.
Steps
Each step is a function base exports and declares in its steps. From code, each takes its arguments as one object, as the Studio passes them, and resolves once it's done:
import { fightMonsters, fightSettings, gather, rest } from "@asterobot:base"; fightSettings({ profile: "@asterobot:base/careful" }); await fightMonsters({ monsters: [4109], maxLevel: 30 }); await rest({ upTo: 90, eat: true }); await gather({ resources: [303], bagsAbove: 90 }); // 303 is Ash Wood A step with nothing to do on the map resolves { nothingToDo: true }, with retryAfter: 60 for gather() and fightMonsters(), since resources grow back and monsters come back. A fight under way when a step starts, or one that comes during gather(), rest() or eat(), such as an aggression, is waited for while the fight module plays it. A step fails with the character died in the fight when the character dies in it.
gather()
Harvests the map's resources that match, one after another, with the job's own action, as the Dofus client does when the player clicks one: the nearest first, or the highest level first. It ends when nothing more matches, after maxHarvests harvests, or once the bags are fuller than bagsAbove. An element whose use fails, taken by someone else or out of reach, is tried once more, then given up.
Argument Default Gathers resources none What gives these items, such as 289, Wheat. With jobs, what either asks for. jobs none Every resource of these jobs. Neither resources nor jobs: every resource of the map. avoid none Never what gives these items. myLevel true Only what the character's level in the job allows. The server would refuse the rest. order nearest nearest first, or the resource of the highest level first. maxHarvests 0 Ends the step after this many harvests on the map. 0: everything that matches. bagsAbove 0 Ends the step once the bags are fuller than this share, 0 to 100. 0: never. fightMonsters()
Attacks the map's monster groups that match, the nearest first, one after another, and waits for each fight's end, which the fight module plays. On a Full socket bot it switches the module on while the script runs, when it's off: a fight can't go on without it. On a MITM bot the switch stays the person's. When no fight starts within 5 s, the group walked away: base walks to its new cell and attacks again, and gives a group up after three attacks.
Argument Default Keeps the groups monsters none With at least one of these monsters. None: any group. avoid none With none of these monsters. races none With a monster of one of these races. None: any race. minLevel 0 Whose level, the sum of its monsters' levels as the game shows it, is at least this. maxLevel 0 Whose level is at most this. 0: no limit. maxMonsters 0 With at most this many monsters, 0 to 8. 0: no limit. maxMonsterLevel 0 With no monster above this level. 0: no limit. archmonsters avoid Without an archmonster (avoid), with or without one (allow), or only with one (only). maxFights 0 Ends the step after this many fights on the map. 0: every group that matches. The groups and their monsters are currentMap()'s actors and their monsters.
fightSettings()
Sets how the fight module plays while the script runs, as asterobot:fight does, and returns at once. What it leaves at keep, or an empty profile, stays as the bot's Fight tab says.
Argument Values Default module keep, on, off. On a MITM bot the switch stays the person's. keep profile A fight profile's name, as the Fight Profiles page shows it. empty speed keep, slow, normal, turbo keep rest() and eat()
rest() sits the character down with the sit emote, EmotePlayRequest { emoteId: 1 }, until its life is back to upTo percent of its maximum, then stands it up with the same emote. With eat, it eats first. It fails with the character's life isn't coming back when life doesn't change for a minute, and with the character's life isn't known yet before the game tells it.
eat() eats from the bags until life is back to upTo: a stack whose game data effects give life back, such as breads and potions, the one healing most without passing what's missing first. It sends ObjectUseRequest { objectUid }, one at a time, at a person's pace. Food the server doesn't take within 5 s is given up for the rest of the step.
Argument Default What it does upTo 100 Stops once life reaches this share of its maximum, 1 to 100. eat false rest() only: eats from the bags first, which is faster. food none Eats only these items. None: any food that gives life back. Life is character().life, which follows the life coming back, faster sitting down, and the food is its items().
equip() and unequip()
equip({ items }) puts each item on, one after another at a person's pace, in a free slot it can go in, else in its first slot, which sends the item there back to the bags. One already worn is left as it is. unequip({ items }) takes them off, or everything the character wears with none given. The slots come from the game data, so a ring goes on either hand. Both send the Dofus client's ObjectSetPositionRequest, and the server confirms with ObjectMovementEvent.
They fail for an item that isn't in the bags (item 1439 isn't in the bags), one that can't be worn (item 1439 can't be worn), and one the server didn't move within 5 s (the game didn't move item 1439).
Fight profiles
Base ships 24 fight profiles, as files of its fightprofiles folder. Every bot can play them, whether or not it plays base, by their name @asterobot:base/<name>, such as @asterobot:base/careful or @asterobot:base/ranged-kite. None of them names a spell, so each plays any class. The guide lists them in Ready-made profiles in base.
A script plays one with setProfile() or fightSettings({ profile: "@asterobot:base/careful" }). Their names stay the same from one version of base to the next.
Settings
@asterobot:base has its own card on the bot's Settings tab, whichever package uses it: see Settings in dependencies.
Setting Values Default What it changes character A character's name Empty The character to play, whatever its capitals. Empty plays the first of the list. community server, french, english, spanish, portuguese, german server The chat community the character joins on entering the world. server takes the one the server gives. ping active, idle active active pings like a person using the client, which gives the server a real latency. idle never pings, like a client no one touches, and keeps reporting the last latency it measured: 0 if it never pinged. bringUp faithful, minimal faithful faithful sends everything the Dofus client sends once the character has loaded: contacts, guild, chat community, the shop's token, the steps of the quests under way and the day's average prices. minimal sends only ContextCreationRequest, the one message needed to reach the map. Started again on the same connection
Every step records how far it got in the bot's memory, under @asterobot:base/connection, next to session.connectionId. A script started again on the same connection, after Stop then Play or a Run from the editor, carries on from there: it doesn't identify a second time, keeps the character already chosen, and goes on counting the server's polls where the last script left them. After a reconnection, the connection has a new id, and everything starts over.
Reacting to what base answers
A message goes to every handler that listens for it, so base answering a message doesn't take it away from your package. Your own on() and wait() get every message base answers, MapCurrentEvent and MapCurrentInstanceEvent included, on both kinds of bot. This script logs each map change, whichever of the two announces it, then the cell the character lands on:
import "@asterobot:base"; import { currentMap, botInfo, botWarn } from "asterobot:bot"; import { on, wait } from "asterobot:protocol"; async function logArrival(traffic) { const mapId = traffic.payload?.mapId; if (mapId === undefined) return; botInfo("Heading to map", mapId); try { // Its contents come once the map has loaded, a moment after this. await wait( (next) => next.type === "MapComplementaryInformationEvent" && next.payload?.mapId === mapId, { timeout: 10000 }, ); botInfo("On map", mapId, "cell", currentMap()?.cellId); } catch (error) { // An error that escapes a handler stops the whole script. botWarn("Never arrived on map", mapId, String(error)); } } export default async function behavior(launch) { // An instanced map is announced with the second message, with the same mapId. on("MapCurrentEvent", logArrival); on("MapCurrentInstanceEvent", logArrival); } Don't answer what base answers. On a Full socket bot, sending ContextReadyRequest, MapInformationRequest, or the answers to the proof of identity and the polls yourself would reach the server twice. Anything else is yours: log, store, look up, plan. When MapCurrentEvent or MapCurrentInstanceEvent arrives, the new map hasn't loaded yet. Base says it's ready 0.4 to 2.4 seconds later, and only then does the server send its contents. Until then currentMap() already names the new map, but with no actors and no cellId. To act on it, wait for its MapComplementaryInformationEvent, as above. Ids are BigInts. Compare them with BigInts, such as mapId === 153879301n. findPath() takes a map id as a BigInt, and record() doesn't: pass it Number(mapId). Base's handlers run before yours, because base loads before your code. Nothing depends on it. On a MITM bot
On a MITM bot, the Dofus client of the person playing does all of this. @asterobot:base then sends nothing and only follows the session. connect() resolves once the person playing stands on a map. A package that acts, walking or fighting, should still check session.shared itself, since the person controls the same character.
Good to know
When the character isn't placed on a map within a minute of entering the world, connect() rejects with base: the character's map never arrived within a minute of entering the world. The Dofus client asks again for a quest's step whenever it changes. @asterobot:base asks only once, on entering the world. A Dofus client whose chat channels were changed in its options also sends SubscribeMultipleChannelRequest on entering the world. A bot's options are the defaults, so @asterobot:base doesn't. The day's average prices are asked once per day, UTC, and Asterobot forgets it asked when it restarts. When the server asks to check a game file with FileCheckEvent, the bot's Console says so, and nothing answers it. Load one version of @asterobot:base per script. Two packages that lock different versions of it load both, and each answers the server.
  • 115 vues
The exact schema of export const parameters, actions, steps and conditions, with every field, the six types, defaults, choices, bounds, ref, how Asterobot reads them and every validation error.
A package puts settings and buttons on a bot's page, and gives steps and conditions to the Studio, by declaring them in its script. The complete schema is below. Declare settings, Declare actions and Steps and conditions walk through it with examples, and asterobot:parameters describes how a script reads the values. Fight profiles aren't declarations: a package ships them as files (Ship fight profiles).
Where declarations go
A package declares settings, actions, steps and conditions as four named exports of its index.js: parameters, actions, steps and conditions.
export const parameters = { maxFights: { type: "int", label: "Stop after", default: 20, min: 1 }, }; export const actions = { recall: { label: "Recall to zaap", async run() { // What the button does. }, }, }; Every export is optional. Exports with these names in other files of the package are ignored, unless index.js re-exports them, as in export { conditions } from "./lib/conditions.js": a re-export counts as an export of index.js, under the name index.js gives it. Every package of the running script can declare its own: the bot's package, and each dependency the script really imports. A dependency listed in asterobot.json that nothing imports declares nothing. Settings and actions appear on the bot's page in the order you write them, and the Studio will list steps and conditions in that order too. These are ordinary JavaScript values, so a default can be computed or imported from another file. How Asterobot reads them
Asterobot runs the top level of the package's modules without calling the default export, then reads the five exports. It does so when a bot's Settings tab shows Package settings, when you pick the package in the Add a bot dialog, each time a script starts, and for every installed package when the Fight Profiles page lists the profiles packages ship.
At that moment there's no bot and no game session, so some functions behave differently when the top level of a module calls them:
Called at the top level While Asterobot reads declarations send(), request(), wait(), sleep(), disconnect(), currentMap(), and every function of asterobot:gamedata, asterobot:pathfinding and asterobot:verification Throws, and reading fails with an error that ends with not available while reading a package's declarations move(), changeMap(), useElement(), travel(), route() Reject with an error that ends with not available while reading a package's declarations on(), onTraffic(), intercept(), off(), onChange(), offChange() Accepted, and does nothing memory.get() and memory.keys() Answer as if the memory were empty: undefined and [] memory.set() and memory.delete() Throw, and reading fails with an error that ends with not available while reading a package's declarations character(), items(), itemCount() and jobLevel() of asterobot:character Answer as if nothing were known of the character: {}, then undefined for the others. They never throw. state(), onFight(), onDecision(), offFight() and offDecision() of asterobot:fight state() returns undefined, onFight() and onDecision() return a handle that never fires, and the other two do nothing The other functions of asterobot:fight Throw, since they read or change a bot's fight module Every function of asterobot:chat Throws, and say() and whisper() reject, with chat.<function>: this bot has no chat module Every function of asterobot:alarms Throws, with alarms.<function>: this bot has no alarms session Holds empty values values An empty object botDebug() and the other console functions of asterobot:bot and asterobot:console Write nothing packages.info() Answers as usual Reading also fails when a top-level await never settles, with package awaits at import time, which cannot be answered without a Game session, and when top-level code runs for more than 5 s without awaiting, with JavaScript execution deadline exceeded. A rejected promise that nothing catches doesn't make reading fail.
So keep the top level of your modules to imports, declarations and quick computations.
Settings
export const parameters is an object. Each key is a setting's name, the name the script reads in values, and each value is an object with these fields:
Field Type Required Description type string Yes "bool", "string", "int", "double", "array" or "map" of string For array and map only The type of the array's items, or of the map's values: "bool", "string", "int" or "double". Refused on the other types. label string No Shown next to the input. Default: the setting's name. description string No A line of help shown with the input placeholder string No Shown in an empty text box or number box unit string No A short word shown after a number box, such as kamas or seconds default depends on type No The value until someone changes it. Without a default, or with null, the setting starts at its type's zero value. choices array No For string and int only: the values allowed. See Choices. min number No The smallest value allowed, for int and double max number No The largest value allowed, for int and double ref string No The game data table the numbers come from, such as "monsters" or "sub_areas". See ref. Any other field is ignored without an error, so a misspelled field such as labell 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. A ref that isn't a string is refused: see Errors.
Types
type The script reads On the bot's page Zero value bool A boolean A switch false string A string A text box "" int A whole number, never a BigInt A number box that steps by 1 0 double A number A number box 0 array An array A list with an input per item, a remove button on each, and Add [] map An object with string keys A list of key and value rows, with Add an entry. A row with an empty key is dropped. {} A setting with choices shows a drop-down list, whatever its type, and its zero value is the first choice.
Values that fit
The default, and every value applied from the bot's page, has to fit the declaration. Asterobot never converts one type into another:
bool takes true or false, nothing else. string takes a string. A number isn't turned into text. int takes a whole, finite number. 3.0 is fine, and 3.5 is refused rather than rounded. double takes a finite number: not NaN, not Infinity. array takes an array whose every item fits of. map takes an object whose every value fits of. With choices, the value has to be one of the choices. With min or max, an int or double value has to be within the bounds, the bounds themselves included. min and max don't apply to a setting that has choices, nor to the items of an array or a map. A saved value that stops fitting after you change a declaration is ignored, and the setting falls back to its default.
Choices
choices takes bare values, objects with a value and a label, or a mix of both. A bare value is its own label.
export const parameters = { slot: { type: "int", label: "Slot", choices: [1, 2, 3], default: 2 }, mode: { type: "string", label: "Strategy", choices: [ { value: "safe", label: "Avoid groups" }, { value: "greedy", label: "Fight everything" }, ], default: "safe", }, }; Every choice's value has to fit the setting's type, and a default has to be one of the choices.
ref
ref names the game data table that the setting's numbers are ids of, such as "monsters", "items", "jobs", "maps", "sub_areas" or "areas", as the Game data tab shows the tables. It's meant for an int, or an array of int, and works the same on an argument of an action, a step or a condition.
export const parameters = { monsters: { type: "array", of: "int", label: "Monsters to fight", ref: "monsters" }, }; The script still reads plain numbers. Asterobot only checks that ref is a string: it doesn't check that the table exists, nor that the setting is an int.
Asteroboard shows a search by name in place of the number box wherever such a setting or argument is edited, and Pick on the world map for maps, sub_areas and areas when a world map is on screen. See Steps and conditions.
Actions
export const actions is an object. Each key is an action's name, and each value is an object with these fields:
Field Type Required Description label string No The button's text. Default: the action's name. description string No Shown as the button's tooltip, and at the top of the arguments dialog args object No The arguments the bot's page asks for before running the action. Each one is declared exactly like a setting, with the same fields and rules, but its value isn't saved. run function Yes What the button does run(args) receives an object holding every declared argument: the value typed in the dialog, or the argument's default when none was given. An action without args receives {}. run may be async.
import { send } from "asterobot:protocol"; export const actions = { say: { label: "Say something", args: { text: { type: "string", label: "Text", default: "Hello" } }, async run({ text }) { await send("ChatChannelMessageRequest", { channel: "GLOBAL", content: text }); }, }, }; Here is what happens around an action:
Its button is in the package's section of Package settings, on the bot's Settings tab. It can only be clicked while a script is running. Otherwise it's disabled, with the tooltip The bot has to be playing a behavior before an action can run. An action with arguments opens a dialog titled with its label, with one field per argument, reset to the defaults every time it opens, and Cancel and Run buttons. The arguments are checked against the declarations before anything runs. A value that doesn't fit shows Couldn't start the action with the reason, and run isn't called. Once the action is on its way, a toast says Action started. When run returns, or its promise settles, a second toast says the action's name followed by finished, or followed by failed with the error. A failure toast stays until you close it. An error in run never stops the script. Asterobot finds the actions before it calls the default export, so the buttons work even when the entry function never finishes. A button runs the action of the script that's running. After you change an action in the editor, start the script again to use the new code. Steps and conditions
export const steps and export const conditions are objects shaped like actions. Each key is a step's or a condition's name, and each value is an object with these fields:
Field Type Required Description label string No The block's name in the Studio. Default: its key. description string No A line of help about what it does args object No The values a plan gives on each call. Each one is declared exactly like a setting, with the same fields and rules, ref included. Without args, the block takes none. run function Yes The function the plan calls Asterobot reads and checks these declarations, but never calls run, neither while reading them nor while the script plays: only a plan does. They never appear under Actions on the bot's page either. Steps and conditions describes what run receives and returns.
Types for the editor
The editor knows these types for declarations: BehaviorParameters, BehaviorActions, BehaviorSteps, BehaviorConditions, BehaviorParameter, BehaviorAction, BehaviorStep, BehaviorCondition, BehaviorBlockContext, BehaviorStepResult, BehaviorChoice, BehaviorParameterType and BehaviorScalarType. Name them in a JSDoc comment to get completion on the fields:
/** @type {BehaviorParameters} */ export const parameters = { enabled: { type: "bool", label: "Answer in chat", default: true }, }; Errors
When declarations can't be read, Package settings on the bot's Settings tab, and the package's settings in the Add a bot dialog, show Couldn't read this package's settings with the reason. The text starts with the package's name and version and read declarations:, such as:
my-first-script@1.0.0: read declarations: package "my-first-script" parameter "enabled": parameter "enabled" default: want a bool, got string In the texts below, <package> is the package's name, <name> a setting's name, <action> an action's name, <step> a step's name, <condition> a condition's name and <argument> an argument's name.
Errors that also stop the script from starting
These say an export doesn't have the right shape. They also make every start fail, with the Problems alert showing read declarations of "<module>": followed by the same text.
Error Cause package "<package>": parameters: must be an object, got <type> parameters is exported, but isn't an object package "<package>": parameters: "<name>" must be an object A setting isn't an object package "<package>": actions: must be an object, got <type> actions is exported, but isn't an object package "<package>": actions: "<action>" must be an object An action isn't an object package "<package>": actions: "<action>" declares no run function An action has no run package "<package>": actions: "<action>": run must be a function An action's run isn't a function package "<package>": actions: "<action>": args: must be an object, got <type> args isn't an object package "<package>": actions: "<action>": args: "<argument>" must be an object An argument isn't an object package "<package>": steps: must be an object, got <type> steps is exported, but isn't an object package "<package>": steps: "<step>" must be an object A step isn't an object package "<package>": steps: "<step>" declares no run function A step has no run package "<package>": steps: "<step>": run must be a function A step's run isn't a function package "<package>": steps: "<step>": args: must be an object, got <type> A step's args isn't an object package "<package>": steps: "<step>": args: "<argument>" must be an object A step's argument isn't an object Conditions give the same texts as steps, with conditions: and the condition's name.
Errors in a setting or an argument
These don't stop a start: the script starts, but with no settings at all. They begin with package "<package>" parameter "<name>": for a setting, with package "<package>" action "<action>": argument "<argument>": for an action's argument, with package "<package>" step "<step>": argument "<argument>": for a step's, and with package "<package>" condition "<condition>": argument "<argument>": for a condition's.
Error Cause parameter "<name>" declares unknown type "<type>" type is missing or isn't one of the six types parameter "<name>" is a <type> and must declare its element type as `of` An array or map without of parameter "<name>" declares element type "<of>", which is not one of bool/string/int/double of isn't one of the four scalar types parameter "<name>" is a <type> and cannot declare an element type of on a type other than array and map parameter "<name>" declares choices, which only a string or an int may do choices on a type other than string and int choices must be an array, got <type> choices isn't an array choice <index> has no value A choice object without value. Choices count from 0. parameter "<name>" choice <index>: <reason> A choice's value doesn't fit the type parameter "<name>" declares min <min> above max <max> min is greater than max parameter "<name>" default: <reason> The default doesn't fit the declaration parameter has no name A setting's name is the empty string ref must be a string, got <type> ref is there, but isn't a string Reasons a value doesn't fit
These reasons follow default: or choice <index>: in the errors above. The same texts appear when a value applied from the bot's page, or an action's argument, doesn't fit.
Reason Cause want a bool, got <type> A bool got something other than true or false want a string, got <type> A string got something other than a string want an int, got <type> An int got something other than a number want a whole number, got <value> An int got a fraction, or Infinity or NaN want a number, got <type> A double got something other than a number want a finite number, got <value> A double got NaN or Infinity want an array, got <type> An array got something other than an array item <index>: <reason> An array item doesn't fit of want a map, got <type> A map got something other than an object key "<key>": <reason> A map value doesn't fit of <value> is not one of <choices> The value isn't among the choices, listed with commas <value> is below the minimum <min> The value is lower than min <value> is above the maximum <max> The value is higher than max <type> is the name Asterobot gives the value's type: string, bool, int64 for a whole number written in the script, float64 for other numbers and for every number sent from the bot's page, []interface {} for an array and map[string]interface {} for an object.
Error messages lists the texts you can meet everywhere else.
  • 82 vues
Every limit Asterobot sets on scripts, with its value and exactly what happens when a script goes past it.
Asterobot runs every script within fixed limits, so that one script can't hold up Asterobot or the other bots. They're the same for every bot and every package, and a package can't change them. None of them depends on the speed of the PC: a slower PC makes a script slower, never different. Here is each one, with its value and what happens past it.
Running code
Limit Value Past the limit Code running without a pause 5 s The script stops, and the Problems alert shows a text containing JavaScript execution deadline exceeded. The game connection stays open. Function calls nested in each other 1024 The script stops. A try/catch can't catch this. The limit is there to stop code that never ends, such as an endless loop, and sits far above what a script's code takes. A pause is an await, or the end of the function Asterobot called. Each of these counts as one run, with its own 5 s:
the top level of the modules, when the script starts; the entry function, up to its first await; one call of a handler: from on(), onTraffic(), intercept(), onChange(), onStopRequest(), onFight(), onDecision(), a selector function, or an action's run; code resuming after an await, together with any other code resuming at the same moment. The text in the alert says which it was, for example Game traffic handler failed: JavaScript execution deadline exceeded for a handler, or drain JavaScript Promise jobs: JavaScript execution deadline exceeded for code that resumed after an await. The place in your code follows it.
Three cases don't stop the script:
In an interceptor, the message goes on unchanged, and the bot's Console shows interceptor failed: JavaScript execution deadline exceeded. In a fight decision hook, the fight module's AI decides, and the bot's Console shows fight decision hook failed: JavaScript execution deadline exceeded: the AI decides. In an action's run, the toast for the action says it failed, with the same text. While Asterobot reads the package's declarations, reading fails. See Declarations. Handlers, waits and sleeps
Limit Value Past the limit Message handlers registered at once with on(), onTraffic(), intercept(), and asterobot:fight's onFight() and onDecision() together 256 The call throws a TypeError with maximum behavior handlers reached Handlers registered at once with onChange() 256, counted apart from message handlers The call throws a TypeError with maximum behavior handlers reached Handlers registered at once with onStopRequest() 256, counted apart from the others The call throws a TypeError with maximum behavior handlers reached wait() calls not settled yet 128 The new wait() rejects with behavior resource limit exceeded: maximum waits reached sleep() calls not settled yet 128 The new sleep() rejects with behavior resource limit exceeded: maximum timers reached A handler removed with off(), offChange() or offStopRequest(), and a wait or a sleep that settled, frees its place. A wait() without a timeout that never matches keeps its place until the script stops.
Pending operations
Limit Value Past the limit Operations waiting for their result: request(), table(), find(), search(), query() and the asterobot:movement functions that return a promise, together 32 The new call's promise rejects with behavior resource limit exceeded: maximum pending operations reached send() doesn't count: it resolves as soon as its message is in line. text() and record() answer right away and don't count either. An operation frees its place when your code receives its result, so whether a script reaches the limit depends on what it does, never on how fast the answers come.
Events waiting for the script
Limit Value Past the limit Game messages waiting to be handled 8 MB The script stops with the script fell too far behind the game Messages the script sent, waiting to be written 8 MB The new send() or request() rejects with behavior resource limit exceeded: more than 8 MB are waiting to be sent, so the connection isn't taking them The events are the messages that arrive, operations that finish, sleeps and wait timeouts that end, applied settings and clicked actions. A script handles them one at a time, in the order they came, so they pile up while its code is busy, for example in a handler that runs long or awaits nothing. Nothing is skipped: there's no limit on how many can wait, only on the size of the game messages waiting, and 8 MB means the PC can't keep up with the script at all. A MITM bot relaying a busy game session receives many messages each second: keep handlers short.
On a MITM bot, Asterobot never slows the game down for what watches it. When the Network tool can't keep up with the messages, Asterobot leaves some out of what it receives, and the game still gets them. A script never carries on with such a hole: it stops instead, with game traffic lost: Asterobot fell behind the game and missed messages. A Full socket bot waits instead, and misses nothing.
Timeouts
Limit Value Past the limit send() None: it resolves as soon as its message is in line, and ignores { timeout } request() 30 seconds from the call unless you pass { timeout } The promise rejects with context deadline exceeded wait() None unless you pass { timeout } With a timeout, the promise rejects with protocol wait timed out. Without one, it waits until the script stops. table(), find(), search() and query() None of their own: they end when their work is done A query() is bounded by its own 10 seconds, see Game data An interceptor's decision None: the message waits for the script, however busy it is An interceptor that runs for more than 5 s is stopped, like any code, and the message goes on unchanged Packages and modules
Limit Value Past the limit Modules the script loads, counting the built-in modules it imports 256 The script doesn't start: reachable module count exceeds limit 256 Total size of those modules 4 MiB (4,194,304 bytes) The script doesn't start: reachable module source exceeds limit of 4194304 bytes Chain of imports, from index.js down 64 modules The script doesn't start: module graph depth <depth> exceeds limit 64 at "<module>" When a script doesn't start for one of these reasons, the toast after Play or Run doesn't say why. The bot's Settings tab does: Package settings shows Couldn't read this package's settings with the text.
Only the .js and .mjs files of a package are part of its code; its fight profiles, in fightprofiles/, and its chat profiles, in chatprofiles/, are its only data. A package archive you import is at most 32 MiB: see Zip files and the command line.
Game data
Limit Value Past the limit Rows returned by one call of table(), find(), search() or query() 200 by default, 5000 at most A larger limit is lowered to 5000. query() sets truncated to true when rows were left out. Length of the SQL passed to query() 65,536 bytes The promise rejects with datacenter: query is <size> bytes, over the 65536 byte limit Time for one query(), once its turn has come 10 seconds The query is interrupted, and the promise rejects Size of a single value a query builds 16 MiB The query fails Queries running at once on one game version, across every bot and Asteroboard 4 The next query waits for its turn, in the order they came, for as long as it takes Bot memory
The bot's memory is shared by every package the bot runs, so these limits are for the whole bot, not for each package.
Limit Value Past the limit Length of a key 256 bytes memory.set() throws a TypeError with memory: a key must be a non-empty string of at most 256 bytes Size of one value 64 KiB memory.set() throws an Error with memory: the value is larger than 64 KiB Keys stored at once 1024 memory.set() of a new key throws an Error with memory: this bot's memory is full (1024 keys or 1 MiB) Size of every value together 1 MiB memory.set() throws the same Error, unless the new value is no larger than the one it replaces Levels of arrays and objects nested in a value 32 memory.set() throws a TypeError that says the value nests more than 32 levels deep A value's size is roughly the size of its JSON text, with every number counted as 8 bytes. memory.delete() frees what the key held.
Consoles
Limit Value Past the limit Lines Asterobot keeps for each bot's Console 200 A Console tab opened later starts with the last 200 lines only Interceptor failures written to the bot's Console for one message The first, and the tenth When several interceptors fail on the same message, the other failures aren't written. The count starts again with each message. Error messages explains every text on this page, and Async and timing shows how to write scripts that stay within the limits.
  • 78 vues
Every error text a script author can meet, grouped by where it appears, with its cause and how to fix it.
The error texts you can meet while writing and running scripts are grouped here by where they appear. Search for a part of the text you see. In the texts, a word in angle brackets such as <module> stands for something that changes: <module> is the path of a file inside Asterobot, such as library/my-first-script/1.0.0/index.js, <type> a message's name, <field> a field's name.
Where you see it What it looks like Section At the top of the bot's page The behavior stopped with an error, with the full text, Copy and Dismiss The script stopped A toast after Play, Run, a load or a version switch Couldn't start the behavior, Couldn't run the package, Couldn't load the selection or Couldn't switch version, without a reason When a package doesn't load Package settings, on the bot's Settings tab, and the Add a bot dialog Couldn't read this package's settings, with the reason When a package doesn't load, and Declarations Inside the script A rejected promise, or an error thrown by a call, that your code can catch Errors a script can catch The bot's Console tab A warning or error line, while the script keeps running Console lines Toasts on Package settings Couldn't apply the settings, Couldn't start the action, or an action's name followed by failed Saving settings and running actions Under the package's manifest, after saving it The reason, or Couldn't save the manifest Saving the manifest The script stopped
The behavior stopped with an error appears at the top of the bot's page when a script that had loaded stops because of an error, while starting or later. In Bot Manager, the bot's Behavior column says Failed. The game connection stays open. Fix the cause, then start the script again with Play, or with Run in the editor.
The beginning of the text says where the script was:
The text starts with The error happened evaluate behavior module "<module>": In the top-level code of the modules, as the script started read declarations of "<module>": While Asterobot read the parameters, actions, steps and conditions exports. See Declarations. initialize behavior module "<module>": In the entry function, before it finished Game traffic handler failed: In a handler registered with on() or onTraffic() evaluate protocol handler selector: In a selector function passed to on() evaluate protocol wait selector: In a selector function passed to wait() parameters.onChange handler <number>: In a handler registered with onChange() fight event handler failed: In a handler registered with onFight() unhandled Promise rejection: Anywhere: a promise rejected and nothing caught it drain JavaScript Promise jobs: In code that resumed after an await What follows is the error itself:
Promise rejected: and the reason, when a promise the start was waiting for rejected. An error thrown by your code: its type and message, such as TypeError: and the message. For an error thrown synchronously, at follows, with the function's name and, in parentheses, the file, the line and the column. A message from one of Asterobot's functions, as listed in Errors a script can catch. For example, a script that sends token instead of ticketKey while starting gets:
initialize behavior module "library/my-first-script/1.0.0/index.js": Promise rejected: IdentificationRequest.token: unknown protobuf field These texts can appear after one of the beginnings above, or on their own:
Text Cause Fix behavior module "<module>" has no unambiguous default export index.js has no default export Add export default async function behavior(launch) behavior module "<module>" default export is not callable The default export isn't a function Export a function behavior module "<module>" default export must be async and return a Promise The default export returned something other than a promise Make it an async function dynamic import() is not supported by this Asterobot runtime The script called import() as a function Use an import statement at the top of the file JavaScript execution deadline exceeded Code ran for more than 5 s without an await, as an endless loop does Look for a loop that never ends, and split long work into parts separated by an await, such as await sleep(0). See Limits. the script fell too far behind the game: more than 8 MB of game messages are waiting for it, so this PC can't keep up with it The script's handlers take longer than the game's messages keep coming, minute after minute Keep handlers short, and move slow work out of them game traffic lost: Asterobot fell behind the game and missed messages: <count> of them On a MITM bot, Asterobot itself fell so far behind the game that it had to skip messages, and the script would have missed them Run fewer bots on this PC, or play the script again send <type>: <reason> A message the script sent couldn't be written, after send() had resolved Look at <reason>; play the script again unhandled Promise rejection: <reason> A promise rejected with nothing to catch it: an await outside try/catch in an async handler or interceptor, or a promise started without await Wrap the await in try/catch, or add .catch() Game session closed: <reason> The game connection closed while the script was running. The script handled every message that arrived before. Connect the bot again, then play the script behavior requested Game disconnect The script called disconnect() Nothing, if that was intended A script stopped with Stop shows no alert.
When a package doesn't load
When a script can't even be loaded, it never runs, and nothing reaches the Problems alert. The toast is all you get, without the reason: Couldn't start the behavior after Play, Couldn't run the package after Run, Couldn't load the selection after a load from the bot's menu, Couldn't switch version after changing the version.
To see the reason, open the bot's Settings tab. Asterobot loads a package the same way to read its settings, so for the bot's package, Package settings shows Couldn't read this package's settings with the text. The package's settings in the Add a bot dialog show it too. For a syntax error, the editor's error badge usually points at the line as well.
When the reason comes from the package's code, the text there starts with <package>@<version>: read declarations:.
Syntax and imports
Text Cause Fix parse module "<module>": <details> That file has a syntax error Fix the syntax. Check the editor's error badge. resolve "<specifier>" imported by "<module>": package "<package>" has no locked dependency for "<specifier>" An import names a package that isn't among the package's dependencies, or is misspelled Fix the name, or add the dependency. See Dependencies. resolve "<specifier>" imported by "<module>": module "<module>" imported by "<module>" is not installed A relative import names a file the package doesn't have Fix the path. Relative imports need the whole file name, extension included, such as ./helper.js. resolve "<specifier>" imported by "<module>": "<module>" imported by "<module>" is a data file, not a module A relative import names one of the package's fight profiles, a .json file of its fightprofiles folder Don't import it: Asterobot reads the package's profiles itself. A script plays one with setProfile() and its name. resolve "<specifier>" imported by "<module>": relative import "<specifier>" escapes package root "<root>" A ../ goes above the package's own folder Import only files of the package, or of a dependency by its name resolve "<specifier>" imported by "<module>": built-in module "<specifier>" is not registered The asterobot: module doesn't exist, which is the case for every asterobot:extension/ module today Import one of the twelve built-in modules. See Extension modules. resolve "<specifier>" imported by "<module>": package "<package>": no installed version of "<name>" satisfies "<version>" An import such as "name@^2.0.0" asks for a version the script's packages don't include Lock a matching version in the dependencies resolve "<specifier>" imported by "<module>": package "<package>": specifier "<specifier>" has an invalid version "<version>": <details> The part after @ in the import isn't latest, a version or a version range Fix the version resolve "<specifier>" imported by "<module>": absolute module specifier "<specifier>" is forbidden An import starts with / Use a relative path or a package name resolve "<specifier>" imported by "<module>": module specifier "<specifier>" contains a backslash An import path uses \ Use / reachable module count exceeds limit 256 The script loads more than 256 modules See Limits reachable module source exceeds limit of 4194304 bytes The script's modules weigh more than 4 MiB together See Limits module graph depth <depth> exceeds limit 64 at "<module>" Imports chain more than 64 modules deep See Limits Packages and dependencies
These texts start with resolve package <name>@<version>:, naming the package being loaded. A problem in one of its dependencies adds dependency "<name>" locked to version "<version>": before the reason, once per level.
Text Cause Fix read package <name>@<version> sources: <details> Asterobot couldn't read that package's files, for example because that version isn't installed Install the package or the dependency at that version package <name>@<version> is not installed: parse asterobot.json: <details> The package's asterobot.json isn't valid JSON Fix the file. See The manifest. package "<name>@<version>": package "<name>" requires asterobot <range>, running <version> The package's compatibleVersion doesn't include the Asterobot version you run Update Asterobot, or change compatibleVersion. See Compatibility. package "<name>@<version>": package "<name>" has an invalid compatibleVersion constraint "<range>": <details> compatibleVersion isn't a valid version range Fix it, such as >=1.5.0 <2.0.0 package "<name>@<version>": package source integrity mismatch The files of a package installed from the marketplace no longer match the checksum recorded at install Install that version again package "<name>@<version>": empty package permission permissions in asterobot.json has an empty entry Remove it package "<name>@<version>": duplicate package permission "<permission>" permissions lists the same permission twice Remove the duplicate package "<name>@<version>": entry module "index.js" is missing The package has no index.js at its root Add it. See Package layout. package "<name>@<version>": package contains no modules The package has no .js or .mjs file Add index.js package "<name>@<version>" dependency "<specifier>" targets package named "<name>" version "<version>" A dependency whose name ends with @ and a version is locked to another version Make the version in the name and the locked version the same package <name>@<version> has an invalid name: <details> The package's name breaks the naming rules See Names and versions Messages about downloading or installing packages from the marketplace are covered in Package errors.
Errors a script can catch
The functions of the built-in modules report problems in two ways. Most return a promise that rejects, which await turns into an error a try/catch can catch. A few throw at once, as noted below.
String(error) gives the texts of this section as they are written here, with two exceptions: an error of type TypeError gives TypeError: followed by the text, and the errors thrown by record(), findPath(), proof(), memory.set() and packages.info(), and the errors move() rejects with, give GoError: followed by the text.
An error your code doesn't catch stops the script, with one of the texts in The script stopped.
Messages and timers
Text Returned by Cause Fix protocol type must be a non-empty semantic message name send(), request() The message name is missing, empty or not a string Pass the name as a string semantic protobuf message is not mapped: <type> send(), request() Asterobot has no message with that name Check the name in the Dofus protocol reference protocol options must be an object send(), request(), wait() The options aren't an object, such as a bare number Pass { timeout: 5000 } unknown protocol option "<name>" send(), request(), wait() The options have a key other than timeout, to and correlated Fix the key timeout/milliseconds must be a finite positive Number send(), request(), wait(), sleep() A timeout that isn't a number above 0, or a sleep() duration that's negative or not a number Pass a number of milliseconds timeout/milliseconds is too large send(), request(), wait(), sleep() A duration too big to be one Pass a smaller number send option "to" must be "server" or "client" send() to isn't a string Use "server" or "client" unknown send target "<value>": expected "server" or "client" send() to is another string Use "server" or "client" send option "correlated" must be a boolean send() correlated isn't true or false Pass a boolean send option "correlated" cannot be combined with to: "client": a frame toward the client is an event, which has no uid send() Both options were passed Drop one of them protocol selector cannot be empty wait(); thrown by on(), intercept() The selector is "" Pass a name, "*" or a function protocol selector must be a semantic type, '*', or predicate wait(); thrown by on(), intercept() The selector is neither a string nor a function Pass a name, "*" or a function protocol.on handler must be callable, protocol.onTraffic handler must be callable, protocol.intercept handler must be callable Thrown as a TypeError The handler isn't a function Pass a function maximum behavior handlers reached Thrown as a TypeError by on(), onTraffic(), intercept(), onChange() 256 handlers of that kind are registered Remove handlers you no longer need with off() or offChange() protocol wait timed out wait() Nothing matched before the timeout Check the selector, or wait longer context deadline exceeded request() No response came back before the timeout. Most Dofus requests never get one. Use send() and then wait() for the event the game sends mitm relay refuses to identify: this Game session was already authenticated by the real DOFUS client request() The script sent IdentificationRequest on a MITM bot Identify only when launch.reason === "initial" behavior resource limit exceeded: maximum pending operations reached send(), request(), table(), find(), search(), query(), move() 32 operations are pending Await operations before starting more behavior resource limit exceeded: maximum waits reached wait() 128 waits are open Give every wait() a timeout behavior resource limit exceeded: maximum timers reached sleep() 128 sleeps are pending Await sleeps instead of starting many at once On a MITM bot, send() of an IdentificationRequest to the server doesn't reject: see Console lines.
Payload errors
send() and request() reject with these texts when a payload doesn't fit its message. An interceptor's { payload } gives the same texts on the bot's console, after build replacement payload for <type>:.
Each text starts with the path of the field: the message's name, then the field names separated by dots, with [<index>] for an item of a list and ["<key>"] for a value of a map.
Text Cause Fix <type>: expected an object The payload is missing, null or undefined Pass {} for a message without fields <path>: expected an object, got <type> The payload, or a nested message, isn't an object Pass an object <path>.<field>: unknown protobuf field The message has no such field Check the field names in the Dofus protocol reference IdentificationRequest.token: unknown protobuf field The script copies an old example that sends token and lang Send ticketKey and languageCode, as in Identify and run <path>: fields "<name>" and "<name>" address the same protobuf field The same field is given under both spellings, such as ticketKey and ticket_key Keep one <path>: oneof <group> selects both "<field>" and "<field>" Two alternatives of the same group are set Set only one <path>.<field>: explicit undefined is not a protobuf value A field is set to undefined Leave the field out <path>.<field>: null is only valid for a message field A field that isn't a nested message is set to null Leave the field out <path>: expected an array A list field got something other than an array Pass an array <path>: invalid array length The value passed for a list has an unusable length Pass a real array <path>: expected a plain object map A map field got something other than an object Pass an object <path>: invalid boolean map key "<key>", and the same with int32, uint32, int64 or uint64 A map key can't be read as the map's key type Use keys such as "true" or "12" <path>: expected boolean A boolean field got another type Pass true or false <path>: expected string A string field got another type Pass a string <path>: expected Uint8Array or ArrayBuffer A bytes field got another type Pass a Uint8Array <path>: expected integral Number A 32-bit integer field got a fraction or something that isn't a number Pass a whole number <path>: Number is outside int32 range The number doesn't fit a signed 32-bit integer Pass a smaller number <path>: expected integral Number in uint32 range An unsigned 32-bit field got a fraction, a negative number or something too big Pass a whole number from 0 to 4294967295 <path>: expected signed 64-bit BigInt or decimal string A 64-bit integer field got a number too big to be exact, a fraction, a string that isn't digits, or another type Pass a BigInt, such as 123456789012n <path>: expected unsigned 64-bit BigInt or decimal string The same, for an unsigned field, or a negative value Pass a positive BigInt <path>: expected Number A floating point field got something that isn't a number Pass a number <path>: unknown enum symbol "<name>" The enum has no value with that name Check the value names in the Dofus protocol reference <path>: expected enum symbol or number An enum field got neither a name nor a whole number Pass the value's name as a string Game data
Text Returned by Cause Fix gamedata.text key must be a string or integer Thrown as a TypeError by text() The key is a fraction, a number outside the 32-bit range, a BigInt, undefined or an object Check the value before passing it gamedata.record table must be a non-empty string Thrown as a TypeError by record() The table name is missing or empty Pass the table's name gamedata.record id must be an integer Thrown as a TypeError by record() The id isn't a whole number, is too big, or is a BigInt or a string Pass a regular whole number datacenter: no such table: "<table>" Thrown by record(); table(), find(), search() This game version has no table with that name Check the table's name in the Game page's game data gamedata.table name must be a non-empty string table() The name is missing or empty Pass the table's name gamedata.find table must be a non-empty string find() The name is missing or empty Pass the table's name gamedata.find filter must be a non-empty object find() The filter is missing, empty, an array or not an object Pass { column: value } datacenter: no such column: table "<table>" has no column "<column>" find() A filter column doesn't exist in that table Fix the column's name gamedata.search query must be a non-empty string search() The query is missing or empty Pass a name gamedata.search options.table must be a string search() options.table isn't a string Pass the table's name as a string datacenter: no text for this language: "<language>" search() The game data has no names in the script's language Extract the game data again, or check session.language gamedata.query sql must be a non-empty string query() The SQL is missing or empty Pass the SQL as a string gamedata.query params must be an array query() params isn't an array Pass an array, such as [31] datacenter: empty query query() The SQL holds only spaces Pass a statement datacenter: query is <size> bytes, over the 65536 byte limit query() The SQL text is too long Shorten the query datacenter: followed by the database's message query() The SQL is wrong, names a table or column that doesn't exist, or writes, in which case the message mentions attempt to write a readonly database Fix the query. Try it in the SQL tab. Pathfinding
Text Returned by Cause Fix pathfinding.findPath mapId must be an integer Number, BigInt or decimal string Thrown as a TypeError by findPath() The map id is a fraction, a number above Number.MAX_SAFE_INTEGER, a string that isn't digits, or another type Pass currentMap().mapId, or the id as a BigInt pathfinding.findPath from must be an integer cell id, and the same with to Thrown as a TypeError by findPath() The cell isn't a whole number, or is a BigInt or a string Pass a cell id from 0 to 559 pathfinding.findPath options must be an object Thrown as a TypeError by findPath() The options are neither an object, null nor undefined Pass an object, or nothing pathfinding.findPath options has no property "<name>" Thrown as a TypeError by findPath() The options have a key other than occupied, cautious and disableDiagonals Fix the key pathfinding.findPath options.occupied must be an array of cell ids Thrown as a TypeError by findPath() occupied isn't an array Pass an array of cell ids pathfinding.findPath options.occupied[<index>] must be an integer cell id Thrown as a TypeError by findPath() That item isn't a whole number Pass cell ids only pathfinding.findPath options.cautious must be a boolean, and the same with disableDiagonals Thrown as a TypeError by findPath() The option isn't a boolean Pass true or false pathfinding.findPath: map <mapId>, cell <from> to cell <to>: pathfinding: not a map cell, cell ids go from 0 to 559: <cell> Thrown as a TypeError by findPath() from or to is outside 0 to 559. For a cell of occupied, the text ends with occupied cell <cell>. Pass cell ids from 0 to 559 failed to load map <mapId> for pathfinding: datacenter: no such record: maps/<mapId> Thrown by findPath() The bot's game version has no map with that id Check the map id failed to open the datacenter: followed by the reason Thrown by findPath() Asterobot can't read the game data of the bot's game version Extract the game data pathfinding.cellToPoint cell must be an integer cell id, pathfinding.distance a must be an integer cell id and the same with b, pathfinding.direction from must be an integer cell id and the same with to Thrown as a TypeError by cellToPoint(), distance(), direction() The cell isn't a whole number, or is a BigInt or a string Pass a cell id from 0 to 559 pathfinding: not a map cell, cell ids go from 0 to 559, after pathfinding.cellToPoint cell:, pathfinding.distance cells: or pathfinding.direction cells: Thrown as a TypeError by cellToPoint(), distance(), direction() A cell is outside 0 to 559 Pass cell ids from 0 to 559 pathfinding.pointToCell x and y must be integers Thrown as a TypeError by pointToCell() x or y isn't a whole number Pass whole numbers Movement
Text Returned by Cause Fix movement.move cell must be an integer cell id, movement.changeMap options.cell must be an integer cell id move(), changeMap(), as a TypeError The cell isn't a whole number, or is a BigInt or a string Pass a cell id from 0 to 559 movement.move cell: pathfinding: not a map cell, cell ids go from 0 to 559, and the same after movement.changeMap options.cell: move(), changeMap(), as a TypeError The cell is outside 0 to 559 Pass a cell id from 0 to 559 movement.changeMap side must be "top", "bottom", "left" or "right" changeMap(), as a TypeError The side is none of the four Pass one of the four movement.useElement element must be an integer element id useElement(), as a TypeError The element id isn't a whole number Pass an id from currentMap().elements movement.useElement options.skill must be an integer skill id useElement(), as a TypeError The skill isn't a whole number Pass a skill id from the element's skills movement.travel mapId must be an integer Number, BigInt or decimal string, and the same for movement.route travel(), route(), as a TypeError The map id isn't a whole number Pass a map id, such as currentMap().id movement.travel options.cell must be an integer cell id, movement.travel options.cell: pathfinding: not a map cell, cell ids go from 0 to 559, and the same for movement.route travel(), route(), as a TypeError The cell isn't a cell id Pass a cell id from 0 to 559 movement.travel options.risky must be a boolean, and the same for the other functions that take it travel(), route(), the map finders, as a TypeError risky isn't a boolean Pass true or false movement.travel options.onLeg must be a function travel(), as a TypeError onLeg isn't a function Pass a function movement.findMaps query must be an object, movement.findMaps query.<name> must be an integer findMaps(), nearestWith(), as a TypeError The query isn't an object, or a field isn't a whole number Pass { subArea: 95 } and the like movement.mapAt x and y must be integers, movement.mapAt worldMap must be an integer mapAt(), as a TypeError A coordinate isn't a whole number Pass whole numbers movement.nearest mapIds must be an array of map ids nearest(), as a TypeError mapIds isn't an array Pass an array, such as findMaps()'s movement.mapsWithin legs must be a whole number of legs mapsWithin(), as a TypeError legs isn't a whole number from 0 Pass a number of legs movement.<function> options must be an object every movement function, as a TypeError The options are neither an object, null nor undefined Pass an object, or nothing movement.<function> options has no property "<name>" every movement function, as a TypeError The options have a key the function doesn't take Fix the key movement.move options.cautious must be a boolean move(), as a TypeError cautious isn't a boolean Pass true or false movement.<function> options.timeout: timeout/milliseconds must be a finite positive Number every movement function, as a TypeError timeout isn't a number above 0 Pass a number of milliseconds move: the bot isn't connected to a game server move(), changeMap(), useElement(), travel() The bot has no game connection Connect the bot first move: this bot has no game data to route on move(), changeMap(), useElement(), travel() The bot's game version has no game data Extract the game data move: the character is already moving; await the walk, map change or use in progress first move(), changeMap(), useElement(), travel() Another walk, map change or use of the bot is under way, from a script or from Asteroboard Await each call before starting the next move: the character is still walking move(), changeMap(), useElement(), travel() The character hasn't finished a walk it didn't ask for, such as one the player started on a MITM bot Wait for that walk to end move: the character's map and cell aren't known yet move(), changeMap(), useElement(), travel() Asterobot hasn't seen the character's map yet, or where the map places the character Wait until the character is on a loaded map move: the game has no path from cell <cell> to cell <cell> move(), changeMap(), useElement() The Dofus client itself would find no route Pick another cell move: the server didn't grant the walk in time move(), changeMap(), useElement(), travel() Neither MapMovementEvent nor MapMovementRefusedEvent answered the walk within the timeout Check the cell, or give the server longer with timeout move: another walk of the character started before this one ended move(), changeMap(), useElement(), travel() Another walk of the character started first, or the walk was cancelled Nothing to fix in the call move: the server didn't acknowledge the end of the walk in time move(), changeMap(), useElement(), travel() No MapMovementConfirmResponse came within the walk's time plus the timeout. On a MITM bot the timeout counts from Asterobot's own confirmation, one second past the walk's time, when the Dofus client hasn't confirmed by then Give the server longer with timeout move: the game session closed during the walk move(), changeMap(), useElement(), travel() The game connection closed first Connect the bot again move: the walk didn't end on the cell it was for: it ended on cell <cell>, not on cell <cell> changeMap(), useElement() The server granted a shorter walk, so the character isn't on the exit Call again move: the map data names no map beyond that side changeMap() That side leads nowhere Pick another side move: no exit leads off that side of the map changeMap() That edge has no exit cell Pick another side move: that cell isn't an exit of that side: cell <cell> changeMap() options.cell isn't one of that side's exits Pass an exit of that side, or no cell move: no exit of that side can be walked to from here (cell <cell>) changeMap() Every exit of that side is out of the character's reach Walk elsewhere first, or pick another side move: the server didn't change map in time; the character is still on map <mapId> changeMap(), useElement() Neither MapCurrentEvent nor MapCurrentInstanceEvent came within the timeout Give the server longer with timeout move: the server refused the map change; the character is still on map <mapId> changeMap(), travel() The server answered MapChangeRequest with MapMovementRefusedEvent; a trip goes another way Leave by another side, or another exit cell move: the character changed map, but the new map's contents never came changeMap(), useElement(), travel() No MapComplementaryInformationEvent came within 20 seconds of the new map, although MapInformationRequest was sent Check the bot's connection move: the server refused the walk move(), changeMap(), useElement(), travel() The server answered the walk with MapMovementRefusedEvent Pick another cell move: the character is still using an element useElement() A timed use of the character's hasn't ended Wait for the use to end move: no such interactive element on this map: <element> useElement() The element isn't in currentMap().elements Pass an element of the character's map move: the game data doesn't place this element on the map: <element> useElement() The map data has no cell for that element Nothing to fix in the call move: the element offers no action right now: element <element> useElement() The element offers nothing now, such as a resource already harvested Wait for it to offer an action move: the element doesn't offer that action right now: skill <skill>; it offers skills <skill>, <skill> useElement() options.skill isn't among the element's actions now Pass one of the skills it offers move: the element offers several actions; name the one to use: skills <skill>, <skill> useElement() The element offers several actions and no skill was named Pass options.skill move: the server refused the use (element <element>) useElement() The server answered with InteractiveUseErrorEvent Check the element can be used by this character move: the server didn't answer the use in time (element <element>) useElement() No InteractiveUsedEvent came within the timeout Give the server longer with timeout travel: the world graph doesn't have this map: map <mapId> travel(), route(), mapsWithin() The trip goes to a map the travel graph doesn't have, such as a house, a haven bag or an instance, or route() starts from one Pick a map of the world, or leave that map first travel: no known way there (map <mapId> to map <mapId>) travel(), route() No way the trip may take leads there: it needs a zaap, an NPC transport, or a leg whose condition Asterobot can't check Get closer first, or pass risky: true move: too many legs of the trip were refused; the last one: followed by the reason travel() The server or the map refused 8 legs of the trip: no exit or path to walk to, an element missing or offering another action, a use refused, or no map change. A trip goes round each one until then. Check the reason, or travel in steps move: the trip keeps ending up off its route: on map <mapId> travel() 8 legs left the character somewhere their route didn't lead Travel in steps move: the trip was stopped travel() stopTravel(), Asteroboard's Stop, or onLeg returning false stopped it Nothing to fix move: nothing leads off this map to a map the world graph has (map <mapId>) travel() The character stands on a map the travel graph doesn't have, and none of its exits, sides or doors that leave worked Leave that map first asterobot:movement describes each case.
Verification
Text Returned by Cause Fix verification.proof challenge must be a decimal string, or undefined when the server sent none Thrown as a TypeError by proof() The challenge is neither a string, undefined nor null Pass traffic.payload?.value of the ServerChallengeEvent verification: the challenge is not a decimal integer: "<challenge>" Thrown by proof() The challenge isn't a whole number written in base 10 Pass the challenge exactly as it arrived asterobot:verification describes both.
Bot memory
These are thrown at once: the functions of memory are synchronous. Each text after memory.set starts with where the value is wrong, from value, as in memory.set value.fields[2] is not a finite number.
Text Thrown by Cause Fix memory.get key must be a non-empty string, and the same with memory.set and memory.delete A TypeError The key is missing, empty or not a string Pass the key as a string memory: a key must be a non-empty string of at most 256 bytes A TypeError from memory.set() The key is longer than 256 bytes Use a shorter key memory.set <path> is undefined, which the memory cannot hold: use memory.delete to remove a key A TypeError The value, or something in it, is undefined Leave the field out, or call memory.delete() memory.set <path> is not a finite number A TypeError NaN, Infinity or -Infinity Store a finite number, or null memory.set <path> is a Symbol, which is not plain data A TypeError A symbol Store a string instead memory.set <path> is a <kind>, which is not plain data A TypeError A function, a Map, a Set, a Date or another built-in object Store its data: an array for a Map, a number from getTime() for a Date memory.set <path> is an instance of a class, which is not plain data: store its fields in a plain object A TypeError An object made with new from a class of your own Copy its fields into a plain object memory.set <path> nests more than 32 levels deep, or contains itself A TypeError Too many levels of arrays and objects, or an object that holds itself Flatten the value memory: the value is larger than 64 KiB memory.set() The value is too large Store less, or split it under several keys memory: this bot's memory is full (1024 keys or 1 MiB) memory.set() The bot holds 1024 keys, or the values would pass 1 MiB together memory.delete() what's no longer needed Limits explains how a value's size is counted.
Character
These are thrown at once: the functions of asterobot:character are synchronous.
Text Thrown by Cause Fix character.itemCount itemId must be an integer item id A TypeError from itemCount() The id isn't a whole number from -2147483648 to 2147483647, or isn't a number at all, such as a string or a BigInt Pass the item's id as a number character.jobLevel jobId must be an integer job id A TypeError from jobLevel() The same for the job's id Pass the job's id as a number character() never throws. asterobot:character describes both.
Fight
These are thrown at once: the functions of asterobot:fight are synchronous.
Text Thrown by Cause Fix fight.setProfile: fight: malformed profile: <field>: <reason> A TypeError from setProfile() The profile object can't be played Fix the field. The format's errors list each reason. fight.setProfile: fightprofiles: no profile has this name: "<name>" setProfile() No profile has that name Check the name on the Fight Profiles page fight.setProfile takes a profile's name or a profile object A TypeError from setProfile() The argument is neither a string nor an object Pass a name or an object fight.setSpeed: fight: the speed must be slow, normal, turbo or custom, not "<speed>" A TypeError from setSpeed() Another speed Pass "slow", "normal", "turbo" or a timing object fight.setSpeed: fight: a custom speed needs its timing, each wait from 0 to 60000 ms A TypeError from setSpeed() "custom" passed as a string, or a wait out of bounds (, not <ms>) Pass a timing object, each wait from 0 to 60000 fight.setSpeed: json: unknown field "<field>" A TypeError from setSpeed() A field a timing doesn't have Use actions, placement, ready, move, cast, endTurn and answerAtOnce fight.setSpeed takes "slow", "normal", "turbo" or a timing object A TypeError from setSpeed() The argument is neither a string nor an object Pass a speed's name or a timing object fight.onFight handler must be callable, fight.onDecision handler must be callable A TypeError The handler isn't a function Pass a function asterobot:fight describes them, and the console lines its hooks can cause.
Chat
say() and whisper() reject; the other functions of asterobot:chat throw at once.
Text Thrown by Cause Fix chat.say channel "<name>" isn't a channel: write it as the protocol does, "SALES" for Trade A TypeError rejecting say() The channel name is unknown Pass a protocol name, such as "SALES" or "SEEK" chat: <reason> say(), whisper() The message wasn't sent; the error's reason says why, such as "cooldown" or "missingItem" See say() and whisper() chat.pause seconds must be a positive number A TypeError from pause() Seconds that are 0, negative, or not a number Pass seconds above 0, or none to pause until resume() chat.setVariable value must be a string, a number or a boolean A TypeError from setVariable() An object, an array or a function as the value Pass text, a number or a boolean chat.setParts: chat: invalid settings: "<part>" isn't a part: write ads, replies, triggers, commands or status A TypeError from setParts() An unknown part Name ads, replies, triggers, commands or status chat.setProfiles[<i>]: chat: malformed profile: <field>: <why> A TypeError from setProfiles() A profile object Asterobot can't accept Fix the field it names chat.setProfiles: chatprofiles: no profile has this name: "<name>" An Error from setProfiles() A name no profile has Pass a name the bot's Chat tab lists asterobot:chat describes them all, and the console lines the guard writes.
Alarms
Text Thrown by Cause Fix alarms.raise name must be a non-empty string A TypeError from raise() The name isn't a string, or is empty Pass the event's name: raise("cheapItem") raises script:cheapItem alarms.raise text must be a string A TypeError from raise() The text is given and isn't a string Pass text, or leave it out alarms.onAlarm handler must be callable A TypeError from onAlarm() The handler isn't a function Pass a function asterobot:alarms describes them.
Packages and settings
Text Returned by Cause Fix packages.info requires a module or package name Thrown as a TypeError by packages.info() The name is missing, empty or not a string Pass a package name package "<name>" is not installed Thrown by packages.info() No package of the running script has that name, asterobot:parameters included Check the name. See asterobot:runtime. package "<name>" is ambiguous across <count> locked versions Thrown by packages.info() The dependencies lock several versions of that package Nothing to fix in the call: the name can't pick one parameters.onChange handler must be callable Thrown as a TypeError by onChange() The handler isn't a function Pass a function 'Set' on a dynamic object returned false Thrown as a TypeError by assigning to a property of values values is read-only Don't assign to values Could not delete property "<name>" of a dynamic object Thrown as a TypeError by deleting a property of values values is read-only Don't delete from values While the script stops
A call made while the script is stopping rejects, or throws, with the reason the script is stopping, and the promises still pending when it stops reject with that reason too. After Stop, the reason is behavior runtime closed. There's nothing to fix: the script is ending.
Console lines
These lines appear on the bot's Console tab. The script keeps running.
Line Level Cause Fix Refused to forward a message that would re-authenticate a relayed session Warning On a MITM bot, the script sent IdentificationRequest to the server. Asterobot didn't forward it, and send() resolved anyway. Identify only when launch.reason === "initial", as in Identify and run interceptor failed: <error> Error An interceptor threw, or ran for more than 5 s Fix the handler, and keep it quick interceptor selector failed: <error> Error An interceptor's selector function threw Fix the selector fight decision hook failed: <error>: the AI decides Warning A fight decision hook threw, or ran for more than 5 s Fix the hook, and keep it quick fight decision hook answered <stage>: not an answer this stage takes: <shapes>: the AI decides Warning A hook answered an object of another shape Answer one of the stage's answers fight decision hook returned a Promise, which counts as no answer: a hook answers synchronously, so it can't be async Warning An async hook, or one returning a promise. Said once per script. Make the hook a plain function that returns its answer The script's decision can't be played: the fight module decides instead Warning The rules engine refused a hook's answer, for the reason that follows. Said once a turn. Answer an action the fighter can play The fight module stops the script's actions: <reason> Info A list of actions stopped early Nothing: the module decides again, and asks the hooks Nothing plays this fight unless the bot's script does: the server disconnects a character left idle for a few minutes Warning On a Full socket bot, the module is off for the fight, or doesn't play its kind Play the fight from the script, or have the module play it with enable() or setFights() build replacement payload for <type>: <error> Error An interceptor returned a { payload } that doesn't fit the message. <error> is one of the payload errors. Fix the payload re-encode <type>: <error> Error Asterobot couldn't rebuild the message with the new fields Return { payload } with the same message's 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 Error An interceptor returned { payload } for a message without a readable name Return { raw }, or leave that message alone A line is written for each message on which an interceptor fails. When several interceptors fail on the same message, only the first failure is written, and the tenth if there are that many, ending with (10 times). An async interceptor whose promise rejects doesn't write a line here: it stops the script with unhandled Promise rejection: <error>.
Saving settings and running actions
These appear as toasts on Package settings, on the bot's Settings tab.
Toast Text under it Cause Fix Couldn't apply the settings <name>: <reason> A value doesn't fit its declaration. The reasons are listed in Declarations. Nothing from the form is saved. Correct the value, then Apply again Couldn't apply the settings package "<package>" declares no parameter "<name>" The form holds a setting the package doesn't declare any more Click Refresh, then apply again Couldn't apply the settings package "<package>" is not part of this bot's behavior The bot's package changed since the form was loaded Click Refresh Couldn't start the action <argument>: <reason> An argument doesn't fit its declaration Correct the value in the dialog Couldn't start the action the running behavior declares no action "<action>" on package "<package>" The running script doesn't declare that action, for example because you added it in the editor without starting the script again Start the script again Couldn't start the action no behavior is running on this bot The script isn't running Play the script first The action's name followed by failed The error the action's run threw, or its promise's rejection reason The action failed. The script keeps running. Fix run When the declarations themselves can't be read, Package settings shows Couldn't read this package's settings instead of the form: Declarations lists those texts.
Saving the manifest
When you save a package's asterobot.json from Asteroboard and Asterobot refuses it, the reason shows under the manifest, or Couldn't save the manifest when there's none. Dependencies explains how to declare them.
The first four texts below come after invalid package manifest:. The last three are checked first, for dependencies that have no URL, and come on their own.
Text Cause Fix empty dependency specifier A dependency has no name Name the dependency dependency "<specifier>": version embedded in its own specifier: not a valid exact version: <details> The version written after @ in the dependency's name isn't one exact version Write an exact version, such as name@1.2.0 dependency "<specifier>": version "<version>" conflicts with the version already in its own specifier The dependency's name holds one version and its version field another Make them the same, or remove one dependency "<specifier>": invalid version "<version>": <details> The version isn't latest, a version or a version range Fix the version dependency "<specifier>" locks version "<version>" in its specifier but declares "<version>" The version in the name and the declared version differ Make them the same dependency "<specifier>" needs an exact version A dependency without a URL has no exact version Give an exact version dependency "<specifier>" needs <name>@<version> installed, or a URL to fetch it from A dependency without a URL isn't installed at that version Install it, or give its asterobot.net URL
  • 61 vues
What asterobot:extension modules will be for, and what happens when a script imports one today.
Extension modules are extra modules that Asterobot will be able to offer scripts, next to the twelve built-in ones, for abilities those don't cover. Their names will start with asterobot:extension/, and a script will import them like any built-in module.
Note
Coming soon. No extension module is available to scripts yet. Until then, a script can use everything the built-in modules described in this reference offer.
Importing one today
A script that imports a module whose name starts with asterobot:extension/ doesn't start. The same goes for any other asterobot: name that isn't a built-in module, such as asterobot:fs. The reason is:
resolve "asterobot:extension/<name>" imported by "<module>": built-in module "asterobot:extension/<name>" is not registered As for any import that can't be resolved, the toast after Play or Run doesn't show this text. The bot's Settings tab does: Package settings shows Couldn't read this package's settings with the reason. Error messages lists the other texts you can meet there.
The modules a script can import today are:
asterobot:protocol, to exchange messages with the game; asterobot:bot, for the session, the bot's console and disconnecting; asterobot:timers, for sleep(); asterobot:gamedata, to read game texts and data; asterobot:console, to write to Asterobot's log; asterobot:runtime, to read package information; asterobot:parameters, to read the package's settings; asterobot:pathfinding, to find routes and work with cell positions; asterobot:movement, to walk the character, take it to the next map and use the elements of its map; asterobot:verification, to answer the game server's proof of identity; asterobot:character, to read the character's level, kamas, life, pods, items and jobs, and whether it's in a fight; asterobot:fight, to drive the fight module, follow fights and take its decisions; asterobot:chat, to say things in the game chat through the bot's chat guard; asterobot:alarms, to raise events for the alarms' rules and hear the bot's.
  • 60 vues
A script that writes the game chat to the bot's console, with package settings to pause it, keep only some channels and leave out some players.
This script writes each chat line the bot receives to the bot's Console tab, as [GUILD] Airelle: anyone up for a dungeon?. It goes further than the handler in React to messages: package settings let people pause it, keep only the channels they choose and leave out some players, their own character included, and staff lines stand out as warnings.
The page assumes you've already created a package and run it on a bot. If you haven't, start with Create a package and Identify and run.
The script
In Library Manager, click New package and name it chat-logger. Replace the contents of index.js with the code below, and save. import { session, botInfo, botWarn } from "asterobot:bot"; import { values } from "asterobot:parameters"; import { on, send } from "asterobot:protocol"; /** @type {BehaviorParameters} */ export const parameters = { enabled: { type: "bool", label: "Log chat", default: true, }, channels: { type: "array", of: "string", label: "Channels", description: "Channel names to log, such as GUILD or PARTY. Leave it empty to log every channel.", }, ignored: { type: "array", of: "string", label: "Ignored characters", description: "Character names whose lines aren't logged. Add your own to leave out what you say.", }, }; 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 || !values.enabled) return; const { senderName, content, fromAdmin } = traffic.payload; // A channel Asterobot has no name for reads as a number. const channel = String(traffic.payload.channel); // Read on every line, so an applied change counts from the next one. const channels = values.channels.map((name) => name.trim().toUpperCase()); if (channels.length > 0 && !channels.includes(channel)) return; const sender = senderName.toLowerCase(); if (values.ignored.some((name) => name.trim().toLowerCase() === sender)) return; const line = `[${channel}] ${senderName}: ${content}`; // Staff lines are rare and usually matter, so they show as warnings. if (fromAdmin) { botWarn(line); } else { botInfo(line); } }); // 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, }); } } How it works
The settings
export const parameters declares three settings. They show on the bot's Settings tab, under Package settings, in the chat-logger section:
Setting Type On the bot's page Default enabled bool Log chat, a switch On channels array of string Channels, a list with Add under its rows Empty ignored array of string Ignored characters, a list with Add under its rows Empty A list can't offer a drop-down, because choices only works on string and int settings, so people type the names themselves. The script forgives the usual slips: it trims spaces, compares channel names in capitals, and ignores upper and lower case in character names. Declare settings covers every field used here.
Reading the settings on every line
The handler reads values.enabled, values.channels and values.ignored each time a chat line arrives. values always holds what was last applied on the bot's page, so a change counts from the next line, with no restart. That's also why turning Log chat off doesn't stop the script: the handler still runs for each line, and returns straight away. Read settings explains values.
The handler awaits nothing, so it needs no try/catch. The check at its top keeps it from reading a payload that isn't there.
Channel names
channel reads as the name of the channel the line was said on. The ones you'll use most:
Name Channel GLOBAL The general channel, which the players around the character see GUILD Guild ALLIANCE Alliance PARTY Party TEAM Team SALES Trade, where players advertise what they sell SEEK Recruitment, to look for players or groups COMMUNITY The community channel, split by language The Dofus protocol reference lists every channel.
When Asterobot has no name for a channel's value, channel reads as that value's number instead of a name. String() turns it into text, so a number typed in Channels matches it like a name would.
Your own lines
The game sends your character's lines back to you like everyone else's, so the logger receives them too. session doesn't say which character the bot plays, so the plain way to leave your lines out is to put your character's name in Ignored characters. The same list works for any player you'd rather not read.
Staff lines
fromAdmin is true on lines the game marks as staff lines. The script writes those with botWarn() instead of botInfo(), so they show at the WARN level among the INFO lines.
Run it
Test it on a MITM bot (man-in-the-middle: the bot relays the game session of the Dofus client you play).
Bring your MITM bot online and enter the game with your character, as Your first bot shows. In the editor, pick that bot and click Run. A toast says Running on followed by the bot's name. On the bot's page, open Console. Under Channels, at the left of the chat box, untick Asterobot. Say something in the Dofus chat. Under the chat line itself, in its channel's colour, a script line such as [GLOBAL] Airelle: hello appears. Then try the settings:
Open the bot's Settings tab, then Package settings. In the chat-logger section, click Add under Ignored characters and type your character's name. Not applied appears next to the package's name. Click Apply. A toast says chat-logger settings applied. Say something in the Dofus chat again. This time only the chat line appears: the script logs nothing. Click Add under Channels, type GUILD and click Apply. From now on, only guild lines are logged. Turn off Log chat and click Apply. Nothing is logged any more, yet the bot's header still shows Stop: the script runs and waits for the switch to come back on. The settings are saved with the bot, so they're still there the next time the script starts.
The script runs on a Full socket bot too, but chat only reaches a character that is in the game. This script identifies the bot and nothing more, so on a Full socket bot it needs a connection script that also chooses a character.
Ideas to take it further
Highlight some words. Add a keywords list setting, and write the lines that contain one of them with botWarn(). Ignore a person rather than a character. senderAccountId stays the same when a player switches characters. It's a 64-bit field, so it reads as a BigInt: compare it with BigInt values, as Payloads explains. Count lines per channel. Keep the counts in a Map, and add a button that writes them to the console: see Declare actions. Answer some lines. Send messages shows how to reply and check that the reply went through. Next, the Inventory watcher follows kamas, pods and item quantities.
  • 68 vues
A script that follows the character's kamas, pods and stack quantities, and writes every change to the bot's console.
This script keeps track of what the character carries and writes each change to the bot's console: kamas spent or earned, pods going up or down, and the new quantity of a stack. It listens to the same messages as the bot's Inventory tab, and shows how a script keeps what it learned from one message to the next.
You need a MITM bot, as Your first bot sets up, and to know how to create a package and run it, as in Create a package.
The messages it follows
Message What it carries Fields the script reads InventoryContentEvent The whole inventory, sent once when the character enters the game kamas, and objects, with one entry per stack whose item holds the stack's uid, gid and quantity KamasUpdateEvent The character's kamas quantity, the new balance InventoryWeightEvent The character's pods inventoryWeight, what the inventory weighs, and weightMax, the most it can weigh before the character is overloaded ObjectsQuantityEvent New quantities for stacks the character already has object, a list of entries with objectUid and quantity ObjectQuantityEvent A new quantity for one stack object, one entry with objectUid and quantity These are the messages behind the Live inventory switch of the bot's Inventory tab. That switch only changes what Asteroboard shows: the script receives the messages whether it's on or not.
The script
Create a package named inventory-watcher, replace the contents of its index.js with this code, and save:
import { session, botInfo, botWarn } from "asterobot:bot"; import { on, send } from "asterobot:protocol"; // What the script knows about the inventory. It all starts empty at each // start, and fills in as the game reports things. let kamas; // a BigInt, once known let pods; // { current, max }, once known const stacks = new Map(); // uid => { gid, quantity } function signed(change) { return change >= 0 ? `+${change}` : `${change}`; } function updateStack(uid, quantity) { const stack = stacks.get(uid); if (!stack) { // The last full inventory didn't have this stack, so there's nothing to compare. botInfo(`Stack ${uid}: ${quantity}`); return; } const change = quantity - stack.quantity; stack.quantity = quantity; botInfo(`Stack ${uid} (item ${stack.gid}): ${quantity} (${signed(change)})`); } export default async function behavior(launch) { on("InventoryContentEvent", (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; kamas = traffic.payload.kamas; stacks.clear(); for (const { item } of traffic.payload.objects) { // item holds a message, so it's only there when the game set it. if (item) stacks.set(item.uid, { gid: item.gid, quantity: item.quantity }); } botInfo(`Inventory received: ${stacks.size} stacks, ${kamas} kamas`); }); on("KamasUpdateEvent", (traffic) => { const balance = traffic.payload?.quantity; if (balance === undefined) return; // The game sends the new balance, not what changed. const change = kamas === undefined ? "" : ` (${signed(balance - kamas)})`; kamas = balance; botInfo(`Kamas: ${balance}${change}`); }); on("InventoryWeightEvent", (traffic) => { if (!traffic.payload) return; const { inventoryWeight: current, weightMax: max } = traffic.payload; // Not worth a line when neither number moved. if (pods && pods.current === current && pods.max === max) return; const change = pods ? ` (${signed(current - pods.current)})` : ""; pods = { current, max }; if (current > max) { botWarn(`Pods: ${current} / ${max}${change}, overloaded`); } else { botInfo(`Pods: ${current} / ${max}${change}`); } }); on("ObjectsQuantityEvent", (traffic) => { for (const record of traffic.payload?.object ?? []) { updateStack(record.objectUid, record.quantity); } }); on("ObjectQuantityEvent", (traffic) => { const record = traffic.payload?.object; if (record) updateStack(record.objectUid, record.quantity); }); // 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, }); } } How it works
What the script remembers
kamas, pods and stacks sit at the top of the module, outside the handlers, so every handler reads and changes the same values. They start empty at each start of the script. Messages that arrived before the start are never delivered, and the game only sends the whole inventory as the character enters the game, so until then the script knows nothing about the stacks. Play, stop and reload explains what a new start keeps.
Totals, not changes
Each of these messages carries a new total: the new balance, the new weight, the stack's new quantity. To say what changed, the script keeps the previous value and subtracts it. The first value it gets has nothing to be compared with, so its line has no change in brackets.
Kamas are BigInt values
kamas, and quantity in KamasUpdateEvent, are 64-bit fields, so they read as BigInt values such as 125000n. Subtracting a BigInt from another works, and so does comparing a BigInt with 0, which is why signed() serves kamas and pods alike. Arithmetic that mixes a BigInt with a regular number throws a TypeError, though: kamas - 500 fails where kamas - 500n works. Pods and stack quantities are 32-bit fields, and read as regular numbers. Payloads has the rules.
Logging a BigInt writes its digits, without the n.
uid and gid
A stack has two ids. uid names this very stack, and quantity updates point to it as objectUid. gid names the item the stack is made of, and every stack of that item shares it. That's why stacks is keyed by uid. The script keeps the gid anyway: the next tutorial turns it into the item's name.
In InventoryContentEvent, each entry of objects wraps its stack. Besides item, it says where the item is equipped and whether it's a favourite. item holds a message, which is only in the payload when the game set it, so the script checks it before reading it.
What it doesn't see
A stack the character didn't have before, such as a new loot, doesn't come through these messages: Asterobot has no readable name yet for the messages the game uses for it. A stack that leaves the inventory completely can go unnoticed too. The script learns about both with the next InventoryContentEvent, when the character enters the game again. Until then, a quantity update for a stack it doesn't know only gives the uid and the new quantity.
Run it
The whole inventory only arrives as the character enters the game, so the script has to be running by then.
Start waiting on your MITM bot (man-in-the-middle: the bot relays the game session of the Dofus client you play), start Dofus and sign in. Stop at the character selection screen. If your character is already in the game, go back to that screen. Once the bot's header shows Connected to game server, pick the bot in the editor and click Run. On the bot's page, open Console and untick Asterobot under Channels, at the left of the chat box. Choose your character in Dofus. A line such as Inventory received: 57 stacks, 125000 kamas appears. Play as usual. When your kamas, your pods or the quantity of one of your stacks change, a line appears. The lines look like this:
Inventory received: 57 stacks, 125000 kamas Pods: 1250 / 3000 Kamas: 124500 (-500) Pods: 1262 / 3000 (+12) Stack 812734 (item 289): 43 (+3) The script only reads. Apart from the identification on a Full socket bot, it sends nothing to the game.
Ideas to take it further
Names instead of ids. Item names from game data turns each gid into the item's name. A warning before the pods are full. Read settings builds one, with a setting for the limit. Kamas earned since the start. Keep the first balance the script receives, and add a button that writes the difference: see Declare actions. Stop a Full socket bot when its inventory is full. Sessions shows disconnect() on the same message, and why a MITM bot must never call it.
  • 79 vues
Turn the item ids of the inventory into names with one SQL query on the game data, and write quantity changes and the whole inventory by name.
Messages name items by number. This script turns those numbers into the names players know, with asterobot:gamedata: when a stack's quantity changes, the console reads Blé: 43 (+3) rather than Stack 812734 (item 289): 43 (+3). A List inventory button also writes the whole inventory by name.
It starts from the stacks of the Inventory watcher and leaves kamas and pods out. Read that page first if uid and gid are new to you.
Find the names in the game data
InventoryContentEvent gives each stack a gid: the id of the item the stack is made of. The item's name is in the game data of the bot's game version, in two tables:
items has one row per item. Its id column holds the id the game uses in its messages, the gid. Its _id column is the key Asterobot gives the row, which isn't always the same number, as Lookups explains. _names holds the names of records, one row per name and per language. record_table and record_id point to the record by its key, language holds the language code, and ordinal 0 marks the record's main name. So a query finds the items by id, and joins their names on _id. Try it in the SQL tab before writing any code:
On the bot's Inventory tab, with Live inventory on, open a row. Its details show the item's id after item. In Game Manager, open the bot's game version, go to its Game data tab, then to SQL. Type this query, with ids from your inventory in place of 289, 290, and click Run: SELECT i.id, n.name FROM items i JOIN _names n ON n.record_table = 'items' AND n.record_id = i._id AND n.language = 'fr' AND n.ordinal = 0 WHERE i.id IN (289, 290) Each item comes back on its own row, with its French name. SQL queries describes the tab.
The script
Create a package named item-names, replace the contents of its index.js with this code, and save:
import { session, botInfo, botWarn } from "asterobot:bot"; import { query } from "asterobot:gamedata"; import { on, send } from "asterobot:protocol"; // A query returns 5,000 rows at most, so ids are looked up in batches. const BATCH_SIZE = 500; function namesQuery(count) { return ` SELECT i.id, n.name FROM items i JOIN _names n ON n.record_table = 'items' AND n.record_id = i._id AND n.language = ? AND n.ordinal = 0 WHERE i.id IN (${Array(count).fill("?").join(", ")})`; } const stacks = new Map(); // uid => { gid, quantity } const names = new Map(); // gid => name, kept for the whole run async function loadNames(gids) { const missing = [...new Set(gids)].filter((gid) => !names.has(gid)); for (let start = 0; start < missing.length; start += BATCH_SIZE) { const batch = missing.slice(start, start + BATCH_SIZE); const result = await query(namesQuery(batch.length), [session.language, ...batch], { limit: batch.length, }); // undefined rather than a result: this game version has no game data. if (result === undefined) throw new Error("No game data for this bot's game version"); for (const [gid, name] of result.rows) names.set(gid, name); } } function nameOf(gid) { return names.get(gid) ?? `item #${gid}`; } function updateStack(uid, quantity) { const stack = stacks.get(uid); // Only stacks from the last full inventory have an item to name. if (!stack) return; const change = quantity - stack.quantity; stack.quantity = quantity; botInfo(`${nameOf(stack.gid)}: ${quantity} (${change >= 0 ? "+" : ""}${change})`); } /** @type {BehaviorActions} */ export const actions = { listInventory: { label: "List inventory", description: "Writes every stack the character carries to the bot's console.", run() { if (stacks.size === 0) { botInfo("The game hasn't sent the inventory since the script started."); return; } const lines = [...stacks.values()].map((stack) => `${nameOf(stack.gid)} x${stack.quantity}`); for (const line of lines.sort()) botInfo(line); }, }, }; export default async function behavior(launch) { on("InventoryContentEvent", async (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; // Filled before the lookup, so quantity updates arriving meanwhile find their stack. stacks.clear(); for (const { item } of traffic.payload.objects) { if (item) stacks.set(item.uid, { gid: item.gid, quantity: item.quantity }); } try { await loadNames([...stacks.values()].map((stack) => stack.gid)); const named = [...stacks.values()].filter((stack) => names.has(stack.gid)).length; botInfo(`Inventory received: ${stacks.size} stacks, ${named} of them named`); } catch (error) { // An error that escapes a handler stops the whole script. botWarn("Couldn't look up item names:", String(error)); } }); on("ObjectsQuantityEvent", (traffic) => { for (const record of traffic.payload?.object ?? []) { updateStack(record.objectUid, record.quantity); } }); on("ObjectQuantityEvent", (traffic) => { const record = traffic.payload?.object; if (record) updateStack(record.objectUid, record.quantity); }); // 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, }); } } How it works
One query for the whole inventory
Looking items up one at a time would take a find() call per item. A single query() with IN (?, ?, ...) names the whole inventory at once, and like every game data call that returns a promise, it does its work outside your code while the bot keeps handling messages. Game data overview explains the difference.
A few details of loadNames():
namesQuery() writes one ? for each id. The values go in the params array in the order of the ? in the SQL: the language first, then the ids. Parameters keep their type, so the ids are compared as numbers. query() returns 200 rows unless you pass a limit, and 5,000 at most. Each batch asks for as many rows as it has ids, and BATCH_SIZE keeps every batch far below that maximum. Rows are arrays in the order of the SELECT, so const [gid, name] reads the id first and the name second. query() resolves to undefined when the bot's game version has no extracted game data. The script throws then, and the handler's catch writes why on the console. Names in the script's language
The query asks for names in session.language. For every start made from Asteroboard that's fr, whatever language the bot itself is set to, so the names come in French. Game data overview explains why. To get another language, pass its code instead of session.language.
Stacks first, names second
The InventoryContentEvent handler fills stacks before it awaits the query. Handlers don't wait for each other, so a quantity update can arrive while the names are still being looked up, and it still finds its stack. Until the query returns, such a stack is written as item # followed by its gid, which is also how an item the game data doesn't know is written.
names lasts for the whole run. When the inventory arrives again, only items without a name yet are looked up. Like every variable, it starts empty at the next start.
The List inventory button
export const actions puts a List inventory button in the package's section of the bot's Settings tab. Its run reads the same stacks and names as the handlers, and writes one line per stack. sort() compares characters one by one, so capitals come before small letters and names starting with an accented letter come after z.
Asterobot keeps the last 200 lines of each bot's console, so a large inventory pushes older lines out of what it keeps. Declare actions covers buttons.
Run it
Check that the bot's game version has game data: in Game Manager, its row should show Ready for bot. A MITM bot (man-in-the-middle: the bot relays the game session of the Dofus client you play) can run without it, and the script then writes Couldn't look up item names: Error: No game data for this bot's game version. Extracting the data while the script runs isn't enough: start the script again afterwards. As for the Inventory watcher, start waiting on your MITM bot, start Dofus and stop at the character selection screen. Once the bot shows Connected to game server, pick it in the editor and click Run. On the bot's Console tab, untick Asterobot under Channels, then choose your character in Dofus. A line such as Inventory received: 57 stacks, 57 of them named appears. Play. When the quantity of one of your stacks changes, a line such as Blé: 43 (+3) appears. Open the bot's Settings tab, then Package settings, and click List inventory under Actions. A toast says Action started, then another one says listInventory finished, and the console lists your stacks by name. Ideas to take it further
Another language. _names has rows for each language the game data holds. Run SELECT DISTINCT language FROM _names in the SQL tab to see which, then add a string setting with choices and pass values.language to the query instead of session.language. The item's type. Join item_types on item_types.id = items.type_id, then its name through _names in the same way. The SQL tab's Examples on items do something similar. Watch a few items. Add a list setting of item names, and write a warning when one of those stacks reaches a quantity you choose. From a name to an id. search("Blé", { table: "items" }) finds items by name. Each result's id is the row's key: read the row with record() to get its id column, the one messages use. See Lookups. The Dofus protocol reference lists the other messages you can build on, and Game data overview everything else a script can read.
  • 71 vues
How the memory a bot's scripts share works, what it keeps and for how long, how to keep state for one game connection or for the whole bot, and when it's emptied.
A script's variables are gone every time it stops, whether through Stop, Run in the editor or a dropped connection. The bot's memory is where a script keeps what it must find again: a count, how far it got, a choice it made. It belongs to the bot, not to the script, and every package the bot runs reads and writes the same one.
This page explains how it behaves and how to use it. asterobot:bot has the reference of its four functions.
A first example
This script counts the chat messages its bot receives. The count goes on across Stop and Play, and across reconnections:
import { memory, session, botInfo } from "asterobot:bot"; import { on, send } from "asterobot:protocol"; const KEY = "@alice:chat-count/total"; export default async function behavior(launch) { on("ChatChannelMessageEvent", () => { const total = (memory.get(KEY) ?? 0) + 1; memory.set(KEY, total); botInfo(`${total} chat messages seen by this bot`); }); // 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 the script after three messages and play it again: the next message shows 4 chat messages seen by this bot. A variable in the module would have started again from 0.
How it works
The memory is a set of keys, each naming one value.
Function What it does memory.get(key) Returns a copy of the value stored under key, or undefined memory.set(key, value) Stores a copy of value under key, replacing what was there memory.delete(key) Removes key, and returns whether it was there memory.keys() Returns every stored key, sorted All four are synchronous: the memory is held by Asterobot, in the computer's memory, and nothing is written to disk.
Keys
A key is a string of up to 256 bytes. Every package on the bot sees every key, so start yours with your package's name, as in @alice:chat-count/total. Two packages then never pick the same key by accident. Nothing checks it: it's a convention.
Values
A value is plain data: null, a boolean, a number, a string, a BigInt, an array or a plain object, nested at most 32 levels. BigInts stay BigInts, which matters for the ids messages carry, such as map and character ids.
Anything else throws a TypeError that says where in the value the problem is. That includes undefined, a function, NaN, a Map, a Date, a symbol and an instance of a class. To remove a key, use memory.delete(), not undefined. To keep a Date, store date.getTime().
Values are copies
set() stores a copy, and get() returns a new copy each time. Changing what you read changes nothing stored:
import { memory } from "asterobot:bot"; const KEY = "@alice:farm/route"; // Adds a cell to the stored route. export function addCell(cell) { const route = memory.get(KEY) ?? { cells: [] }; route.cells.push(cell); // Changes this copy only. memory.set(KEY, route); // Now it's stored. } The keys of an object you read back come sorted by name, whatever order you wrote them in.
How long it lasts
The memory belongs to the bot. It's created with the bot, and it's gone only when the bot is:
What happens The script's variables The bot's memory session.connectionId currentMap() Stop, then Play Start over Kept Same Kept Run from the editor, or loading another package Start over Kept Same Kept The script stops with an error Start over Kept Same Kept Apply on the bot's Package settings, or a bot setting other than its account or game version changed Kept: the script keeps running Kept Same Kept The game connection closes, and the bot connects again Start over Kept One more undefined until the character is on a map A new Dofus session through a MITM bot Start over Kept One more undefined until the character is on a map The bot's Ankama account or game version is changed Start over Emptied: the bot is rebuilt Counts from 1 again undefined The bot is deleted Gone Gone Asterobot restarts Gone Emptied for every bot Counts from 1 again undefined A bot can't read another bot's memory, and every bot has its own.
Choosing how long your data lives
The memory keeps everything until the bot goes away. Most data belongs to something shorter, so store what it belongs to next to it, and ignore a value that belongs to something else.
For the whole bot
Store the value as it is, as the first example does. It's there on every connection, until the bot is rebuilt or Asterobot restarts.
For one game connection
Much of what a script learns only holds for one game connection: the connection was identified, the character was selected, the server sent three polls. After a reconnection it's wrong, and must be ignored.
session.connectionId says which connection a script runs on: a BigInt that grows by one each time the bot gets a new game connection, and stays the same for every start on one connection. Store it next to the value:
import { memory, session, botInfo } from "asterobot:bot"; import { on, send } from "asterobot:protocol"; const KEY = "@alice:chat-count/connection"; 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, }); } } launch.reason can't replace the id. "initial" means no start has succeeded on the connection yet: a start that fails half way, after sending something, leaves the next one "initial" on the same connection. See When a start counts.
Keep the id inside the value rather than in the key. A key such as `@alice:chat-count/${session.connectionId}` adds a new key for every connection, and they pile up until the memory is full. With the id inside, each new connection's first set() replaces the old value.
For one character
The same bot can play different characters over time. For data about one character, store its id, from currentMap()?.characterId, next to the value, the same way as the connection's id.
Keeping a connection's state
A script that goes through steps, such as identifying and then waiting for its character, needs to know on each start how far the connection already got. Keep that in one object per connection, under one key, with the connection's id and a field for each step that has happened:
import { memory, session, botInfo } from "asterobot:bot"; import { on, send } from "asterobot:protocol"; const KEY = "@alice:hello/connection"; // The state of the current connection: what's stored, if it was stored on // this connection, or a fresh state with only the id. function connectionState() { const stored = memory.get(KEY); return stored?.id === session.connectionId ? stored : { id: session.connectionId }; } // Adds fields to the current connection's state. function updateConnection(changes) { memory.set(KEY, { ...connectionState(), ...changes, id: session.connectionId }); } export default async function behavior(launch) { on("CharacterLoadingCompleteEvent", () => { updateConnection({ loadedAt: Date.now() }); }); const state = connectionState(); if (state.loadedAt !== undefined) { const seconds = Math.round((Date.now() - state.loadedAt) / 1000); botInfo(`The character has been loaded for ${seconds} s`); } // 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) { updateConnection({ identified: true }); await send("IdentificationRequest", { ticketKey: session.gameToken, languageCode: session.language, }); } } This script uses the state to identify instead of launch.reason. That way it identifies exactly once per connection, even when a start fails half way.
How this state behaves:
A field is absent until its step has happened. A fresh state is { id } and nothing else, so state.loadedAt === undefined means "not loaded yet on this connection". Record before you send, when sending twice would be wrong. If the script stops between set() and send(), the next start knows it must not send again. The other order risks sending twice. Handlers keep it up to date, whichever step the script is at, so a script started again later reads what happened while it ran. It resets by itself on a new connection, without being deleted. The stored value keeps the old connection's id, so connectionState() reads it as a fresh state, and the first updateConnection() replaces it. It's empty after Asterobot restarts, like the rest of the memory. The bot comes back disconnected, so there's no connection left for it to describe anyway. @asterobot:base keeps its own state this way, under @asterobot:base/connection.
Sharing between packages
Every package the bot runs sees the same memory: its own package, its dependencies, and any package the bot plays later. A value one package stores is there for another to read.
// In @alice:scout: remember where the bot saw an ash tree. import { memory } from "asterobot:bot"; export function sawAshTree(mapId, cellId) { const trees = memory.get("@alice:scout/ash-trees") ?? []; memory.set("@alice:scout/ash-trees", [...trees, { mapId, cellId }]); } // In @bob:harvest: read what the scout found, if anything. import { memory } from "asterobot:bot"; export function ashTrees() { return memory.get("@alice:scout/ash-trees") ?? []; } A map id read from a message is a BigInt, and it comes back a BigInt: ashTrees()[0].mapId === 153879301n.
Only rely on keys a package says are meant to be shared, and only read them: the format of another package's key is that package's business, and can change with its next version. The keys of @asterobot:base are its own bookkeeping. Read them if you need to, and never write them.
Writing safely
A bot runs one script at a time, and all its packages run in that one script. The four functions are synchronous. So nothing else can write between your get() and your set(), as long as there's no await between them:
import { memory, botInfo } from "asterobot:bot"; import { sleep } from "asterobot:timers"; const KEY = "@alice:chat-count/total"; export async function countLater() { await sleep(1000); // Read, change and write with no await in between. Reading before the // sleep would lose whatever a handler added during it. const total = (memory.get(KEY) ?? 0) + 1; memory.set(KEY, total); botInfo("Total:", total); } set() throws when a value isn't plain data or doesn't fit the limits. In a handler, catch it: an error that escapes a handler stops the whole script.
import { memory, botWarn } from "asterobot:bot"; import { on } from "asterobot:protocol"; // Called from the entry function. export function rememberLastMessage() { on("ChatChannelMessageEvent", (traffic) => { try { memory.set("@alice:chat/last", traffic.payload?.content ?? null); } catch (error) { // An error that escapes a handler stops the whole script. botWarn("Couldn't store the message:", String(error)); } }); } Limits
The limits are for the whole bot, all packages together:
Limit Value Keys 1024 One key 256 bytes One value 64 KiB All values together 1 MiB Levels of arrays and objects in a value 32 A value's size is roughly the size of its JSON text, with every number counted as 8 bytes. memory.delete() frees what a key held. The memory is meant for small pieces of state, not for data sets: Limits gives the exact error each limit throws.
While Asterobot reads your package's settings
Asterobot also runs the top level of your modules when it reads your package's settings, without a bot. At that moment get() returns undefined, keys() returns an empty array, and set() and delete() throw, which makes the reading fail. So call the memory functions from your entry function and your handlers, as the samples on this page do, not from the top level of a module. See Declarations.
Seeing what's stored
On a bot's page, Tools > Memory lists every key with its value and when it last changed, under Key, Value and Last changed. It refreshes by itself when a script changes the memory, and only reads it.
An empty memory shows Nothing stored yet. A BigInt shows as its digits followed by n, as in 153879301n, and null shows as a dash. When the list can't be read, the page shows Couldn't read this bot's memory. The refresh button next to the description reads it again. The asterobot:bot reference has every rule and error of the four functions, and Sessions covers what else changes from one game connection to the next. The next chapter, Game protocol, goes into messages in detail.
  • 66 vues
commitment(), publicKey() and proof(), which compute the bot's answers to the proof of identity the game server asks for on each game connection, with when to call each one and every error.
Once a game session is open, the game server asks the client to prove who it is, in three exchanges. asterobot:verification computes what the bot answers with. Your script sends the messages, and the secrets behind the answers stay inside Asterobot: your code only ever sees the values that go on the wire.
Only a Full socket bot needs it. On a MITM bot (man-in-the-middle: the bot relays the game session of a Dofus client that someone plays), the Dofus client answers these exchanges itself, and a script answering too would send each answer twice.
Most scripts never import this module. @asterobot:base answers the three exchanges for every package that depends on it, on a Full socket bot, and leaves them to the Dofus client on a MITM bot. This page is for writing such a package yourself, or for understanding what base sends.
import { commitment, publicKey, proof } from "asterobot:verification"; These are the module's only exports. There's no verification object to import.
Export Returns What it gives you commitment() A string, right away The id of ClientIdRequest publicKey() A string, right away The challengeKey of ClientChallengeInitRequest proof() A string, right away The proof of ClientChallengeProofRequest Every value is a whole number written in base 10, which is how the three messages carry them. None of the functions returns a promise.
The three exchanges
The server sends Answer with Carrying ServerVerificationEvent ClientIdRequest id: commitment() ServerSessionReadyEvent ClientChallengeInitRequest challengeKey: publicKey() ServerChallengeEvent ClientChallengeProofRequest proof: proof(traffic.payload?.value) Answer each one as soon as it arrives, and only when it arrives: the Dofus client sends each of these requests from the handler of the message that asks for it, and from nowhere else.
When they come
ServerSessionReadyEvent closes the burst of messages that follows an accepted identification, once per connection. ServerVerificationEvent, then ServerChallengeEvent, come later and again from time to time: in captures, the first came about five and a half minutes into a session, and more followed. The client's code shows the poll can also arrive right after the connection opens, before the server has answered the identification. Register the handlers above the identification block, as the sample below does, so none can be missed. The challenge comes with a value or without one. Both happen, even on the same game launch. Without a value, the event is empty, and proof() works the challenge out itself. Send them with a uid
The Dofus client sends these three requests with a uid of its own, and nothing ever answers them. Send them the same way, with correlated: true:
await send("ClientIdRequest", { id: commitment() }, { correlated: true }); A plain send() would put -1 where the client puts a uid. A request() would wait for a response that never comes, and reject with context deadline exceeded once its timeout runs out. Sending explains the three ways to send.
Answer at once
Send each answer as soon as its event arrives. The server waits for each answer before its next step, so the three can't cross.
commitment()
commitment() returns a string.
Each call draws a new random secret, keeps it inside Asterobot, and returns the value that proves it without revealing it. That secret replaces the previous one and is what the next proof() uses. So call it once for every ServerVerificationEvent, and never ahead of time: the Dofus client does the same, and a second call before the challenge makes the proof use the newer secret.
Every call returns a different value.
publicKey()
publicKey() returns a string.
It returns the bot's public key, which the server checks every proof against. It's the same value every time, and it belongs to the bot rather than to your script. It stays the same when the script stops and plays again, and across the bot's reconnections, as the key of a Dofus client stays the same for as long as the client runs. Captures confirm it: a client's proofs on two connections of one game launch both verify against the key it sent on the first.
A bot gets a new key when Asterobot starts again, and when a change of its Ankama account or of its game version makes Asterobot rebuild it.
proof()
proof(challenge?) returns a string.
Parameter Type Description challenge string, undefined or null The value of the ServerChallengeEvent you're answering, exactly as it arrived Pass traffic.payload?.value. The field is optional, so it's absent when the server sent no value, and proof() then answers the way the Dofus client does in that case: it works the challenge out itself, from the values of the exchange and the device identifier the bot sent when it signed in (see Device identifier). A bot that didn't sign in on its own uses an empty one.
proof() draws nothing: it answers with the secret of the latest commitment(). Before the first commitment() of a bot, that secret is zero, which is also what the Dofus client answers with when a challenge comes before any ServerVerificationEvent.
A complete example
This script answers the three exchanges on a Full socket bot, and leaves them to the Dofus client on a MITM bot:
import { session, botWarn } from "asterobot:bot"; import { on, send } from "asterobot:protocol"; import { commitment, publicKey, proof } from "asterobot:verification"; async function answer(type, payload) { try { // With a uid, as the Dofus client sends these three. await send(type, payload, { correlated: true }); } catch (error) { // An error that escapes a handler stops the whole script. botWarn(`Couldn't send ${type}:`, String(error)); } } export default async function behavior(launch) { // On a MITM bot the Dofus client answers these itself. if (!session.shared) { on("ServerVerificationEvent", () => answer("ClientIdRequest", { id: commitment() })); on("ServerSessionReadyEvent", () => answer("ClientChallengeInitRequest", { challengeKey: publicKey() })); on("ServerChallengeEvent", (traffic) => { try { answer("ClientChallengeProofRequest", { proof: proof(traffic.payload?.value) }); } catch (error) { // proof() throws for a value that isn't a number, and an error // thrown in a handler stops the whole script. botWarn("Couldn't answer the challenge:", 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, }); } } A script that stops and plays again in the middle of a session keeps working with it: the key is the bot's, and a challenge is answered with the secret of the bot's latest commitment(), whichever run of the script made it.
Checked against the real client
The computation was read out of the Dofus client's code: the 768-bit group of RFC 2409 section 6.1 with G = 2, the proof (k + c·x) mod Q, and the challenge the client works out when the server sends none. The server checks a proof with G^proof = id × challengeKey^c (mod P).
Asterobot's tests check all of it against the real client too. They hold two checks a Dofus client answered, taken from captures of one game launch:
Check Challenge Result 1 None: the client worked it out The client's proof verifies against the challenge proof() works out 2 A value from the server The client's proof verifies against that value So the group, the proof and both kinds of challenge are the client's, and every answer these functions give passes the same check. The tests also cover the path from a script: a script answering the three events as the sample above does passes the check on both kinds of challenge.
Errors
Error Type Cause verification.proof challenge must be a decimal string, or undefined when the server sent none TypeError challenge is neither a string, undefined nor null verification: the challenge is not a decimal integer: "<challenge>" Error challenge isn't a whole number written in base 10. Spaces around it and a leading sign are accepted. String(error) gives TypeError: or GoError: followed by the text.
The three functions throw when the top level of a module calls them while Asterobot reads the package's declarations, with an error that ends with not available while reading a package's declarations: see Declarations. Call them from your handlers.
The Dofus protocol reference describes the six messages of the exchange.
  • 51 vues
A script that harvests what its character can on each map of a route, then takes the next map, with recipes for doors, suns, zaaps and maps whose side leads to several maps.
This script walks a route of maps. On each map it harvests every resource its character can, then leaves by the next side of the route. It shows how a script finds what can be used on a map, uses it, waits while a harvest lasts, and changes map, with asterobot:movement doing the walking. Recipes at the end cover doors, suns, zaaps and maps whose side leads to several maps.
You need a MITM bot, as Your first bot sets up, a character with a gathering job on a map that has resources it can harvest, and to know how to create a package and run it, as in Create a package.
The script
Create a package named harvest-route, replace the contents of its index.js with this code, and save:
import { botInfo, botWarn, currentMap, session } from "asterobot:bot"; import { record } from "asterobot:gamedata"; import { changeMap, useElement } from "asterobot:movement"; import { send, wait } from "asterobot:protocol"; // The sides to leave by, one map after the other. Write your own route here. const ROUTE = ["right", "right", "bottom"]; // A harvest action gathers an item, and the skills table says which one. // Every other action (use, save, open, sit...) gathers none. function harvests(skillId) { return (record("skills", skillId)?.gathered_ressource_item ?? -1) > 0; } // The elements of the character's map with a harvest action on offer right // now, and that action. function harvestable() { return (currentMap()?.elements ?? []).flatMap((element) => { const skill = element.skills.find((offer) => harvests(offer.id)); return skill ? [{ id: element.id, skillId: skill.id }] : []; }); } async function harvestMap() { for (const { id, skillId } of harvestable()) { try { const use = await useElement(id, { skill: skillId }); botInfo(`Harvesting element ${id} for ${use.duration} ms`); // The character stays busy until the game says the harvest is over. if (use.duration > 0) { await wait( (traffic) => traffic.type === "InteractiveUseEndedEvent" && traffic.payload?.elementId === id, { timeout: use.duration + 5000 }, ); } } catch (error) { // Harvested by someone else meanwhile, out of reach, or refused by the // game: leave it and try the next one. botWarn(`Couldn't harvest element ${id}: ${String(error)}`); } } } 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, }); } for (const side of ROUTE) { await harvestMap(); try { const { mapId, cellId } = await changeMap(side); botInfo(`Left by the ${side}: now on map ${mapId}, cell ${cellId}`); } catch (error) { botWarn(`Couldn't leave by the ${side}: ${String(error)}`); return; } } await harvestMap(); botInfo("Route done"); } How it works
Finding what to harvest
currentMap() lists the map's interactive elements in elements. Each one has its id and the actions it offers right now in skills, each with the skill id. A resource that was just harvested offers nothing until it regrows, so it's left out on its own.
The elements don't say what they are. The skill does: each skill is a row of the game data's skills table, and a harvest skill's gathered_ressource_item is the item it gathers, such as 289, wheat, for skill 45. Other skills, such as 184, "Use", have -1 there. To harvest only one kind of resource, compare gathered_ressource_item with that item's id instead.
harvestable() reads the map once, when harvestMap() starts. By the time the script gets to an element, another player may have harvested it: useElement() then rejects, and the script moves on.
Using an element
useElement(id, { skill }) does what the Dofus client does when the player clicks the element: the character walks within reach of it, then the game is asked to use it. Naming the skill matters on elements that offer several actions: without it, useElement() rejects with move: the element offers several actions; name the one to use.
The promise resolves as soon as the game accepts the use, with duration, how long the harvest lasts in milliseconds. During that time the character can't use anything else, and useElement() would reject with move: the character is still using an element. So the script waits for InteractiveUseEndedEvent about that element, which the game sends when the harvest is over. Like every wait(), it has a timeout, here the harvest's duration and five seconds more.
Moving on
changeMap(side) walks the character to the exit of that side it reaches soonest, takes it to the next map, and resolves once the new map has loaded, so the next harvestMap() reads the new map's elements. When a side has no exit, or none the character can walk to, it rejects, and the script stops there with a line in the console.
What the script sends
useElement() and changeMap() send the walks, the confirmations a Full socket bot needs, InteractiveUseRequest and MapChangeRequest. On a MITM bot your Dofus client loads each new map. The script itself sends nothing but the identification, on a Full socket bot.
On a Full socket bot
The identification doesn't take a Full socket bot's character into the game: choosing the character and entering the world are a script's too. @asterobot:base does all of it. In a package that depends on base, replace the identification block with await connect() from @asterobot:base, which resolves once the character stands on its map. Base also asks for every new map, and changeMap() would do it itself if nothing did.
Run it
Start waiting on your MITM bot, start Dofus and sign in, and choose a character with a gathering job. Walk it to a map with resources it can harvest. Change ROUTE to the sides that lead from that map to the next ones you want to harvest. In the editor, pick the bot and click Run. On the bot's page, open Console and untick Asterobot under Channels, at the left of the chat box. Your character walks to each resource and harvests it, then leaves the map, as if you had clicked. Don't click in Dofus while it runs: your own walk would replace the script's. The lines look like this:
Harvesting element 489389 for 3000 ms Harvesting element 489391 for 3000 ms Couldn't harvest element 489402: GoError: move: the server refused the use (element 489402) Left by the right: now on map 154010883, cell 293 Harvesting element 490017 for 3000 ms Route done The bot's Map tab shows the same walks live, and its Resources and objects list turns each harvested resource Unavailable.
Recipes
These pieces go inside a script like the one above, which already imports botInfo, botWarn, currentMap, record, changeMap, useElement and wait. Add query from asterobot:gamedata where a recipe uses it.
Go through a door
A door, a staircase or a teleporter is an element whose action takes the character to another map. The game's travel graph says which elements of a map do it, with the action to use and the map they lead to:
const result = await query( `SELECT t.transition_id, t.skill_id, destination.map_id FROM world_transitions t JOIN world_vertices origin ON origin.uid = t.from_uid JOIN world_vertices destination ON destination.uid = t.to_uid WHERE origin.map_id = ? AND t.type = 32`, [Number(currentMap().mapId)], ); // [elementId, skillId, destinationMapId] for each door of the map, or nothing // when the bot's game version has no game data. const doors = result?.rows ?? []; useElement() resolves once the game accepts the use, and the new map comes a moment later. To go on once it has loaded, wait for its MapComplementaryInformationEvent, started before the use so it can't be missed:
const [elementId, skillId] = doors[0]; const before = currentMap().mapId; await Promise.all([ wait( (traffic) => traffic.type === "MapComplementaryInformationEvent" && traffic.payload?.mapId !== before, { timeout: 30000 }, ), useElement(elementId, { skill: skillId }), ]); On a Full socket bot, the new map only loads when something asks for it: @asterobot:base does, or your script answers MapCurrentEvent and MapCurrentInstanceEvent itself. Unlike changeMap(), a door's use doesn't do it for you.
Take a sun
An exit drawn as a sun is an element whose only action is skill 339, Point Out an Exit. useElement() walks the character onto it, and resolves on the next map, once that map has loaded:
const sun = currentMap()?.elements.find( (element) => element.skills.length === 1 && element.skills[0].id === 339, ); if (sun) { const { mapId } = await useElement(sun.id); botInfo(`Through the sun to map ${mapId}`); } Use a zaap
A zaap offers two actions: 114, "Use", which opens its destinations, and 44, "Save", which saves it as your respawn point. Name the one you want:
const zaap = currentMap()?.elements.find((element) => element.skills.some((offer) => offer.id === 44)); if (zaap) await useElement(zaap.id, { skill: 44 }); Choosing a destination after Use isn't built in: the game sends the destinations, and travelling to one is a message of its own.
Leave from a given exit
On a few maps, one side leads to different maps depending on the exit cell, such as the top and the bottom of the right side. Pass the cell to leave from:
await changeMap("right", { cell: 475 }); Choose the exit shows how to list a side's exit cells and find which ones lead where.
Try again after a refusal
The game can refuse a walk, for example when another actor got in the way. The error's message says so, and a script can try again:
async function changeMapPatiently(side, tries = 3) { for (let attempt = 1; ; attempt++) { try { return await changeMap(side); } catch (error) { if (attempt >= tries || !String(error).includes("the server refused the walk")) throw error; botWarn(`Walk refused, trying again (${attempt}/${tries})`); } } } asterobot:movement lists every error text these functions reject with.
Ideas to take it further
A route from the bot's settings. Declare the route as a setting, as Declare settings shows, so it changes without editing the script. Only one resource. Keep the elements whose skill gathers the item you want, with its id from Item names from game data. Stop when the pods are full. Inventory watcher follows the pods: end the route when they reach the maximum. Loop the route. Make it a circle, such as right, bottom, left, top, and run it until the script is stopped.
  • 107 vues
Give your package's own functions to the Studio with export const steps and export const conditions, with their fields, what run receives and returns, the ref field that asks for game data, and the errors Asterobot reports.
The Studio is where a plan is drawn: places on the world map, the order the bot visits them in, and what it does at each one. What a plan can do in the game comes from packages. A step is something done on a map, such as gathering, and a condition is a yes/no question, such as "are the bags fuller than 90%?". Your package gives the Studio its own steps and conditions by declaring them next to its settings and actions.
Plans are played by @asterobot:base, so a package that draws them depends on it. The Studio shows how a player draws one with your steps and conditions.
A function and its declaration
A step or a condition is an ordinary function your package exports, which your own code can call like any other. The declaration only describes it for the Studio, and points to it with run. Steps go in an object exported as steps from index.js, and conditions in one exported as conditions:
import { session, botInfo } from "asterobot:bot"; import { send } from "asterobot:protocol"; import { sleep } from "asterobot:timers"; /** Waits the given number of seconds, and ends early when the plan stops. */ export async function rest({ seconds }, context) { for (let waited = 0; waited < seconds; waited++) { // A plan only stops between steps, so a long step checks for itself. if (context?.stopping) return; await sleep(1000); } } /** Whether the clock of the computer running Asterobot has passed the hour. */ export function after({ hour }) { return new Date().getHours() >= hour; } /** @type {BehaviorSteps} */ export const steps = { rest: { label: "Rest", description: "Waits, and ends early when the plan stops.", args: { seconds: { type: "int", label: "Duration", unit: "seconds", default: 30, min: 1 }, }, run: rest, }, }; /** @type {BehaviorConditions} */ export const conditions = { after: { label: "After", args: { hour: { type: "int", label: "Hour", default: 20, min: 0, max: 23 }, }, run: after, }, }; 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, }); } // Declared for the Studio, and still an ordinary function. if (after({ hour: 20 })) botInfo("It's past 8 PM."); } Steps and conditions follow the rules of settings and actions about where they're declared:
Only the exports of index.js count. The Studio will list them in the order you write them. A package can export any of parameters, actions, steps and conditions, or none of them. Asterobot reads them the way it reads settings, by running the top level of your modules without a bot. Declare settings describes what that means for your code. Each package declares its own, dependencies included. The Studio will offer the steps and conditions of the package you edit next to those of the packages it depends on. Asterobot never calls run, neither while it reads the declarations nor while your script plays: only a plan calls it, when a bot plays the plan. Steps and conditions aren't buttons either, so they never appear under Actions on the bot's page. A function you want both in a plan and on a button is declared twice, once in steps and once in actions.
Fields
Steps and conditions have the same fields. Only what run gives back differs.
Field Required What it does label No The block's name in the Studio. Without it, the key is shown, such as rest. description No A line of help about what the block does. args No The values a plan gives on each call. Each one is declared exactly like a setting, with the same types, choices, bounds and defaults, and can ask for game data with ref. run Yes The function. Other fields are ignored.
What run receives
A plan calls run(args, context):
args holds every argument you declared: the value set in the plan, or the argument's default when the plan leaves it out. An argument's value is checked with the same rules as a setting: the type, the choices, min and max. Without a default, an argument starts at its type's zero value, 0 for a number, or at its first choice when it has choices, as a setting does. When that breaks its own rules, such as min: 1, or when the argument asks for one id with ref, a plan can't leave it out: the Studio shows Argument <name> needs a value. and base refuses to play the plan until it's set. context says where the plan is: Field What it holds map The map the bot is on, as a number stop The stop of the plan being run, as the plan describes it lap How many times the plan has gone round, from 0 stopping true once the plan or the bot is asked to stop A plan only stops between steps. A step that can take long, such as rest above, checks context.stopping as it goes and returns early once it's true.
When your own code calls the function, it passes whatever it wants, and there's no context unless you build one. Write context?.stopping, as above, in a function both a plan and your code call.
What run returns
A step's run may be async, and resolves with one of these:
Nothing: the step did what it does on this map. { nothingToDo: true }: there was nothing to do on this map. Add retryAfter, a number of seconds, to say when there may be something again, such as { nothingToDo: true, retryAfter: 120 } for a resource that grows back in two minutes. A condition's run returns true or false, or a promise of one. Anything else, such as 1 or "yes", counts as an error.
A step or a condition that throws, or whose promise rejects, has failed, and the plan decides what happens next. A condition about something the game hasn't told your script yet should throw, with a message saying what's missing, rather than guess an answer.
Ask for game data with ref
ref names the game data table that a setting's or an argument's numbers come from. It goes on an int, or an array of int, of any declaration: settings, the arguments of actions, steps and conditions.
/** @type {BehaviorParameters} */ export const parameters = { monsters: { type: "array", of: "int", label: "Monsters to fight", ref: "monsters" }, subarea: { type: "int", label: "Subarea", ref: "sub_areas" }, }; The value is the name of a table of the game data, as the Game data tab shows it: monsters, items, jobs, maps, sub_areas, areas and so on. The script still reads plain numbers: keys of that table, the ids search() returns and record() looks up. See Lookups. Asterobot only checks that ref is a string. It doesn't check that the table exists, nor that the setting is an int, so a misspelled table name isn't reported. Asteroboard shows a search by name in place of the number box wherever the setting or the argument is edited: the bot's Settings tab, an action's dialog and the Studio. For maps, the number box stays, since maps have no names worth searching by. For maps, sub_areas and areas, Pick on the world map is offered too when a world map is on screen, as in the Studio. A table Asteroboard can't search keeps the plain number box.
When Asterobot checks the declarations
Asterobot checks steps and conditions at the same moments as settings: 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. Problems fall into the same two groups as for settings and actions.
A mistake in the shape of the steps or conditions export stops the script from starting at all, and the Problems alert shows a text starting with read declarations of:
Error Cause package "<package>": steps: must be an object, got <type> steps is exported, but isn't an object package "<package>": steps: "<step>" must be an object A step isn't an object package "<package>": steps: "<step>" declares no run function A step has no run package "<package>": steps: "<step>": run must be a function A step's run isn't a function package "<package>": steps: "<step>": args: must be an object, got <type> args isn't an object package "<package>": steps: "<step>": args: "<argument>" must be an object An argument isn't an object Conditions give the same texts with conditions: and the condition's name.
A mistake in an argument doesn't stop the script, but it makes the whole package's declarations unreadable: Package settings shows Couldn't read this package's settings with the reason, and the script starts with no settings at all, even the ones that are fine. The reason starts with package "<package>" step "<step>": argument "<argument>": for a step's argument, or with package "<package>" condition "<condition>": argument "<argument>": for a condition's, followed by the texts Declarations lists for a setting. With default: 24 on the hour argument above, Package settings shows:
my-package@1.0.0: read declarations: package "my-package" condition "after": argument "hour": parameter "hour" default: 24 is above the maximum 23 A ref that isn't a string is refused the same way, with ref must be a string, got <type>, on a setting as well as on an argument.
Typing with JSDoc
Put /** @type {BehaviorSteps} */ above export const steps, and /** @type {BehaviorConditions} */ above export const conditions, as in the example above. The editor then completes the fields, and marks a condition whose run returns something other than a boolean, and a step whose run returns something other than nothing or { nothingToDo }.
These types need no import:
Type What it describes BehaviorSteps The whole steps object BehaviorConditions The whole conditions object BehaviorStep One step BehaviorCondition One condition BehaviorBlockContext The context that run receives BehaviorStepResult What a step's run may resolve with Next, Ship fight profiles explains how a package ships fight profiles.
  • 45 vues
character(), items(), itemCount() and jobLevel(), which tell a script its character's level, kamas, energy, life, pods, whether it's in a fight, its items and its jobs, with when each one is known and every error.
asterobot:character tells your script what the game has said about its character: level, kamas, energy, life, how full its bags are, whether it's in a fight, its items and its job levels. Use it to decide what to do next, such as going to the bank when the bags are nearly full or resting when life is low.
import { character, items, itemCount, jobLevel } from "asterobot:character"; These are the module's only exports. There's no character object grouping them.
Export Returns What it gives you character() An object, right away Level, kamas, energy, life, pods, and whether a fight is under way items() An array or undefined, right away Every stack the character has, worn or in its bags itemCount() A number or undefined, right away How many of an item the character has jobLevel() A number or undefined, right away The level of one of its jobs None of the functions returns a promise, and none of them changes anything in the game.
Where the values come from
Asterobot follows the character itself, for the whole game session, from the messages the server sends: the character's selection, its characteristics, its inventory and the weight of its bags, its job list, and when it enters or leaves a fight. So a script started or reloaded in the middle of a session reads the character as it is, not only what it saw since it started.
A value the game hasn't sent yet is missing, never guessed. character() leaves the property out, and items(), itemCount() and jobLevel() return undefined. A missing value is never 0 or null, so a script can always tell "zero" from "not known yet".
Every call asks again and returns a new value. Nothing updates by itself: read it again when you need it.
character()
character() returns an object with the properties below. Each one is a number, except inFight, and each one is absent until the game has sent it.
Property Type Value level number The character's level kamas number The kamas it carries, not those in its bank energy number Its energy points life number Its life points. See Life. maxLife number The most life points it can have, erosion taken off weight number What its bags weigh, in pods maxWeight number The most its bags can weigh, in pods inFight boolean true while a fight is under way, false otherwise import { botInfo, botWarn } from "asterobot:bot"; import { character } from "asterobot:character"; const { weight, maxWeight, life, maxLife } = character(); if (weight !== undefined && maxWeight) { botInfo(`Bags: ${Math.round((100 * weight) / maxWeight)}%`); } if (life !== undefined && maxLife && life < maxLife / 2) { botWarn("Below half life"); } For a character carrying 98 pods out of 1075 with full life, the console shows Bags: 9%.
The object is the script's own: changing it changes nothing, neither in Asterobot nor in the next call's answer.
Life
life and maxLife come from the character's characteristics, which the server sends as the character enters the world and later on. Asterobot works them out the way the Dofus client's character sheet does: the health points of the character's level and its vitality make the full amount, erosion takes the maximum down, and the life lost takes life down.
Between two of these messages, life follows the regeneration the way the Dofus client counts it. LifePointsRegenBeginEvent starts it, at one life point every regenRate tenths of a second, and a new rate, as the character sits down or stands up, goes on from there. UpdateLifePointsEvent stops it. life never goes past maxLife. The damage the character takes during a fight isn't followed: life is the last value until the next characteristics.
Pods
weight and maxWeight come from InventoryWeightEvent, which the server sends with both numbers whenever what the character carries changes. They're up to date as long as the session lasts. weight can be above maxWeight: the character is then overloaded.
Fights
inFight becomes true when the server switches the character into a fight, and false again when it switches it back to the world. Both switches come with ContextCreationEvent. It's false from the moment the character is selected, so a character that has entered the world and hasn't fought yet reads false, not "not known".
travel() waits for the end of a fight by watching the same switch.
items()
items() returns every stack of items the character has, worn or in its bags, sorted by uid, or undefined until the inventory is known. Each call returns a new array.
Property Type Value uid number The stack's own id, which the requests about it name, such as ObjectUseRequest and ObjectSetPositionRequest itemId number The item's id, a key of the game data's items table quantity number How many the stack holds position number Where the character wears it: 0 amulet, 1 weapon, 2 and 4 rings, 3 belt, 5 boots, 6 hat, 7 cape, 8 pet, 9 to 14 the Dofus, 15 shield. 63 for the bags The list follows what the server says: stacks gained, used up or thrown away, moved in or out of a slot, and their quantities.
import { items } from "asterobot:character"; import { send } from "asterobot:protocol"; // 468 is Incarnam Bread. const bread = items()?.find((stack) => stack.itemId === 468 && stack.position === 63); if (bread) await send("ObjectUseRequest", { objectUid: bread.uid }); itemCount()
itemCount(itemId) returns how many of an item the character has, worn or in its bags, or undefined until the inventory is known.
Parameter Type Description itemId number The item's id: a key of the game data's items table, as search() returns it import { botInfo } from "asterobot:bot"; import { itemCount } from "asterobot:character"; // 303 is Ash Wood. const ash = itemCount(303); if (ash !== undefined) botInfo(`Ash Wood: ${ash}`); An item the character has none of gives 0. It follows the updates the server sends, so a stack gained, used up or that grows or shrinks is counted at once.
Note
Until the server sends the whole inventory again, which it does when the character enters the world, two changes aren't seen. An item the character gains in a new stack during the session, such as the first piece of a resource it didn't carry, isn't counted. And a stack it loses whole, such as one it puts entirely in the bank, sells or uses up, is still counted at its last quantity. The server adds and removes a stack with messages Asterobot doesn't read.
jobLevel()
jobLevel(jobId) returns the level of one of the character's jobs, 0 for a job it doesn't have, or undefined until the job list is known.
Parameter Type Description jobId number The job's id: a key of the game data's jobs table, such as 2 for Lumberjack or 24 for Miner import { botInfo } from "asterobot:bot"; import { jobLevel } from "asterobot:character"; const lumberjack = jobLevel(2); if (lumberjack === 0) botInfo("This character isn't a lumberjack."); When the values are known
The server sends most of these values as the character enters the world, before it's placed on its first map:
Value Known from level, inFight The character's selection kamas, items(), itemCount() The inventory, which follows the selection weight, maxWeight The weight of the bags, sent with the inventory jobLevel() The job list energy, life, maxLife, and kamas again The characteristics sent as the character enters the world In a recorded world entry, every value was known before the character was placed on its first map.
Before the selection, character() is empty, except for a brief moment: the server sends the character's characteristics right before selecting it, and the selection then forgets them. In that recording, they gave the character 0 life and 0 energy, and the ones sent a moment later gave it full life and 10000 energy. So don't decide anything from character() until level is there.
Values last as long as the game connection. They survive the script being stopped and played again, and they're forgotten when the bot connects again, until the server sends them for the new session.
While Asterobot reads declarations
When Asterobot reads a package's settings, steps and conditions, it runs the top level of its modules without a bot (see Declarations). There's no character then: character() returns an empty object, and items(), itemCount() and jobLevel() return undefined, whatever you pass them. None of them throws, so importing asterobot:character at the top of a file is always safe.
Errors
Error Type Cause character.itemCount itemId must be an integer item id TypeError itemId isn't a whole number from -2147483648 to 2147483647, or isn't a number at all, such as a string or a BigInt character.jobLevel jobId must be an integer job id TypeError The same for jobId String(error) gives TypeError: followed by the text. character() never throws.
The Dofus protocol reference describes the messages these values come from, if you'd rather follow one yourself.
  • 54 vues
How scripts and Asterobot's fight module share a bot's fights: starting a fight from a script, knowing when it starts and ends, driving the module and taking its decisions, what the module sends and holds back, and what a script must send when the module is off.
Asterobot has a fight module that plays a bot's fights by itself, when the person running the bot switches it on: it places the character, says it's ready, plays every turn and answers everything the Dofus client answers during a fight. How it decides is set by fight profiles. The guide describes it for players: The fight module and Fight profiles.
The module plays fights, it never starts one. Starting fights, and deciding what happens between them, is your script's job.
Who does what
Your script The fight module, switched on The fight module, switched off Walking to a group and attacking it Yes Never Never Answering the fight map's load Yes Nothing Placement and ready Yes, for the parts its profile leaves on Nothing Playing the character's turns, and those of the summons it controls Yes Nothing Answering the server during the fight (acknowledgements, ready for the next turn) Yes Nothing, but what Hurry the server adds Everything after the fight Yes Nothing Nothing The person running the bot chooses whether the module plays, on the bot's Fight tab. Your script can override that choice while it runs, and the profile and speed too, with asterobot:fight. Write your package so it works either way, or switch the module as it needs it.
Start a fight
A Dofus client attacks a monster group by walking onto the group's cell, then sending AttackMonsterRequest with the group's actor id. The server then removes the group from the map and switches the character into a fight.
To know which actors of the map are monster groups, read the map's contents when they arrive: a monster group's actorInformation.rolePlayActor holds a monsterGroupActor. The ids and cells of the actors standing on the map now are in currentMap().
import { currentMap, botWarn } from "asterobot:bot"; import { on, send, wait } from "asterobot:protocol"; import { move } from "asterobot:movement"; // The actor ids of the monster groups the server placed on the map, read // each time a map's contents arrive, after every fight included. const groups = new Set(); on("MapComplementaryInformationEvent", (traffic) => { groups.clear(); for (const actor of traffic.payload?.actors ?? []) { if (actor.actorInformation?.rolePlayActor?.monsterGroupActor) groups.add(actor.actorId); } }); // Walks onto a monster group and attacks it. Resolves with true once the // fight has started, false when the group was gone or the attack got no answer. async function attack() { const group = currentMap()?.actors.find((actor) => groups.has(actor.id)); if (!group) return false; await move(group.cellId); await send("AttackMonsterRequest", { monsterGroupId: group.id }); try { await wait( (traffic) => traffic.type === "ContextCreationEvent" && traffic.payload?.context === "FIGHT", { timeout: 5000 }, ); return true; } catch { // The group walked away while the character walked to it: the server // doesn't answer an attack from a cell the group left. botWarn("The attack got no answer"); return false; } } Call attack() once the character stands on a map, for example after @asterobot:base has taken a Full socket bot into the game.
Groups walk around. When one moves while the character walks to it, the server ignores the attack, and the Dofus client walks again and attacks from the group's new cell. Do the same: try again after a failed attack, from a fresh currentMap().
Groups that arrive later come with GameRolePlayShowActorsEvent, in the same actors shape, and ContextRemoveElementEvent removes the ones that leave. A script that waits on a map for groups to come back reads those too.
Know when a fight starts and ends
What How A fight starts ContextCreationEvent with context: "FIGHT". The MapCurrentEvent that follows names the fight's map. The fight ends ContextCreationEvent with context: "ROLE_PLAY", after FightEndEvent, then the world map the character is now on. After a defeat, that's another map. Whether a fight is under way, at any moment character().inFight from asterobot:character, true from the first switch to the second. To wait for a fight to end, give the wait a long timeout: fights can last many minutes.
await wait( (traffic) => traffic.type === "ContextCreationEvent" && traffic.payload?.context === "ROLE_PLAY", { timeout: 30 * 60_000 }, ); Your script keeps running during the fight, and its handlers keep receiving every message the server sends, fight messages included. travel() waits out a fight that starts on the way, and @asterobot:base leaves the fight's map to the fight module.
While the module plays
Don't send fight actions. Moves, spells and the end of the turn sent by a script would reach the server alongside the module's, and the turn would go wrong. The module doesn't hold back what a script sends. To decide an action, answer it from a decision hook: the module sends it, checked by the rules engine. Your interceptors don't see the clicks it holds. On a relayed session, the module is asked first about every message the Dofus client sends. The clicks it holds back, the moves, spells and turn ends of the person playing, and placement or ready when the profile decides them, go neither to the server nor to your interceptors. Every other message reaches them as usual. Its console lines are the bot's. The module writes what it does in the bot's console, such as The fight module casts a spell, between your script's lines. When the module is off
With the module off, Asterobot sends nothing during a fight, but what Hurry the server adds when it's on. On a relayed session, the person's Dofus client plays it. On a Full socket bot, nothing answers the server unless your script does, and the fight doesn't move: the server waits for each fighter's answer before the next turn.
A script that plays fights itself sends what the Dofus client sends:
The fight's map. After MapCurrentEvent names it, FightPingsRequest (empty) then ContextReadyRequest with mapId, the fight map's id. Unlike a world map, no MapInformationRequest. Placement, optionally: FightPlacementPositionRequest with cellId, a cell of the character's team from FightPlacementPossiblePositionsEvent, and entityId, the character's id. Ready: FightReadyRequest with isReady: true. Without it, the fight starts when the placement countdown runs out. Its turns: when FightTurnEvent names the character, or a summon FightControlEvent gave it to play (its masterId the character, its slaveId the summon, with the summon's spells), MapMovementRequest to walk, GameActionFightCastRequest with spellId and cell to cast (spell 0 for the weapon or bare hands, a fighter aimed at through its cell), and FightTurnFinishRequest to end the turn. Acknowledgements: GameActionAcknowledgementRequest with actionId, the action id of each outermost sequence the character or a summon it controls authored, as SequenceEndEvent gives it, and valid: true. The server doesn't wait for them, but the Dofus client sends them. Ready for the next turn: FightTurnReadyRequest with isReady: true for every FightIsTurnReadyEvent. The server waits for this one. The Dofus protocol reference describes each message and its fields. It's a lot of work to do well, which is what the module is for.
Faster fights
Three switches on the bot's Fight tab work in every fight, whether the module plays it, the person, or your script. Scripts can't change them. What they change for a script:
Hurry the server: Asterobot also sends GameActionAcknowledgementRequest for each outermost sequence nobody else acknowledges, a monster's or an ally's, and FightTurnReadyRequest { isReady: true } every 100 ms from the FightTurnEndEvent of the character's turn until the next FightTurnEvent, 20 at most. Neither is one of the answers listed above, so a script that plays fights keeps sending its own. Fewer animations, on a relayed session: a GameActionFightEvent with targetedAbility reaches the Dofus client with silentCast: true, and one with staging or executeScript doesn't reach it at all. Skip walking in fights, on a relayed session: a MapMovementEvent during a fight reaches the Dofus client with its last cell alone. On every session, the module then answers without waiting for a fight walk to play. Your script's interceptors see casts and walks as the server sent them, and never see a staging or executeScript that Fewer animations drops. Its handlers receive all of them as the Dofus client does: silent, shortened, or not at all.
Ship fight profiles
A package can ship fight profiles alongside its script: they show on the Fight Profiles page of everyone who installs it, and any bot can play them. See Ship fight profiles, and the Fight profile format for every field.
Drive the module
asterobot:fight is the module for scripts that work with the fight module:
Switch it, play another profile or change its speed while the script runs, with enable(), disable(), setProfile() and setSpeed(). The bot's Fight tab says what the script overrides. Read the fight under way with state(): every fighter's life, AP, MP, cell, states and monster, the round and whose turn it is. Follow every fight with onFight(): its start, each turn's start and end, the module's actions with their reasons, and its end. Take its decisions with onDecision(): where to place, holding the ready, and each action of a turn, from one of the AI's turns to a whole turn of your own. The rules engine checks every action. The choice of challenges comes later, with the challenges themselves.
That's the end of the protocol chapter. The next one, Game data, covers reading items, monsters, maps and texts from a script.
  • 53 vues
Ship fight profiles in a package as JSON files in its fightprofiles folder, so everyone who installs it can play them, with naming, versions, extends, how Asterobot reads them, testing and every error.
A fight profile tells Asterobot's fight module how a bot plays its fights. Players make their own on the Fight Profiles page (Fight profiles), and a package can ship some too: a farming package can come with the profiles its fights are tuned for, and a package can exist only to share profiles. Once the package is installed, its profiles show on the Fight Profiles page, and any bot can play them.
@asterobot:base ships 24 profiles this way, one for each common way of fighting, none naming a spell: its fightprofiles folder is a good place to see complete profiles, and a profile of yours can extend one of them.
Where the files go
A package ships each profile as one JSON file in a fightprofiles folder at its root, next to index.js. A file holds one profile in the fight profile format, exactly as the Fight Profiles page's Export downloads it, and its name, without .json, is the profile's name within the package:
my-package/ ├── index.js ├── asterobot.json └── fightprofiles/ ├── air-cra.json └── air-cra-careful.json fightprofiles/air-cra.json:
{ "version": 1, "name": "Air Cra", "description": "Hits from afar and steps back after casting.", "role": "damage", "range": "ranged", "elements": ["air"], "positioning": "kite" } fightprofiles/air-cra-careful.json:
{ "version": 1, "name": "Air Cra, careful", "extends": "@some-author:fighters/air-cra", "risk": 20 } For a package named @some-author:fighters, these are @some-author:fighters/air-cra and @some-author:fighters/air-cra-careful: the package's name, a /, then the file's name. That full name is the one a bot's Profile list shows, the one extends names, and the one players see on the Fight Profiles page with the Package source.
Only .json files directly in fightprofiles/ are profiles. A subfolder of it, or a file with another extension, is left out, as any other file of the package is (Package layout). Name files like keys: lower case and dashes, such as air-cra-careful.json. The name can't hold a /. Give each profile a name: the format requires one. The page and the bots go by the full name made from the file's name, whatever name says. Profiles are data, never run. A script can't import one: Asterobot refuses it as not a module. The easiest way to write one is to build it on the Fight Profiles page, where the editor checks every value as you go, then Export it and put the downloaded file in your package's fightprofiles folder. In the package editor, Add a file takes a path such as fightprofiles/air-cra.json, and opens it as JSON.
Which version plays
A bot can play a package's profile whether or not it plays the package itself, and every bot plays it from the newest version of the package installed. The page lists each profile from that version too. So when you publish an update, its profiles replace the old ones for everyone who updates, at their next fight, and the profiles that extend them, yours or the players', follow.
A player who wants to keep a profile as it is, whatever your updates do, copies it into their own profiles with Copy. When a player removes your package, the bots set to one of its profiles play the default profile, and their console says so.
So treat a profile's key like a public name: renaming one in an update leaves the bots set to the old name on the default profile.
Extending
A package's profile can extend another profile, with the same rules as the players' own (Extend another profile): one of the same package by its full name, another package's, or Idle. Only one level: a profile can't extend a profile that extends another. Players can extend yours too, and keep their changes on top of your updates.
A profile can't extend one of the player's own profiles in a reliable way, since you don't know their names.
How Asterobot reads them
Asterobot reads the files themselves, without running the package's script. So a package whose script has an error still lists its profiles, and the Fight Profiles page lists the profiles of every installed package, whether a bot plays it or not. Each file is read again when the page lists the profiles and when a fight starts, so a package updated in the meantime plays from the next fight.
The files travel with the package's modules: installing from a folder or a .zip, exporting, publishing on asterobot.net and copying a package all carry them, and the package's integrity checksum covers them (The manifest).
Errors
A profile never stops anything else: not the package's script, nor its other profiles. A file that isn't valid JSON, or holds a value Asterobot can't use, is marked Can't be played on the Fight Profiles page, with the reason, and a bot set to it plays the default profile. Warnings, such as an id the game data doesn't have, show the same way. The fight profile format lists every error and warning.
Test your profiles
Install your package in your library, from its folder or a .zip (The editor, Zip and command line). Open Fight Profiles. Your profiles are listed with the Package source: check that each one is Ready, and read its warnings. Open one with View to see every setting as the editor reads it. Set a bot's Profile to it, switch on Play this bot's fights, and read the Turn log after a fight. To try a change, Copy the profile, edit the copy, then Export it back into your package's fightprofiles folder.
Next, the Packages chapter describes the manifest, starting with every field of asterobot.json.
  • 37 vues
The exact JSON format of a fight profile, every field with its type, values and default, conditions, actions and targets, how extends merges, and every error and warning Asterobot reports.
A fight profile is a JSON object. Players write them on the Fight Profiles page, where the editor shows the same JSON (Fight profiles), and packages ship them as files, one per profile, in their fightprofiles folder (Ship fight profiles). This page is the complete format. What each setting does in a fight is explained, for players, in Profile settings and Rules and conditions.
Every field is optional but name. A field left out plays its default, and a field Asterobot doesn't know is kept and ignored, so a profile written for a newer Asterobot still loads.
{ "version": 1, "name": "Air Cra", "role": "damage", "range": "ranged", "elements": ["air"], "risk": 40, "positioning": "kite", "spells": [{ "id": 32465, "use": "prefer" }], "rules": [ { "when": { "self.life": { "below": "30%" } }, "do": { "retreat": { "to": "outOfReach" } } } ] } Fields
Field Type Values Default version number 1 1 name string Required, not blank description string "" extends string A profile's name: "Idle", a pool profile's, or a package's "<package>/<name>" "": none role string "damage", "tank", "support", "healer" The role the game ranks first for the character's class range string "auto", "melee", "mid", "ranged" "auto" elements array of strings "earth", "fire", "water", "air", "neutral" []: whatever hits hardest risk number 0 to 100 50 placement string "auto", "close", "far" "auto" positioning string "auto", "charge", "hold", "kite", "hide" "auto" focus string "auto", "lowestLife", "closest", "mostDangerous" "auto" allies string "ignore", "help", "protect" "help" waitForAllies number 0 or more 0 parts object See parts Every part on weapon object See weapon Normal use, no limit spells array See spells [] opening object See opening No opening monsters array See monsters [] rules array See rules [] weights object See weights The role's weights challenges any Kept and ignored: challenges aren't played yet An empty string in a field that takes a word, such as "role": "", is the same as leaving it out. A word the field doesn't take is an error.
parts
Field Type Default When false placement boolean true The module doesn't place the character. ready boolean true The module doesn't say the character is ready. movement boolean true The character never walks in its turns. challenges boolean true No effect yet. Writing parts with some fields leaves the others true.
weapon
Field Type Values Default use string "prefer", "normal", "avoid", "never" "normal" maxPerTurn number 0 or more, 0 for no limit but the weapon's own 0 spells
Each entry says what the profile does with one spell.
Field Type Values Default id number A spell's id. Required, not 0: the weapon is set with weapon. use string "prefer", "normal", "avoid", "never" "normal" maxPerTurn number 0 or more, 0 for no limit but the spell's own 0 target string "any", "enemy", "ally", "self" "any" minTargets number 0 or more: the enemies a cast must hit 0 when object A condition, where target.life and target.distance test the fighter the spell is cast on Always opening
Field Type Values firstTurn array Steps cast first in the character's first turn, before everyTurn's everyTurn array Steps cast first in each of its turns A step is a spell's id, "weapon", or { "spell": <id or "weapon">, "target": <target> } with a target. Steps are cast in order, each when it can be; a step that can't be cast is passed over. A spell listed twice is cast twice.
monsters
Field Type Values Default id number A monster's id race number A monster race's id focus string "first", "last", "ignore" As any other keepAway number 0 or more: the fewest cells the character ends its turns from it 0 noMelee boolean true: ends its turns at least 2 cells from it false Each entry needs id or race. It applies to every enemy it matches.
weights
An object of numbers, 0 or more, by scoring term: damage, kill, heal, control, buff, summon, safety, position, waste, allyHarm and mark. A term left out keeps the role's weight. How a turn is scored gives each role's weights.
Rules
rules is an array of rules, applied in order. Each rule is an object:
Field Type Value when object A condition. Left out, the rule always applies. do object Exactly one action. Actions
do holds one of these keys:
Key Value Kind cast { "spell": <id or "weapon">, "target": <target> }, spell required, target optional Forcing attack { "target": <target> }, target optional Forcing heal { "target": <target> }, target optional Forcing buff { "target": <target> }, target optional Forcing endTurn { "after": <seconds> }, after 0 or more, default 0 Forcing retreat { "to": "cover" } or { "to": "outOfReach" } Movement approach { "target": <target> }, target optional: the nearest enemy Movement keepDistance { "min": <cells>, "max": <cells> }, 0 for no bound Movement focus { "target": <target> } Modifier avoid { "spell": <id or "weapon">, "target": <target> }, either or both Modifier Every matching modifier applies. Of the movement rules, the first matching one applies. Of the forcing actions, the opening's steps come first and then the rules in order, and the first one that can be played now is the character's next action. How rules apply describes it in full.
Targets
A target is one of these strings, or an object:
Target Names "self" The character "lowestLifeEnemy" The enemy with the least life left "lowestLifeAlly" The ally with the least life left, the character not counted "closestEnemy" The enemy nearest the character "mostDangerousEnemy" The enemy that could deal the character the most where it stands { "monster": <id> } An enemy of that monster { "race": <id> } An enemy of that monster race { "fighter": <id> } The fighter with that id in the fight Ties go to the lowest fighter id.
Conditions
A condition is an object whose keys are subjects, each with its test, and the groups all, any and not. It holds when every test and every group in it holds.
{ "self.life": { "below": "50%" }, "any": [{ "enemies.within": { "cells": 1, "atLeast": 1 } }, { "fight.round": { "atLeast": 5 } }], "not": { "self.state": { "is": 42 } } } Group Value Holds when all An array of conditions Every one holds any An array of conditions At least one holds not A condition It doesn't hold A test is an object with some of these fields:
Field Type Holds when below number, or a percent as a string such as "30%" The value is strictly less above number, or a percent as a string The value is strictly more is number The count is exactly this, or that state, monster or race is there atLeast number The count is this or more atMost number The count is this or less cells number For within: the distance counted Subject Test fields Reads self.life below, above The character's life; a percent is of its maximum life self.shield below, above Its shield points; a percent is of its maximum life self.ap below, above Its AP self.mp below, above Its MP self.state is It is in that state enemy.state is A living enemy is in that state ally.state is A living ally, the character not counted, is in that state ally.life below, above A living ally's life is in that range; a percent is of that ally's maximum life enemies.count is, atLeast, atMost Living enemies, summons included allies.count is, atLeast, atMost Living allies, summons included, the character not counted enemies.within cells, then is, atLeast, atMost Living enemies within cells cells of the character allies.within cells, then is, atLeast, atMost Living allies within cells cells enemy.monster is That monster is among the living enemies enemy.race is A monster of that race is among the living enemies fight.round is, atLeast, atMost The fight's round fight.turn is, atLeast, atMost The character's turns in the fight, the current one included target.life below, above In a spell's when only: the life of the fighter it's cast on target.distance below, above In a spell's when only: that fighter's distance in cells Each subject takes only its own test fields, at least one of them, and within needs cells of 1 or more: anything else is refused, naming the subject. self.ap, self.mp and target.distance take numbers, not percents. A subject the module doesn't know never holds, with a warning, and so do target.life and target.distance in a rule's when, and challenge, reserved for challenges, until the module plays them.
How extends merges
A profile with extends starts from the profile it names and writes its own fields over it:
Field Merged A string, number or boolean, and the lists elements, opening.firstTurn and opening.everyTurn The profile's own replaces the extended one's An object (parts, weapon, opening) Field by field weights Term by term spells The extended profile's entries, then the profile's own; an entry with the same id replaces the extended one monsters The same, by id and race rules The extended profile's rules, then the profile's own The extended profile can't extend another one. A profile that extends a package's profile plays it from the newest version of that package installed.
Errors
Asterobot refuses a profile it can't play: the editor won't save it, an import is refused, and a pool file or a package's profile shows Can't be played on the Fight Profiles page, a bot set to it playing the default profile. The reason starts with fight: malformed profile:, followed by the field, in the JSON's own path, and what's wrong. Only the first problem is reported.
Reason Cause <field>: a <type>, not a <json type> A field holds the wrong kind of value, such as a string where a number goes. The types are Go's: int, int32, float64, string, bool. version: required, 1 version is less than 1 name: required name is missing or blank <field>: "<value>" is not one of <values> A word a field doesn't take: role, range, placement, positioning, focus, allies, weapon.use, spells[i].use, spells[i].target, monsters[i].focus, rules[i].do.retreat.to, or a target's name risk: <n> is not from 0 to 100 risk out of range elements[i]: "<value>" is not earth, fire, water, air or neutral An unknown element <field>: <n> is below 0 A negative waitForAllies, weapon.maxPerTurn, spells[i].maxPerTurn, spells[i].minTargets, monsters[i].keepAway, weights.<term> or rules[i].do.endTurn.after spells[i].id: required: the weapon is the profile's weapon A spell entry without a spell, or with spell 0 monsters[i]: an id or a race is required A monster entry with neither <target>: names nobody A target object without monster, race or fighter rules[i].do: one action, not <n> A rule's do with no action, or more than one rules[i].do.keepDistance: min <a> is above max <b> min above a non-zero max fight: a spell is an id or "weapon": "<text>" A spell named by any other text when "<key>": <reason> A condition's test that isn't an object of the right shape <path>.<subject>: takes below or above, takes is or takes is, atLeast or atMost A test without the fields its subject takes, or with others <path>.<subject>: a number, not a percent A percent on self.ap, self.mp or target.distance <path>.<subject>.cells: required, 1 or more enemies.within or allies.within without cells <path>.<subject>.cells: only allies.within and enemies.within take it cells on another subject rules[i].do.cast.spell: required: a spell's id, or "weapon" A cast without a spell extends: "<name>" extends another profile, and one level is all The extended profile extends another extends: "<name>": <reason> The extended profile can't be read spells[i] is the entry's place in the list, counted from 0, as in spells[0].use. A target's field is written in full, such as rules[2].do.attack.target or opening.firstTurn[0].target.
Pool profiles have their own name errors when they're saved: fightprofiles: a profile's name can't be empty, start with @, or hold / or a character a file name can't, and fightprofiles: a profile already has this name.
Warnings
Warnings don't stop a profile from playing. The Fight Profiles page counts them and the editor lists them.
Warning Cause version: <n> is newer than this module's 1: what it adds is ignored A profile written for a newer Asterobot <path>.<subject>: no such subject: never holds A condition's subject this Asterobot doesn't know <path>.target.<field>: only a spell's condition has a target: never holds target.life or target.distance in a rule's when <path>.challenge: challenges aren't played yet: never holds A challenge test, before the module plays challenges weights.<name>: no such scoring term: ignored A weight with an unknown name extends: no profile has this name: its own fields play over the default profile The extended profile is missing spell <id>: not in the game data, ignored An id the newest installed game version doesn't have, also given as monster, monster race and state name: "<name>", but its file is "<file>.json": it plays as "<file>" A pool file whose name differs from its file name
  • 42 vues
Drive the fight module from a script: switch it on or off, change its profile and speed, read the fight under way, follow every fight's events, and take its decisions yourself at the placement, ready and each action of a turn, with the rules engine checking every action.
asterobot:fight lets your script work with the bot's fight module, which plays its fights as its fight profile says. The guide describes the module for players: The fight module.
import { enable, disable, enabled, setProfile, profile, setSpeed, speed, setFights, fights, state, onFight, offFight, onDecision, offDecision, } from "asterobot:fight"; These are the module's only exports. There's no fight object grouping them.
Export Returns What it does enable(), disable() Nothing Switch the module on or off while the script runs enabled() A boolean Whether the module is on, all settings considered setProfile() Nothing Play another profile while the script runs profile() An object The profile in effect setSpeed(), speed() Nothing, a string or a timing object Set or read the speed setFights(), fights() Nothing, an array Set or read the kinds of fight it plays state() An object or undefined The fight under way onFight(), offFight() A handle, nothing Follow every fight's events onDecision(), offDecision() A handle, nothing Take the module's decisions Every function is synchronous: none returns a promise.
Overrides
enable(), disable(), setProfile(), setSpeed() and setFights() override the settings the person saved on the bot's Fight tab. The tab then says what the script overrides, and the person's settings play again once the script stops.
They last while the script runs. A reload starts a new script: its first override replaces the old script's, and until then the old script's stay. A reload that fails to start leaves the running script's. The switch takes effect at the next turn. Switched during a fight, the module starts or stops playing at the next turn's start, so one turn is never played by two. Before the first turn, and for the profile and the speed, the change applies at once. On a relayed session the switch stays the person's. enable() and disable() change nothing there, and enabled() says what's in effect. The profile, the speed and the kinds of fight still apply. A profile the script sets plays every fight, against players too, whatever Profile in PvP fights the tab sets. enable() and disable()
enable() switches the module on and disable() off, as the switch on the Fight tab does. enabled() returns true when the module is on, all settings considered: the saved switch, or the script's.
import { disable } from "asterobot:fight"; // This package plays its fights itself. disable(); setProfile()
setProfile(nameOrProfile) plays another profile while the script runs.
Argument Plays A profile's name "Idle", a profile of the person's pool, or a package's, such as "@some-author:fighters/air-cra" "" The default profile An object A profile your script writes, in the fight profile format An object is read as JSON.stringify writes it, then checked as a saved profile is. Its extends is merged in, one level as always. A malformed profile throws a TypeError naming the field, and its warnings go to the bot's console. A name no profile has, or a saved profile that can't be played, throws an Error.
import { setProfile } from "asterobot:fight"; setProfile({ version: 1, name: "Careful", extends: "@some-author:fighters/air-cra", risk: 20, rules: [{ when: { "self.life": { below: "30%" } }, do: { retreat: { to: "cover" } } }], }); profile()
profile() returns the profile in effect as a plain object in the format, the profile it extends already merged in, so without extends. The weapon is written "weapon". The object is a copy: change it, then set it back.
import { profile, setProfile } from "asterobot:fight"; // Keep everything, but walk no more. const current = profile(); setProfile({ ...current, parts: { ...current.parts, movement: false } }); A field this Asterobot doesn't read isn't in the copy.
setSpeed() and speed()
setSpeed(speed) sets the speed while the script runs: "slow", "normal" or "turbo", or a custom timing object, as the Custom speed of the Fight tab sets it:
setSpeed({ actions: { min: 0 }, // no wait between actions cast: { min: 300, max: 800 }, // 300 to 800 ms before each spell, at random endTurn: { min: 1000 }, // 1 s before ending the turn answerAtOnce: true, // answer the server at once, as "turbo" does }); Field What it is actions The wait before every decision without one of its own: each move, spell and end of turn, the placement and ready. Required. placement, ready, move, cast, endTurn The wait before a decision of that kind. Absent waits actions. answerAtOnce true answers the server at once, as "turbo" does: the fight map's load, each action's acknowledgement and the turn-ready answers. Absent or false answers once the game client would have played what it answers. A wait is { min, max } in milliseconds, each from 0 to 60000: min, or a random wait from min to max when max is above it. A wait never costs the turn: a decision goes out 2 seconds before the turn ends at the latest. The waits only change when the module acts, never what it decides.
Any other value, a field a timing doesn't have, or a wait out of bounds throws a TypeError. speed() returns the speed in effect: the preset's name, or the timing object for a custom speed. Speed says what each one does, and what "turbo" and answering at once risk.
setFights() and fights()
setFights(kinds) sets the kinds of fight the module plays while the script runs, as the Fights it plays checkboxes of the Fight tab do. An empty array plays none. In a fight of another kind, or of a type whose meaning isn't known yet, the module is off, as with disable(), from the moment the game says what the fight is, just after its map's load. An unknown kind throws a TypeError. fights() returns the kinds in effect: ["monsters"] unless the person or a script chose others.
Kind Fights "monsters" Fights against monsters, quest fights where an NPC fights beside the character included, and breaches "aggressions" A player attacking another "duels" Duels between players "kolizeum" Kolossium fights "taxCollectorsAndPrisms" Fights for a perceptor (tax collector), a prism or an alliance's territory import { setFights } from "asterobot:fight"; // Defend against aggressions while farming. setFights(["monsters", "aggressions"]); Starting a fight against players is your script's job, as for monsters: accepting a duel, registering for the Kolossium. Against players, the module reckons each enemy hits bare-handed, since it doesn't know their spells yet: it plays, but takes more risks than against monsters.
state()
state() returns the fight under way, as the module follows it from the server's messages, or undefined outside one. Every call returns a new snapshot.
Property Type Value characterId BigInt The bot's character round number The fight's round, 0 before it starts turn BigInt The fighter whose turn it is; absent during the placement fighters array Every fighter, the dead ones included Each fighter:
Property Type Value id BigInt Its id in the fight: negative for monsters and summons kind string "character", "sidekick" or "creature" ally boolean Whether it's on the character's side, the character included team number Its team: fighters of one team share it alive boolean Whether it's alive cell number Its cell invisible boolean Whether the server hides it from the other team: cell is then where it was last seen level number Its level life, maxLife, shield number Its life, its most life, its shield points ap, mp number The AP and MP it has left states array of numbers The states it's in, by spell_states id breed number A character's class, by breeds id monsterId, grade number A creature's monster, by monsters id, and its grade summonerId BigInt The fighter that summoned it, for a summon import { state } from "asterobot:fight"; const fight = state(); const mastogob = fight?.fighters.find((f) => f.monsterId === 2854 && f.alive); onFight()
onFight(handler) calls handler with each event of every fight the bot is in, whether the module plays it or not, until offFight(handle). It returns the handle.
type When Also has "started" The bot entered a fight, or got one back after a reconnection "turnStarted" A fighter's turn started fighter (BigInt), round "actionPlayed" The module played an action: only the module's own fighter, round, action, spell, cell, reasons, scripted "turnEnded" A fighter's turn ended fighter, round "ended" The fight ended, or the session did first round, summary actionPlayed describes what the module sent, as the Turn log keeps it:
Property Value fighter The character, or for a turn's action the summon it controls whose turn it is action "placement", "ready", "move", "cast" or "endTurn" spell The spell cast: its id, or "weapon" cell The cell cast on, walked to, or placed on reasons score, the score of the turn this action starts, terms, what the score counted by term, and options, the best options weighed, { spell, cell, score }. Absent for the placement, ready, and an end of turn a rule asked for. scripted true when a decision hook decided it ended's summary has rounds, turns (the character's), actions (the module's), alive (whether the character was alive at the end) and duration, the fight's length in milliseconds as the server gave it, absent when the session ended first.
import { onFight } from "asterobot:fight"; import { botInfo } from "asterobot:bot"; onFight((event) => { if (event.type === "ended") botInfo(`Fight over in ${event.summary.rounds} rounds, alive: ${event.summary.alive}`); }); Handlers run in the order they were registered. What a handler returns is ignored, and an async handler is fine. A handler that throws stops the script, as an on() handler does.
An event reaches the script at the moment the module follows it: before your protocol handlers hear the message it comes from, on every PC. actionPlayed comes when the module sends the action.
onDecision()
onDecision(hook) asks hook at each of the module's decisions, until offDecision(handle). It returns the handle. The module decides first, as its profile says, then asks your hooks, and plays your answer instead when the rules engine accepts it.
The hook receives { stage, fight, options }:
stage When options, best first The hook can answer "placement" Before the fight, when the profile places the character The placement cells, { cell, score } { cell }: one of the options "ready" Before the fight, when the profile says the character is ready None { wait: true } "action" Before each action of a turn the module plays, the character's and those of the summons it controls The best turns the AI found for the rest of the turn, { actions, score, terms } { option: i }, { actions: [...] } or { endTurn: true } fight is what state() returns. The challenges' stage comes with the challenges, which the module doesn't choose yet.
Answers
{ cell } places the character on that cell. It must be one of the options: the character's own cell, or one of its team's placement cells nobody stands on. { wait: true } holds the character's ready. The hooks are asked again each time the fight changes, such as an ally joining, and the placement countdown still starts the fight when it runs out. { option: i } plays the first action of options[i]. Then the module decides again: the AI searches again, and the hooks are asked again. { actions: [...] } plays the actions one after the other, without asking again between them. Return every action and { endTurn: true } to play the whole turn. { endTurn: true } ends the turn. Each action is one of:
Action Does { move: cell } Walks to the cell { cast: spell, cell } Casts the spell on the cell: its id, or "weapon" for the weapon or bare hands { cast: spell, target } Casts the spell on a fighter's cell: target is an id of fight.fighters { endTurn: true } Ends the turn Each turn of options lists its actions the same way, ending with { endTurn: true }, so { actions: options[i].actions } plays a whole turn the AI found.
undefined, null and a promise are no answer: the next hook is asked, then the AI decides. A hook is never awaited.
import { onDecision } from "asterobot:fight"; onDecision(({ stage, fight, options }) => { if (stage !== "action") return; // Open on the Royal Mastogob with Explosive Arrow when it's there. const mastogob = fight.fighters.find((f) => f.monsterId === 2854 && f.alive); if (mastogob && fight.round === 1) return { actions: [{ cast: 32445, target: mastogob.id }] }; // Otherwise prefer the best turn that doesn't move. const still = options.findIndex((o) => !o.actions.some((a) => "move" in a)); return still >= 0 ? { option: still } : undefined; }); What the module checks
The rules engine checks each action before it's sent, as it checks the AI's: the AP, the range, the line of sight, the cooldowns, the cells a walk can reach with its tackle. The module also never sends an action the server refused earlier in the turn. When it can't play an answer, or a hook throws, runs past the time limit or answers something of another shape, the AI's decision is played and the bot's console says why.
A list of actions stops early, and the module decides again with the AI and the hooks:
at an action the rules engine or the server refuses; when the fight isn't what the engine predicted after the action before: a fighter that died or lived otherwise, one on another cell, or the fighter acting with other AP or MP left. Life isn't compared, since the roll is the server's. The console says why, with The fight module stops the script's actions:.
Timing
The module waits for your answer as long as it takes. The question goes through the script's queue like every message, so on a slow PC it's answered later, never skipped: such a PC may lose a turn, but it never decides otherwise. When no hook is registered, nothing is asked and nothing waits. What a hook decides goes out after your frames. An answer takes its turn among what your script sends, as a movement does: a hook that calls send(X) and then returns a cast puts X on the wire first. What the AI decides alone is sent directly. The speed still applies. Each action goes out after the wait the speed sets before it, as the AI's do. The Turn log marks the decisions your hooks made By the script, and actionPlayed has scripted: true.
While Asterobot reads declarations
When Asterobot reads a package's settings, steps and conditions, it runs the top level of its modules without a bot (see Declarations). There's no fight then: state() returns undefined, onFight() and onDecision() return a handle that never fires, and offFight() and offDecision() do nothing. The other functions throw, since they read or change a bot's module.
Errors
Error Type Cause fight.setProfile: fight: malformed profile: <field>: <reason> TypeError The profile object can't be played. The format's errors list each reason. fight.setProfile: fightprofiles: no profile has this name: "<name>" Error No profile has that name. For a package's profile, the reason follows, such as no version of <package> is installed. fight.setProfile: fight: malformed profile: <field>: <reason>, for a name Error The saved profile of that name can't be played fight.setProfile takes a profile's name or a profile object TypeError The argument is neither a string nor an object fight.setSpeed: fight: the speed must be slow, normal, turbo or custom, not "<speed>" TypeError Another speed fight.setSpeed: fight: a custom speed needs its timing, each wait from 0 to 60000 ms TypeError "custom" passed as a string: pass the timing object fight.setSpeed: fight: a custom speed needs its timing, each wait from 0 to 60000 ms, not <ms> TypeError A wait out of bounds fight.setSpeed: json: unknown field "<field>" TypeError A field a timing doesn't have fight.setSpeed takes "slow", "normal", "turbo" or a timing object TypeError The argument is neither a string nor an object fight.onFight handler must be callable, fight.onDecision handler must be callable TypeError The handler isn't a function maximum behavior handlers reached TypeError 256 handlers are registered already, protocol handlers included These lines appear on the bot's Console and don't stop the script:
Line Cause fight decision hook failed: <error>: the AI decides A hook threw, or ran for more than 5 s fight decision hook answered <stage>: not an answer this stage takes: <shapes>: the AI decides A hook answered an object of another shape fight decision hook returned a Promise, which counts as no answer: a hook answers synchronously, so it can't be async An async hook, or one returning a promise. Said once per script. The script's decision can't be played: the fight module decides instead The rules engine refused the answer, with the reason. Said once a turn. The fight module stops the script's actions: <reason> A list of actions stopped early Nothing plays this fight unless the bot's script does: the server disconnects a character left idle for a few minutes On a Full socket bot, the module is off for the fight, or doesn't play its kind A hook that fails again and again is reported at the 1st, 10th, 100th failure in a row, ending with (10 times) and so on.
Fights explains how scripts and the module share a fight, and what a script sends when it plays fights itself.
  • 63 vues
Say things in the game chat from a script through the bot's chat guard, which waits out a channel's cooldown and keeps to the server's limits, with templates that pick random wording, fill in the character's level, map or kamas, and link items from its inventory; switch the bot's chat module and pick its chat profiles while the script runs; answer the private messages the bot gets, and hear what the module does.
asterobot:chat lets your script say things in the game chat through the bot's chat module, and set the module while it runs. Every message the bot sends goes through the module's guard, which keeps to the limits the game server sets and says: your script's messages, the ones typed in the bot's Console, the module's ads, replies and triggers' messages, and the alarms' lines.
import { say, whisper, pause, resume, setVariable, variables, setFlag, flags, setDryRun, dryRun, enable, disable, enabled, setParts, parts, setProfiles, profiles, onChat, offChat, onAnswer, offAnswer, } from "asterobot:chat"; These are the module's only exports. There's no chat object grouping them.
Export Returns What it does say() A promise Say a message in a channel whisper() A promise Whisper a message to a character pause(), resume() Nothing Stop sending for a while, or start again setVariable(), variables() Nothing, an object Set or read your own {variables} setFlag(), flags() Nothing, an array Set the flags the profiles' conditions test setDryRun(), dryRun() Nothing, a boolean Write messages to the console instead of sending them enable(), disable(), enabled() Nothing, a boolean Switch the chat module on or off setParts(), parts() Nothing, an object Switch parts of the module, such as its ads setProfiles(), profiles() Nothing, an array Pick the chat profiles the bot talks from onChat(), offChat() A subscription, nothing Hear what the module does onAnswer(), offAnswer() A subscription, nothing Answer the private messages the bot gets say() and whisper() return a promise. The other functions are synchronous.
The guard
The guard is the bot's one queue for chat messages. It sends one message at a time, and waits for the server's answer before the next: the server saying the message back, a refusal, or 5 seconds without an answer, which count as sent.
Rule What happens A channel's cooldown When the server answers "Use of this channel is restricted. You will be able to post again in 37 seconds.", the guard waits 37 seconds and 1 more, then sends the message once more. Messages to other channels go on meanwhile. Refused a second time, the message is dropped. At most 6 messages a minute Counted for the bot, whispers included, 6 unless the bot's Chat tab sets another number, from 1 to 30. A message past the limit waits its turn. "Too much spam!" Nothing is sent for 5 minutes, then 10, 20, 40, and 60 at most for each warning after it. An hour without a warning starts over at 5. A mute When a member of staff mutes the character, nothing is sent until the mute ends, plus a minute. The game's "is letting you speak again" ends it at once. A channel that refuses the character No guild, no group, a level or a subscription short, a beginners' channel, prison: your messages to that channel are refused for the rest of the game session. "This channel cannot be used on this map" lasts until the next map. Items that can't be shown When the channel refuses item links, the message goes again with each item written as [its name], and so do the next messages to that channel this session. The person's messages, typed in the Console, go first. A pause, a back-off or the limit doesn't hold them back: only a channel's cooldown does. The alarms' lines come next, and a pause doesn't hold them back either. Then come the answers to commands, the replies and the triggers' messages, and last your script's messages and the ads.
say() and whisper()
say(channel, text) says a message in a channel. whisper(name, text) whispers it to the character of that name.
Argument What it is channel A channel as the protocol names it: "SALES" is Trade, "SEEK" Recruitment, "GLOBAL" General, "GUILD", "PARTY" the group, "ALLIANCE"... name The name of the character to whisper text A template The promise resolves once the server took the message, with { dryRun: false }, or { dryRun: true } in a dry run. It rejects with an Error whose reason says why the message wasn't sent:
reason Why "notInGame" The character isn't in the game "paused" Sending is paused "spam" The guard is backing off after "Too much spam!" "muted" A member of staff muted the character "cooldown" The channel was still in its cooldown when the message went once more "refused" The server refused the message or its channel, or the whisper's recipient isn't connected "tooLong" The message is past the game's 256 characters or 16 item links "template" The template can't be read "missingVariable" A variable has no value "missingItem" An item to link isn't in the inventory "stopped" The script stopped before the message went "sendFailed" The message didn't reach the server import { say, setVariable } from "asterobot:chat"; import { botWarn } from "asterobot:bot"; setVariable("price", 12); try { await say("SALES", "{Selling|WTS} {item:289} at {price} k each, /w me"); } catch (error) { if (error.reason === "missingItem") botWarn("No more wheat to sell"); else throw error; } When the script stops, its messages that are still waiting are dropped. A message already sent waits for its answer.
Templates
A template is the text of a message with parts that are filled in each time it is said.
Write Says {Hi|Hello|Hey} One of the choices, at random each time. Choices can be nested, {Hi|Hello {there|you}}, and empty, {!|}. {item:289} A link to that item, from the character's inventory, as the game makes one. An item in the bags is linked before a worn one. {name} The character's name {level} Its level {server} Its server's name {subarea}, {area} Where it is, in the bot's language {position} A link to its map, as the game makes [4,-19] typed in its chat box: {{map,4,-19,1}} in the text, which the game shows as [4,-19] {kamas} Its kamas {pods} Its pods, used and maximum, such as 1200/3400 {time} The computer's time, HH:MM {anything else} A variable you set with setVariable() Nothing broken goes out: a message isn't sent when one of its variables has no value, an item it links isn't in the inventory, or, filled in, it's longer than the game's 256 characters or links more than 16 items. The length counts each item link as the game's code for it, which is longer than the item's name, so that the server never finds the message too long.
A template can't be read when a { has no } or the reverse, when {} is empty, when a part like {a {name}} nests without a |, or when it has a colon that isn't an item link, such as {a:b}.
pause() and resume()
pause(seconds) stops the module's and your scripts' messages for that many seconds. Without seconds, it lasts until resume(). The messages waiting are dropped with "paused", and so are the ones said while it lasts. The person's messages from the Console still go.
A pause outlives the script that set it, like a button the person pressed.
import { pause } from "asterobot:chat"; // Nothing more for ten minutes. pause(600); setVariable() and variables()
setVariable(name, value) sets a variable your messages read as {name}. A number or a boolean is written as text; null or undefined deletes the variable. variables() returns your variables, as a copy.
The character's own variables, such as {name} or {level}, win over yours of the same name, and yours win over the bot's chat profiles'. Your variables last until the script stops, or until a newer script, after a reload, sets its own. The module's ads read them too.
setFlag() and flags()
setFlag(name) sets a flag, which the flag test of the bot's chat profiles reads (Conditions); setFlag(name, false) clears it. flags() returns the names of the flags set. Your flags last until the script stops, or until a newer script sets its own.
import { setFlag } from "asterobot:chat"; // The profile's "shop open" ad runs from now on. setFlag("shopOpen"); setDryRun() and dryRun()
setDryRun(true) writes the module's and your scripts' messages to the bot's console instead of sending them: Dry run: would say in Trade: Selling [Wheat] at 12 k each. Everything else goes on as if they were sent, the limit of 6 a minute included. The person's messages from the Console are always sent. dryRun() says whether a dry run is on.
The dry run lasts until the script stops, or until a newer script sets its own.
enable(), disable() and enabled()
enable() switches the bot's chat module on while the script runs, and disable() off: with the module off, the bot posts no ad, answers no private message, fires no trigger, obeys no command and leaves its status alone. enabled() says whether the module runs now. The switch the person saved on the Chat tab comes back when the script stops.
On a relayed session, only the person's switch counts: enable() and disable() change nothing there, and enabled() says what the person set.
setParts() and parts()
setParts(parts) switches the parts it names while the script runs, the others staying as they are: setParts({ ads: false }) keeps the module on but stops its ads. parts() returns every part, on or off: { ads, replies, triggers, commands, status }.
setProfiles() and profiles()
setProfiles(list) sets the bot's chat profiles while the script runs, in order, in place of the person's. Each item is the name of a profile, as the bot's Chat tab lists it, or a profile object in the chat profile format, which crosses into Asterobot as JSON.stringify() writes it and is checked as a saved profile is. profiles() returns the profiles in effect, as plain objects in that format, copies: changing one changes nothing until you pass it to setProfiles().
import { setProfiles, setVariable } from "asterobot:chat"; setVariable("price", 11); setProfiles([ "Sell wheat in Astrub", { version: 1, name: "Script ad", ads: [{ channels: ["SALES"], messages: ["{item:289} at {price} k"], every: { min: 300, max: 600 } }], }, ]); The ads of a new list start over: their sends counted from zero, their first send as soon as they can. Setting the same profiles again keeps their counts. A profile's warnings, such as a variable no profile sets, go to the bot's console as chat.setProfiles[1]: <warning>.
onChat() and offChat()
onChat(handler) has handler hear what the module does, in the order it happens, until offChat(subscription) or the end of the script. Each event is an object:
Field What it holds type "sent": a line went, or was written to the console in a dry run. "replied": a reply went. "refused": a line wasn't sent. "stopped": an ad's stop ended it. "soldOut": an item an ad links ran out. "backoff": every line waits, after "Too much spam!" or a mute. dryRun Whether the line was written to the console instead of sent who Who said the line: "person", "alarm", "command", "reply", "trigger", "script" or "ad" where In which channel, or to whom; for a stop or an item sold out, the ad text The line reason, detail Why a line wasn't sent, as say() rejects; which stop ended an ad; which item ran out; why every line waits Each field without a value is left out. The handler only observes: what it returns is ignored, and it can be async. A handler that throws stops the script.
A trigger with the script action gives an event of its own when that action's turn comes, after the actions before it are done, { type: "trigger", name, line }: the trigger's name, and the line it fired on, { senderName, senderAccountId, senderCharacterId, content, channel }, the ids as BigInts. A trigger without that action fires without the script hearing it.
import { onChat, whisper } from "asterobot:chat"; onChat(async (event) => { if (event.type !== "trigger" || event.name !== "wtb") return; const wanted = event.line.content.match(/\d+/)?.[0] ?? "some"; await whisper(event.line.senderName, `{Hi|Hello} ${event.line.senderName}, I can sell you ${wanted}`); }); import { onChat } from "asterobot:chat"; import { botWarn } from "asterobot:bot"; onChat(({ type, where, detail }) => { if (type === "soldOut") botWarn(`${where}: ${detail} sold out, restocking`); }); onAnswer() and offAnswer()
onAnswer(handler) has handler asked first about each private message the bot's replies get, until offAnswer(subscription) or the end of the script. It's asked with { kind: "reply", line }, line being the message: { senderName, senderAccountId, senderCharacterId, content, channel }, the ids as BigInts. It answers:
Answer What the bot does A text Whispers it back, as a template where {sender} names who wrote, after the time a person would take to read and type it { skip: true } Says nothing Nothing, null, undefined or an empty text The next handler is asked, then the profiles' reply rules decide The handler can be async: the bot waits up to 30 seconds for its answer, the reading and typing time running meanwhile. Past that, or when it throws or answers something else, the reply rules decide and the console says so. Its answers aren't counted by the profiles' answers per player, so it can hold a conversation. It's never asked about a message from one of your own bots or from a member of staff.
It's also asked about a command a profile takes but doesn't know, with { kind: "command", line, command, args }: !sell 10 wheat from a trusted sender is command "sell", args ["10", "wheat"]. A text answers the sender; nothing leaves the command ignored.
import { onAnswer } from "asterobot:chat"; import { memory } from "asterobot:bot"; onAnswer(({ kind, line }) => { if (kind !== "reply" || !/price|prix/i.test(line.content)) return; return `{Hi|Hello} {sender}, ${memory.get("wheatPrice") ?? 12} k each`; }); While Asterobot reads declarations
When Asterobot reads a package's settings, steps and conditions, it runs the top level of its modules without a bot (see Declarations). Every function of asterobot:chat then throws, or rejects for say() and whisper(), since there's no chat module to talk through.
Errors
These are thrown at once, or reject the promise of say() and whisper():
Error Type Cause chat.say channel "<name>" isn't a channel: write it as the protocol does, "SALES" for Trade TypeError The channel name is unknown chat: PRIVATE isn't a channel to say something in Error A whisper's channel, or one the game doesn't show: use whisper() chat.say must be a string, chat.whisper must be a string, chat.whisper name must be a string TypeError An argument isn't a string chat.pause seconds must be a positive number TypeError Seconds that are 0, negative, or not a number chat.setVariable name must be a string TypeError The name isn't a string chat.setVariable value must be a string, a number or a boolean TypeError An object, an array or a function as the value chat.setFlag name must be a string TypeError The name isn't a string chat.onChat handler must be callable, chat.onAnswer handler must be callable TypeError The handler isn't a function maximum behavior handlers reached TypeError The script holds as many handlers as a script can chat.setParts takes an object, { ads: false } TypeError The parts aren't an object chat.setParts: chat: invalid settings: "<part>" isn't a part: write ads, replies, triggers, commands or status TypeError An unknown part chat.setProfiles takes an array of profiles' names or profile objects TypeError The list isn't an array chat.setProfiles[<i>] must be a profile's name or a profile object TypeError An item that is neither chat.setProfiles[<i>]: chat: malformed profile: <field>: <why> TypeError A profile object Asterobot can't accept, naming the field chat.setProfiles: chatprofiles: no profile has this name: "<name>" Error A name no profile has chat.setProfiles: chat: malformed profile: <field>: <why> Error A saved profile, named in the list, that can't be used chat.<function>: this bot has no chat module Error Asterobot is reading declarations The bot's Console shows these lines:
Line Cause Dry run: would say in Trade: <message> A message in a dry run Waiting 38 s for the cooldown in Trade, then sending the line once more The server answered that the channel is in its cooldown The server said "Too much spam!": nothing is sent for 5 minutes A spam warning <moderator> muted the character for 15 minutes: nothing is sent until 21:40 A mute The mute is lifted: lines are sent again The mute ended Not said in Trade: <reason> A message typed in the Console, a reply or a command's answer that couldn't be sent Not answered <sender>: <reason> A reply whose message can't be filled Command !stop from <sender>: the script is stopped A command, and what the bot did Trigger wtb: <sender> in Trade: <line> A trigger fired Trigger wtb: whisper not done: <reason> One of a trigger's actions failed Trigger wtb: the chat is paused for 600 s A trigger's pauseChat Dry run: would invite <sender> to the party, Dry run: would ignore <sender>, Dry run: would play smiley 3 A trigger's action in a dry run Couldn't ignore <sender>: the list is full The game refused a trigger's ignore Status set to away by the chat profiles The status part set the character's status The status changed elsewhere: the chat profiles leave it The status changed from the bot's header, the person's client or a script Status not set: <reason>, Status not set back: <reason> The status part couldn't set the status, or take back the one before The answer hook didn't answer <sender> within 30 s: left to the rules An onAnswer() handler took too long chat answer hook failed: <error>: the reply rules decide, chat answer hook rejected: <error>: the reply rules decide An onAnswer() handler threw, or its promise rejected chat answer hook answered something else than a text, { skip: true } or nothing: the reply rules decide An onAnswer() handler's answer of another kind
  • 10 vues
Raise an event of your own from a script for the alarms' rules to take, such as an item found under a price, and hear every event of the bot as it happens: a member of staff, a whisper, the character's name said, the chat module's events, a trigger's, and the game messages the rules hook.
asterobot:alarms connects your script to the bot's alarms. It raises events of its own, which the rules on the Alarms page take like any other, and it hears every event of the bot as it happens, whatever the rules say.
import { raise, onAlarm, offAlarm } from "asterobot:alarms"; These are the module's only exports.
Export Returns What it does raise() Nothing Raise a script:<name> event for the bot onAlarm(), offAlarm() A subscription, nothing Hear every event of the bot raise()
raise(name, text?) raises the event script:<name> for the bot, its fields name and text. A rule taking script:<name>, or Every script event (script), fires on it, and its reactions read {name} and {text}.
import { raise } from "asterobot:alarms"; // A script scanning the marketplace found what it looks for. raise("cheapItem", "Wheat at 8 k"); On the Alarms page, a rule with Events script:cheapItem and the reaction Notify shows "Bot-Name: script:cheapItem" with "Wheat at 8 k" in every open Asteroboard. The script decides what's worth an alarm; the person decides what follows.
onAlarm() and offAlarm()
onAlarm(handler) has handler hear every event of the bot, until offAlarm(subscription) or the end of the script, whether a rule takes it or not, except the events this script raised itself with raise(): a handler that raises what it hears can't loop. Each event is an object:
Field What it holds name "staff", "whisper", "mention", "spam", "refused", "soldOut", "stopped", "trigger:<name>", "script:<name>" or "game:<Message>" bot, character, server The bot's name, its character's and its server's at When it happened, a Date fields What it says, as texts: reason, who, sender, channel, text, minutes... as the alarms guide lists them A game message is an event only while a rule on the Alarms page hooks it (game:<Message>): its fields are the message's, named as in the protocol with dots for nested ones, and json the whole message. To follow a message no rule hooks, listen to it with on() instead.
The handler only observes: what it returns is ignored, and it can be async. A handler that throws stops the script. The script hears its own raise() too.
import { onAlarm } from "asterobot:alarms"; import { stopTravel } from "asterobot:movement"; import { botWarn } from "asterobot:bot"; // Stop walking as soon as a member of staff shows up on the map, whatever the // rules on the Alarms page do. onAlarm(({ name, fields }) => { if (name !== "staff" || fields.reason !== "map") return; botWarn(`${fields.who} is on the map: standing still`); stopTravel(); }); While Asterobot reads declarations
When Asterobot reads a package's settings, steps and conditions, it runs the top level of its modules without a bot (see Declarations). Every function of asterobot:alarms then throws, since there's no bot to raise an event for.
Errors
Error Type Cause alarms.raise name must be a non-empty string TypeError The name isn't a string, or is empty alarms.raise text must be a string TypeError The text is given and isn't a string alarms.onAlarm handler must be callable TypeError The handler isn't a function alarms.raise: this bot has no alarms Error Called while Asterobot reads declarations
  • 9 vues
A script that has the bot's chat module sell wheat for you, at a price from the package settings, with an ad in Trade, an answer giving the stock to whoever asks the price, and a whisper to every buyer, which the script hears.
This script has the bot's chat module sell wheat for you. It posts an ad in Trade every few minutes, answers whoever asks the price with what's left in the bags, and whispers every player who asks for wheat in Trade. The price comes from the package settings, so you change it on the bot's page while the script runs.
The script writes no chat logic of its own: it hands the module a chat profile and a price, and the module does the rest through its guard, waiting out each channel's cooldown and keeping to the game's limits. You need to know how to create a package and run it, as in Create a package.
The script
Create a package named wheat-seller, replace the contents of its index.js with this code, and save:
import { botInfo, botWarn } from "asterobot:bot"; import { itemCount } from "asterobot:character"; import { onAnswer, onChat, setDryRun, setProfiles, setVariable } from "asterobot:chat"; import { values, onChange } from "asterobot:parameters"; // 289 is Wheat. const WHEAT = 289; /** @type {BehaviorParameters} */ export const parameters = { price: { type: "int", label: "Price of one wheat", unit: "kamas", default: 12, min: 1, }, dryRun: { type: "bool", label: "Dry run", description: "Write the messages in the bot's console instead of sending them.", default: true, }, }; // The chat profile the bot talks from while the script runs: one ad, and // one trigger for the buyers in Trade. const shop = { version: 1, name: "Wheat seller", // The script's own price wins over this one. variables: { price: "12" }, ads: [ { name: "Wheat in Trade", channels: ["SALES"], messages: ["{Selling|WTS} {item:289} at {price} k each, /w me", "{item:289} for sale, {price} k, /w {name}"], every: { min: 180, max: 300 }, }, ], triggers: [ { name: "buyer", when: { all: [ { channel: { is: "SALES" } }, { "sender.myBot": false }, { text: { keywords: ["wtb", "buy"] } }, { any: [{ text: { keywords: ["wheat"] } }, { items: { has: WHEAT } }] }, ], }, do: [{ whisper: ["{Hi|Hello} {sender}, I sell {item:289} at {price} k each"] }, { script: true }], }, ], }; export default async function behavior() { setVariable("price", values.price); setDryRun(values.dryRun); setProfiles([shop]); onChange(({ name }) => { if (name === "price") setVariable("price", values.price); if (name === "dryRun") setDryRun(values.dryRun); }); // Asked first about each private message the bot gets. A price question // gets the stock too; anything else is left to the profiles' replies. onAnswer(({ kind, line }) => { if (kind !== "reply" || !/\b(price|how much)\b/i.test(line.content)) return; const left = itemCount(WHEAT); if (left === undefined) return; if (left === 0) return "Sorry {sender}, no wheat left for now"; return `{Hi|Hello} {sender}, ${left} {item:289} left, {price} k each`; }); onChat((event) => { if (event.type === "trigger" && event.name === "buyer") { botInfo(`Buyer in Trade: ${event.line.senderName}: ${event.line.content}`); } if (event.type === "soldOut") botWarn("Out of wheat: the ad waits for more"); }); } How it works
The profile comes from the script
setProfiles([shop]) gives the bot the script's own profile, in place of the ones you picked on its Chat tab, for as long as the script runs. It's an object in the chat profile format, which Asterobot checks as it checks a saved profile: a mistake throws a TypeError naming the field. When the script stops, the bot goes back to your profiles.
The ad links the wheat with {item:289}, as the game links an item, from the character's inventory. While the character has none, the ad waits, and the Chat tab says why. {Selling|WTS} picks one wording at random each time, and the ad says one of its two messages at random, never the same twice in a row.
The price is a variable
The messages read {price}, which setVariable() sets from the package setting. When you apply a new price on the bot's page, onChange() sets the variable again, and the next message says it. The profile, the ad's counts and its next send stay as they were.
The script's variables win over the profile's, so the profile's own price is never said while the script runs. It's there because Asterobot checks the profile on its own, and warns in the console about a variable the profile reads but doesn't set.
The trigger
The trigger fires on a line said in Trade, by a player who isn't one of your bots, with the word wtb or buy, and either the word wheat or a link to wheat. all and any combine the tests as in a fight rule. Keywords are whole words, capitals and accents ignored, so WTB Wheat fires it and wheatgrass doesn't.
Its actions run in order. The whisper waits the time a person would take to read the line and type the answer, and {sender} names who said the line. Then script: true tells the script: onChat() gets { type: "trigger", name: "buyer", line }. A player fires it at most once an hour, the default.
The answer
onAnswer() is asked about each private message before the profile's reply rules. A text answers: the bot whispers it back after the reading and typing time, filled in like any message, so {sender}, {price} and the item link work. return with nothing leaves the message to the reply rules, and this profile has none, so the bot says nothing.
itemCount() is undefined until the game has sent the inventory. The handler then answers nothing rather than say a wrong number.
The dry run
The Dry run setting starts on: the bot writes every message in its console, such as Dry run: would say in Trade: ..., instead of sending it. Everything else runs as if they were sent, the ad's wait and the trigger's limit included. Turn it off once the messages read well.
Run it
Try it on a MITM bot whose character is in the game, with some wheat in its bags, as Your first bot sets up.
On the bot's page, open the Chat tab and turn on Chat module. On a relayed session only your switch counts: a script can't turn the module on there. Leave Ads, Replies and Triggers on. In the editor, pick the bot and click Run. Back on the Chat tab, the tab says the running script sets the profile Wheat seller and the dry run, and the Ads section shows Wheat in Trade with its next send. Open the Console. Within a few seconds, a line starting Dry run: would say in Trade: writes the ad. The replies and the triggers answer neither your own bots nor yourself, so try them with a friend, or with an account this Asterobot has never played:
Have them whisper the bot price?. A few seconds later, Dry run: would say to their name writes the answer, with the stock. Have them say wtb wheat in Trade. The console says Trigger buyer: with their name and line, then, a few seconds later, the whisper, then the script's Buyer in Trade: line. On the bot's Settings tab, under Package settings, change Price of one wheat and click Apply. The next ad says the new price. Turn off Dry run and click Apply. The bot now sends for real. Click Stop: the Chat tab goes back to your own profiles, and the ad stops.
Ideas to take it further
Sell more than wheat. Make the price a map setting of item ids to prices, set one variable per item, and give the profile an ad per item. Open hours. Add when: { flag: "open" } to the ad, and have the script call setFlag("open") and setFlag("open", false) when it decides the shop opens and closes. Get notified. Add { raise: "buyer" } to the trigger's actions, and a rule on the Alarms page that takes the trigger:buyer event and notifies Asteroboard. Ship the profile. Save the profile as chatprofiles/wheat-seller.json in the package, as Package layout shows, and pass its name to setProfiles(): people can then copy it and make it their own. The asterobot:chat reference covers every function used here.
  • 12 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.