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 |