Output, log and storage
What a script does when it is not talking to the game: write down what it sees, raise an alert, and remember from one pass to the next.
Writing to the console
The console is the panel below the editor, on the Scripts page. It shows what your script writes, live.
| Function | Returns | |
|---|---|---|
Print(...) | nothing | Writes a line. Arguments are joined with a space. |
print(...) | nothing | The same, under its other name. |
Printf(format, ...) | nothing | Writes a formatted line, the way fmt.Printf does. |
Print("target", coord, "loot", loot) // target [P:1:64:6] loot 75
Printf("%d planets, %d moons", np, nm)
A Print line goes both to the console and to the bot's log. You will find it again on the Logs page, tagged with your script's name, which is handy for reading back a whole night.
Logging by level
When a script runs for a long time, writing everything at the same level makes the log unreadable. These functions write at a level, and the Logs page can filter on it.
| Function | Level | When to use it |
|---|---|---|
LogDebug(...) / LogDebugf(format, ...) | Info | The detail that interests you alone, on a debugging day. |
LogInfo(...) / LogInfof(format, ...) | Info | The normal run: "campaign started", "twelve targets kept". |
LogWarn(...) / LogWarnf(format, ...) | Warn | What deserves a look without being serious: a lost target, an unreadable page. |
LogError(...) / LogErrorf(format, ...) | Error | What failed. |
LogDebug is not a level of its own. It writes at Info, exactly like LogInfo: routing a "debugging detail" to a real Debug level would have made it vanish from the logs of an instance set to Info, which is the default setting. A LogDebug line therefore cannot be filtered separately from LogInfo on the Logs page.
LogInfof("%d targets kept out of %d", kept, seen)
if err != nil { LogError("reading failed:", err) }
The f variants take a format as their first argument, the others join their arguments. All of them accept as many arguments as you want.
Remembering between two passes
A script that restarts starts over from scratch. These five functions give it a memory, stored in the bot's database, that survives both a shutdown and an update.
| Function | Returns | |
|---|---|---|
Put(key, value) | 1: an error | Stores a value. |
Get(key) | 2: the value and an error | Reads it back. A missing key returns the empty string, without an error. |
Has(key) | 1: a boolean | Says whether the key exists. |
Delete(key) | 1: a boolean | Erases. |
StorageKeys() | 1: a list | Every key stored by this script. |
Put("last.system", 128)
sys, err = Get("last.system") // ← TWO targets, see pitfall no. 1
if err != nil { LogError(err) }
if sys == "" { sys = 1 } // never written: empty string
for key in StorageKeys() {
Print(key)
}
Get is the textbook case of the first pitfall: written sys = Get("last.system"), it returns [128 <nil>] and all your comparisons fail silently.
What the memory keeps. Values are stored as JSON: a number comes back a number, a boolean comes back a boolean, a table comes back a table. Each script has its own, no script reads its neighbour's, and renaming a script takes its memory with it. To share, see below.
A key never written returns the empty string, and that traps lists
This is the first run: the key does not exist yet, Get returns "", and a string cannot be looped over. Your loop stops on "for cannot loop over type string", and your blacklist was empty anyway.
// Wrong on the first run.
blacklist, err = Get("blacklist")
for d in blacklist { ... } // ← "for cannot loop over type string"
// Right: start from a list, and only read back if the key exists.
blacklist = []
if Has("blacklist") {
blacklist, err = Get("blacklist")
}
And the message will not show the failing line. When the fault is inside a function body, the engine only reports the line of its DECLARATION - and if that function calls another, it is still the caller's declaration that comes out. A broken loop deep inside your inArray() will be announced on the line of the func that calls it. The message now says so, but it cannot name the real line: the engine threw it away.
Sharing between bots
Put's memory belongs to one script. These five functions store in a memory shared by every bot of your Kepler: what a script writes on one account, another script reads back on another account. Same rules as above: values are stored as JSON, a missing key returns the empty string, and the memory survives both a shutdown and an update.
| Function | Returns | |
|---|---|---|
PutShared(key, value) | 1: an error | Stores a value, readable by every bot. |
GetShared(key) | 2: the value and an error | Reads it back. A missing key returns the empty string, without an error. |
HasShared(key) | 1: a boolean | Says whether the key exists. |
DeleteShared(key) | 1: a boolean | Erases. |
SharedKeys() | 1: a list | Every shared key. |
// On the bot that decides
PutShared("fs.target", "4:120:8")
// On any other bot of the same Kepler
target, err = GetShared("fs.target") // ← TWO targets, like Get
if err != nil { LogError(err) }
if HasShared("fs.done") {
DeleteShared("fs.done")
}
for key in SharedKeys() {
Print(key)
}
Every script of every bot writes to the same place. Prefix your keys - fs., farm. - so you don't overwrite another script's: nothing protects you here. Deleting or renaming a script never touches this memory, since it belongs to none of them.
Sharing works within one Kepler. The bots of the same installation see each other; on the Cloud, two instances are two separate Keplers, and share nothing.
Alerting the outside world
| Function | Returns | |
|---|---|---|
SendDiscord(webhook, message) | 1: an error | Posts to a Discord webhook. |
SendDiscordFile(webhook, name, content) | 1: an error | Posts a text file as an attachment. |
SendTelegram(chatID, message) | 1: an error | Writes into a Telegram conversation. |
err = SendDiscord("https://discord.com/api/webhooks/…", "debris at " + coord)
if err != nil { LogError("Discord:", err) }
Both write using the bot's own settings. TELEGRAM_CHAT_ID and DISCORD_WEBHOOK carry what you filled in on the Notifications page: a script copied from Ninja that writes SendDiscord(DISCORD_WEBHOOK, …) therefore goes to the right place, with nothing more to configure.
SendDiscord(DISCORD_WEBHOOK, "debris at " + coord)
SendTelegram(0, "debris at " + coord) // 0: the conversation from settings
Those two constants are read when the script starts. If you change your settings while it runs, restart it. SendTelegram(0, …), on the other hand, re-reads its own on every call.
A table goes out as a file, not as confetti
SendDiscord cuts at two thousand characters and posts the pieces. For an alert that is what you want; for a three-hundred-row table it is no longer a table. SendDiscordFile sends it in one piece, as an attachment.
rows = "coord;player;points\n"
for p in found {
rows = rows + p.Coord + ";" + p.Name + ";" + Itoa(p.Points) + "\n"
}
err = SendDiscordFile(DISCORD_WEBHOOK, "inactives.csv", rows)
if err != nil { LogError("Discord:", err) }
Nothing is written to disk, not on your machine and not on the server: the content goes straight into the request. That is the whole reason this exists rather than file access - what a script usually wants is not a file, it is getting its result to someone.
The second use is debugging. When a run goes wrong, the thing to look at is the raw answer that confused your parsing, not a summary of it, and a message truncates it exactly where it gets interesting.
The name is cleaned up, never refused: a path is reduced to its last part, an empty name becomes rapport.txt. The script wanted to send its content, not argue about the name.
The size limit is your Discord server's, not ours: its ceiling rises with the server's tier. Above 25 MiB we refuse outright; below that, if Discord refuses, you get its own answer, and that is what states the real limit.
This one is Kepler's own. The reference tool only knows SendDiscord.
Nothing forces you to use them: an address written out in full works too, and Put or !global.ank share it between your scripts.
// in !global.ank
MY_WEBHOOK = "https://discord.com/api/webhooks/…"
Telegram
SendTelegram(chatID, message) writes with the bot's Telegram bot, the one whose token you pasted on the Notifications page. You have no token to copy into your scripts, and nothing secret sits in them.
| Form | Where the message goes |
|---|---|
0 | The conversation from your settings. This is the ordinary case. |
| A positive number | That conversation. |
| A negative number | A group or a channel — Telegram numbers them that way. |
With no token or conversation in your settings the call returns an error and nothing is sent: SendTelegram: Telegram is not configured yet. Test its return value, as you would for Discord.
The text goes out as it is, with no formatting. That is deliberate: a script's output contains coordinates, nicknames and angle brackets, and in HTML mode a single one of them would make Telegram refuse the whole message — the alert would be lost at the worst moment.
A message longer than 4,096 characters is split rather than rejected. Discord does the same at 2,000.
Steering your own script
| Function | Returns | |
|---|---|---|
Exit() | nothing | Stops the script cleanly. |
Terminate() | nothing | The same. |
IsPaused() | 1: a boolean | Says whether the player has paused the script. |
Exit and Terminate have no effect from !global.ank: that file defines the shared environment, and stopping it would make no sense. A warning in the log reminds you if you try.
if IsPaused() { SleepSec(30); continue }
A few conveniences
| Function | Returns | |
|---|---|---|
Shuffle(list) | nothing | Shuffles a list in place. |
Bytes2Str(bytes) | 1: a string | Converts bytes to text. Anko cannot write a []byte literal, hence this function. |
targets = [1, 2, 3, 4, 5]
Shuffle(targets) // the order changes, the list stays the same
Shuffle shuffles the list you give it and returns nothing: do not write l = Shuffle(l) or you will lose the list.
The current time is read with Clock, Date and Now, on the Time and waiting page.
Raising an alert
| Function | Returns | |
|---|---|---|
Notify(title, message) | nothing | Writes to the account's journal, at Warn level. |
A difference from Ninja, better read here. There, Notify pops a bubble on the desktop of the machine running the bot. Kepler usually runs on a server nobody is watching: a bubble there would be seen by no one, and claiming otherwise would be a lie.
So it writes at Warn level into the account's journal — the one the Journal page shows and that can be read back later. To be told on your phone, call SendDiscord or SendTelegram: those reach outside, and that is a deliberate gesture rather than a side effect.