asterobot:protocol is how a script exchanges messages with the game: it sends them, waits for them, reacts to them, and changes or blocks them on their way through. Listening, Waiting, Sending and Intercepting explain how to use each part.
import { send, request, wait, on, onTraffic, intercept, off } from "asterobot:protocol";
These are the module's only exports. There's no protocol object to import.
| Export | What it does |
|---|---|
send() |
Sends a message, and resolves once it's sent |
request() |
Sends a request, and resolves with the response matched to it |
wait() |
Resolves with the next inbound message that matches |
on() |
Calls a handler for every inbound message that matches |
onTraffic() |
Calls a handler for every message, in both directions |
intercept() |
Decides whether a message goes on unchanged, is dropped or is replaced |
off() |
Removes a handler registered with on(), onTraffic() or intercept() |
Message names and fields are the ones in the Dofus protocol reference.
send()
send(type, payload, options?) returns Promise<void>.
| Parameter | Type | Description |
|---|---|---|
type |
string | The message's name, such as "ChatChannelMessageRequest" |
payload |
object | The message's fields. Pass {} for a message you send without fields: leaving payload out is an error. |
options.timeout |
number | Milliseconds before the promise rejects. Default: 30000. |
options.to |
"server" or "client" |
Where the message goes. Default: "server". |
send() builds the message and writes it. The promise resolves as soon as the message is written: it doesn't wait for an answer, so use wait() when you need to know what the game did.
With to: "server", the message goes to the game server as if the Dofus client had sent it. onTraffic() handlers see it go out, and no interceptor is asked about it.
With to: "client", the message is delivered as an event from the server:
- On a MITM bot, Asterobot writes it to the Dofus client, which acts on it as if the server had sent it.
- On a Full socket bot there's no Dofus client, so the message only reaches the script's own
on(),onTraffic()andwait().
Either way it arrives as an inbound message, so the same script works on both kinds of bot.
On a MITM bot, an IdentificationRequest sent to the server is never forwarded, because the Dofus client has already identified the session. The promise resolves anyway, and the bot's Console shows the warning Refused to forward a message that would re-authenticate a relayed session.
The promise rejects with:
| Error | Cause |
|---|---|
protocol type must be a non-empty semantic message name |
type is missing, empty or not a string |
semantic protobuf message is not mapped: <type> |
Asterobot has no message with that name. Check the spelling, and see After a game update. |
<type>: expected an object |
payload is missing, null or undefined |
<type>.<field>: unknown protobuf field |
The message has no field with that name, for example IdentificationRequest.token: unknown protobuf field |
Another text starting with <type> |
A field's value doesn't fit. Payload errors lists them all. |
protocol options must be an object |
options isn't an object, for example send(type, payload, 5000) |
unknown protocol option "<name>" |
options has a key other than timeout and to |
timeout/milliseconds must be a finite positive Number |
timeout isn't a number greater than 0 |
timeout/milliseconds is too large |
timeout is too big to be a duration |
send option "to" must be "server" or "client" |
to isn't a string |
unknown send target "<value>": expected "server" or "client" |
to is a string other than those two |
behavior resource limit exceeded: maximum pending operations reached |
32 operations are already pending. See Limits. |
It also rejects when the message can't be written before the timeout, or when the game connection closes.
await send("ChatChannelMessageRequest", { channel: "GLOBAL", content: "Hello" });
request()
request(type, payload, options?) returns Promise<ProtocolTraffic>.
| Parameter | Type | Description |
|---|---|---|
type |
string | The request's name |
payload |
object | Its fields, as for send() |
options.timeout |
number | Milliseconds before the promise rejects. Default: 30000. |
options.to is accepted and ignored: a request always goes to the server.
request() sends the message as a request carrying a new uid, and resolves with the response the server sends back with the same uid, as a traffic object whose kind is "response".
Only a few Dofus requests get such a response. The game answers most of them with an event, or not at all, and request() then waits for its whole timeout before rejecting. For those, send the message with send() and wait() for the event.
The promise rejects with the same argument, payload and limit errors as send(), apart from the two about to, and with:
| Error | Cause |
|---|---|
context deadline exceeded |
No response came back before the timeout |
mitm relay refuses to identify: this Game session was already authenticated by the real DOFUS client |
The script sent IdentificationRequest on a MITM bot. The request isn't forwarded. |
wait()
wait(selector, options?) returns Promise<ProtocolTraffic>.
| Parameter | Type | Description |
|---|---|---|
selector |
string or function | A message name, "*" for any message, or a function that receives a traffic object and returns true for the message you want |
options.timeout |
number | Milliseconds before the promise rejects. There's no default: without a timeout, wait() waits until the script stops. |
options.to is accepted and ignored.
wait() looks at inbound messages that arrive after the call, and resolves with the first one that matches. When several waits match the same message, they all resolve with it. For each message, the handlers registered with on() and onTraffic() run before the waits are checked.
Since it only sees what arrives after the call, start the wait before you send the message it waits for, for example with Promise.all(): the answer can arrive before await send() has resolved. Waiting shows how.
A selector function runs for every inbound message until the wait settles. If it throws, the whole script stops, with evaluate protocol wait selector: <error>.
The promise rejects with:
| Error | Cause |
|---|---|
protocol wait timed out |
The timeout passed before a message matched |
protocol selector cannot be empty |
selector is "" |
protocol selector must be a semantic type, '*', or predicate |
selector is neither a string nor a function |
protocol options must be an object, unknown protocol option "<name>", timeout/milliseconds must be a finite positive Number, timeout/milliseconds is too large |
The options are wrong, as for send() |
behavior resource limit exceeded: maximum waits reached |
128 waits are already open |
const echo = await wait(
(traffic) => traffic.type === "ChatChannelMessageEvent" && traffic.payload?.content === "Hello",
{ timeout: 5000 },
);
on()
on(selector, handler) returns a handle, { id }, where id is a BigInt.
| Parameter | Type | Description |
|---|---|---|
selector |
string or function | A message name, "*" for any message, or a function that returns true for the messages to handle |
handler |
function | Called with the traffic object of each matching inbound message. It may be async. |
on() sends nothing and returns right away. From then on, handler runs for every inbound message that matches, until you pass the handle to off() or the script stops. Outbound messages never reach it, whatever the selector.
Handlers run one message at a time, in the order they were registered, onTraffic() handlers included. A handler registered while a message is being handled starts with the next message. An async handler returns at its first await, and the next handler doesn't wait for it to finish.
An error in a handler stops the script:
| What happened | Text in the Problems alert |
|---|---|
| The handler threw | Game traffic handler failed: <error> |
An async handler's promise rejected |
unhandled Promise rejection: <error> |
| The selector function threw | evaluate protocol handler selector: <error> |
on() throws a TypeError at once, instead of returning, when:
| Error | Cause |
|---|---|
protocol selector cannot be empty |
selector is "" |
protocol selector must be a semantic type, '*', or predicate |
selector is neither a string nor a function |
protocol.on handler must be callable |
handler isn't a function |
maximum behavior handlers reached |
256 handlers from on(), onTraffic() and intercept() are already registered |
If the script is already stopping, on() throws the reason it's stopping.
const handle = on("ChatChannelMessageEvent", (traffic) => {
if (!traffic.payload) return;
botInfo(`${traffic.payload.senderName}: ${traffic.payload.content}`);
});
onTraffic()
onTraffic(handler) returns a handle, { id }.
It works like on("*", handler), except that it receives messages in both directions:
- what the game server sends;
- what the Dofus client sends, on a MITM bot;
- what the script sends with
send()andrequest(); - what someone sends from the bot's Network tool.
A message dropped by an interceptor never reaches it.
onTraffic() throws a TypeError with protocol.onTraffic handler must be callable when handler isn't a function, and with maximum behavior handlers reached past the limit. Errors in the handler stop the script, with the same texts as for on().
intercept()
intercept(selector, handler) or intercept(handler) returns a handle, { id }.
| Parameter | Type | Description |
|---|---|---|
selector |
string or function | Optional. A message name, "*", or a function that returns true for the messages to decide about. With only one argument, handler is asked about every message. |
handler |
function | Called with the traffic object, and returns a decision. It must answer synchronously. |
An interceptor is asked about the messages the bot carries: every inbound message from the game server, and on a MITM bot every outbound message from the Dofus client. It's never asked about:
- a message the script sends, nor one sent from the bot's Network tool, although both still reach
onTraffic(); - on a MITM bot, the Dofus client's first message, and the responses to the script's own
request()calls; - a message Asterobot can't read at all. A MITM bot forwards such a message without showing it to the script, and a Full socket bot closes its game connection when it receives one.
For each message, interceptors are asked in the order they were registered. One whose selector doesn't match, or that returns no decision, leaves the message to the next. The first decision wins, and the interceptors after it aren't asked.
As soon as one interceptor is registered, every message the bot carries waits for the script, whatever the selectors say, because Asterobot checks them inside the script. Remove an interceptor with off() once you no longer need it.
| Return value | What happens |
|---|---|
undefined, null, or any value not listed below |
No decision. The next interceptor is asked, and when none decides, the message goes on unchanged. |
"drop" |
The message stops here. On a MITM bot the other side never receives it, and on every bot on(), onTraffic() and wait() never see it. On a Full socket bot, dropping the response to a request() makes that request() wait for its timeout. |
{ payload } |
The message goes on, rebuilt from these fields. It keeps its name, its kind and its uid. A field you leave out of the object is left out of the message, and the fields Asterobot has no name for are lost. |
{ raw } |
The message is replaced by these bytes, a Uint8Array or an ArrayBuffer. They stand for the whole message as it travels on the connection, envelope included, not for traffic.rawAny. raw is used when both raw and payload are present. |
After a replacement, the script's handlers and waits see the new message. A MITM bot forwards raw bytes even when Asterobot can't read them, and its handlers then see the original message. On a Full socket bot, bytes Asterobot can't read change nothing.
An async handler returns a promise, and a promise isn't a decision: the message goes on before the promise settles. If that promise rejects, nothing catches it, and the script stops with unhandled Promise rejection: <error>.
The message waits at most 50 ms for a decision, counted from when it arrives, so time the script spends on other work counts too. Past that, it goes on unchanged and nothing is written anywhere: the handler keeps running, and its answer is ignored. On a MITM bot, the person playing feels a slow interceptor as lag. Intercepting explains these rules with examples.
When a handler throws, or its selector function throws, the script doesn't stop. The message goes to the next interceptor, or on unchanged, and the bot's Console gets an error line for that message. When several interceptors fail on the same message, the line is written for the first failure only, and again at the tenth, ending with (10 times).
| Console line | Cause |
|---|---|
interceptor failed: <error> |
The handler threw, or ran for more than 250 ms |
interceptor selector failed: <error> |
The selector function threw |
build replacement payload for <type>: <error> |
The payload doesn't fit the message. <error> is one of the payload errors. |
re-encode <type>: <error> |
Asterobot couldn't rebuild the message with the new fields |
cannot replace the payload of <wire name>: this build has no name for it, so there is nothing to build a replacement from - use { raw } instead |
{ payload } was returned for a message without a readable name |
intercept() throws a TypeError, like on(), for a bad selector, with protocol.intercept handler must be callable when handler isn't a function, and with maximum behavior handlers reached past the limit.
intercept("ChatChannelMessageRequest", (traffic) => {
if (!traffic.payload) return;
return { payload: { ...traffic.payload, content: traffic.payload.content.toUpperCase() } };
});
On a MITM bot, this turns everything the player types in chat into capital letters before the server receives it.
off()
off(handle) returns undefined.
It removes a handler registered with on(), onTraffic() or intercept(), given the handle they returned. A handler removed while a message is being handled doesn't run for that message if its turn hasn't come yet. off() never throws: a handle that was already removed, or anything else, is ignored. It doesn't remove what onChange() from asterobot:parameters registered: use offChange() for that.
The traffic object
Handlers, selector functions and interceptors receive a traffic object, and wait() and request() resolve with one. Traffic explains its properties in depth.
| Property | Type | Value |
|---|---|---|
sequence |
BigInt | The message's position in the bot's traffic. A dropped message leaves a gap. On a MITM bot, the object request() resolves with has 0n. |
direction |
string | "inbound" from the server towards the client, "outbound" from the client towards the server |
kind |
string | "request", "event" or "response". The type also lists "unknown", which scripts never receive, because a message Asterobot can't read isn't delivered. |
uid |
number | The number that pairs a request with its response. -1 on events, and on messages sent with send(). |
type |
string | The message's readable name, or "" when Asterobot has no name for it |
wireType |
string | The name the message has on the connection, which changes between game versions |
typeUrl |
string | The full type identifier of the message's content |
payload |
object, or absent | The message's fields, when Asterobot could read them |
unknown |
boolean | true when there's no payload |
decodeError |
string, or absent | What went wrong while reading the message's content. It can be there along with a payload. |
rawAny |
Uint8Array | The message's content as bytes, without its envelope. Always present, possibly empty. |
Payload values
A payload's field names are the camelCase names, such as senderName. When sending, the names from the protocol definitions, such as sender_name, work too, but not both for the same field. Payloads covers every case.
| Field type | A script reads | A script can send |
|---|---|---|
| 64-bit integer | A BigInt | A BigInt, a string of digits, or a whole number no bigger than Number.MAX_SAFE_INTEGER either way |
| 32-bit integer | A number | A whole number within the field's range |
| Floating point | A number | A number |
| Boolean | A boolean | A boolean |
| String | A string | A string |
| Bytes | A Uint8Array | A Uint8Array or an ArrayBuffer |
| Enum | The value's name, such as "GLOBAL", or a number for a value without a name |
The name or the number |
| List | An array | An array |
| Map | An object with string keys | A plain object |
| Nested message | An object | An object, or null to leave it out |
| Group of alternatives (oneof) | Only the field that's set | One field of the group at most |
A nested message, an optional field or an alternative that the message doesn't set is absent from payload. Other fields are always there, with 0, "", false or an empty array when they're not set.
When sending, a field you leave out isn't sent. Setting a field to undefined is an error, and null is only accepted for a nested message.
The editor completes message names and payload fields, and knows the types ProtocolTraffic, ProtocolOptions, ProtocolSelector, ProtocolSubscription and ProtocolDecision.
Aucun avis à afficher.