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:
itemshas one row per item. Itsidcolumn holds the id the game uses in its messages, thegid. Its_idcolumn is the key Asterobot gives the row, which isn't always the same number, as Lookups explains._namesholds the names of records, one row per name and per language.record_tableandrecord_idpoint to the record by its key,languageholds the language code, andordinal0marks 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 theparamsarray 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 alimit, and 5,000 at most. Each batch asks for as many rows as it has ids, andBATCH_SIZEkeeps every batch far below that maximum.- Rows are arrays in the order of the
SELECT, soconst [gid, name]reads the id first and the name second. query()resolves toundefinedwhen the bot's game version has no extracted game data. The script throws then, and the handler'scatchwrites 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, keep only Script in Sources, then choose your character in Dofus. A line such as
Inventory received: 57 stacks, 57 of them namedappears. - 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.
_nameshas rows for each language the game data holds. RunSELECT DISTINCT language FROM _namesin the SQL tab to see which, then add astringsetting withchoicesand passvalues.languageto the query instead ofsession.language. - The item's type. Join
item_typesonitem_types.id = items.type_id, then its name through_namesin 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'sidis the row's key: read the row withrecord()to get itsidcolumn, 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.
Aucun avis à afficher.