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, sostate.loadedAt === undefinedmeans "not loaded yet on this connection". - Record before you send, when sending twice would be wrong. If the script stops between
set()andsend(), 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 firstupdateConnection()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 in153879301n, andnullshows 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.