A bot runs its script one piece of code at a time. Once you know how those pieces are scheduled, the 250 ms limit, long loops and the order of handlers stop being surprises.
One piece of code at a time
Each bot runs its own copy of the script, and bots run side by side without sharing anything. Inside one bot, a single piece of your code runs at any moment: the entry function, a handler, or the rest of an async function after an await. The next piece only starts once the current one has finished or reached an await.
Everything ready to run waits in the bot's queue, and is handled in the order it arrived:
- a message, for your handlers and your
wait()calls - the result of a
send(), arequest()or a game data call - a
sleep()that's over, or await()whose timeout ran out - settings applied from Package settings, or an action button someone pressed
- a question for your interceptors about a message on its way
await keeps the bot responsive
When your code reaches await on something that hasn't settled yet, that piece of code ends there and the bot goes back to its queue: other messages are handled and other handlers run. Once the awaited thing settles, the rest of your function continues as a new piece.
That's what lets a handler await wait() for an answer while the bot keeps receiving messages. A message only resolves the wait() calls already open when it arrives, so open the wait before you send what it answers: Waiting shows how.
Code that runs without awaiting does the opposite. As long as it runs, nothing else happens for that bot: no handler runs, messages pile up in the queue, and interceptors can't answer, so after 50 ms the message they were asked about goes through unchanged (see Intercepting).
The 250 ms limit
A piece of code may run for 250 ms at most. Past that, Asterobot interrupts it, and the script stops with an error containing JavaScript execution deadline exceeded. The bot stays connected.
The limit applies to each of these on its own:
- the top-level code of your modules, at each start
- the entry function, up to its first
await - each call of a handler registered with
on(),onTraffic()oronChange() - the code that continues after an
await. When several awaited things settle together, the code that continues from all of them shares one 250 ms.
An interceptor or an action button's run that goes over the limit doesn't stop the script: the interceptor's message goes through unchanged, and the action is reported as failed.
Only time spent running counts. Waiting on an await doesn't: a sleep(10_000) or a five-second wait() is fine. What goes over is work done in one go, such as a long loop, a big calculation, or thousands of synchronous calls like record() and text() in a row.
To get through a long job, let the bot breathe now and then with await sleep(0). Each part then runs as its own piece, with its own 250 ms:
for (let i = 0; i < ids.length; i++) {
handle(ids[i]);
// Every 500 items, let the bot handle its queue and start a new 250 ms.
if (i % 500 === 499) await sleep(0);
}
For game data, a single find() or query() often replaces thousands of record() calls, and it does its work outside your code's 250 ms. See Game data overview.
sleep()
import { sleep } from "asterobot:timers";
await sleep(1500);
sleep() is the only timer scripts have. It takes a number of milliseconds, and 0 is allowed. Its promise resolves once the time has passed, and your code continues when the bot reaches it in its queue, so a busy bot resumes a little late.
| When | sleep() rejects with |
|---|---|
| The duration is negative, infinite or not a number | timeout/milliseconds must be a finite positive Number |
| 128 sleeps are already in progress | behavior resource limit exceeded: maximum timers reached |
| The script stops | The reason it stopped, such as behavior runtime closed after Stop |
There's no setTimeout(). To do something later without holding up the function you're in, call an async function without awaiting it, and let that function await sleep() first. Catch the errors inside it: a rejected promise that nothing catches stops the script.
Loops
Start long loops without awaiting them
A start only counts once the entry function has returned, as The entry function explains. A while (true) loop awaited inside the entry function keeps the Play button loading for good, and keeps the next start "initial". Start the loop from the entry function without awaiting it:
import { session, botInfo, botWarn } from "asterobot:bot";
import { send } from "asterobot:protocol";
import { sleep } from "asterobot:timers";
async function doOneRound() {
botInfo("One more minute");
}
async function everyMinute() {
while (true) {
try {
await doOneRound();
} catch (error) {
botWarn("This round failed:", String(error));
}
// Outside the try: once the script stops, sleep() rejects, and that
// rejection is what ends the loop.
await sleep(60_000);
}
}
export default async function behavior(launch) {
// A new game connection starts with this message. MITM bots never launch
// with "initial": the Dofus client has already identified their session.
if (launch.reason === "initial") {
await send("IdentificationRequest", {
ticketKey: session.gameToken,
languageCode: session.language,
});
}
// Not awaited: the start only counts once this function has returned.
everyMinute();
}
Keep the await sleep() outside the try. When the script stops, every pending sleep() rejects, and so does every later call. A catch around the sleep() would catch that rejection and send the loop round again instead of ending it.
To let people pause such a loop, read a setting at each round, as in if (values.enabled) await doOneRound();. Don't skip the sleep() when the setting is off: a loop that never awaits runs into the 250 ms limit. Read settings covers values.
React to messages instead of polling
Most scripts don't need a loop at all. A handler registered with on() runs each time a message arrives, and wait() pauses a function until one does. Listening and Waiting cover both.
The order things run in
When a message arrives:
- Your interceptors are asked first, in the order they were registered, and the first decisive answer wins. A message they drop never reaches the next steps. See Intercepting.
- Each handler matching the message is called, in the order it was registered. Handlers registered with
on()only receive inbound messages, and those registered withonTraffic()receive both directions. - The
wait()calls matching the message resolve, the oldest first. - The code that continues because of this message, such as a function awaiting one of those
wait()calls, runs after every handler has been called.
Handlers don't wait for each other. An async handler runs until its first await, then the next handler is called, and the rest of the first one runs later, possibly after other messages. A handler that throws stops the script: see Errors.
Messages are handled in the order they arrive. The onChange() handlers of settings are the exception to any ordering: they have no fixed order.
When the bot falls behind
The queue holds 64 items. If your code keeps the bot busy while messages keep arriving, which happens quickly on a busy MITM session (man-in-the-middle: the bot relays the game session of a Dofus client someone plays), the queue fills up and the script stops with behavior event-loop queue overflow. Keep handlers short, and don't do heavy work on every message.
Interceptors never fill the queue: when it's full, the message they would have been asked about goes through unchanged.
A few calls have caps of their own. At most 32 send(), request() and game data calls can be in progress at once, all of them counted together, and the next one rejects with behavior resource limit exceeded: maximum pending operations reached. At most 128 wait() calls can be open, and the next one rejects with behavior resource limit exceeded: maximum waits reached. Limits lists every limit.
Next, Errors.
Aucun avis à afficher.