Kepler. Documentation · Scripts 325 functions available right now Français

Account, galaxy, players and messages

Knowing who you are, looking at what sits in a system, recognising the other players, reading the highscore, and writing to someone.

The account

FunctionReturns
GetCachedPlayer()1: your own recordPlayerID, PlayerName, Points, Rank, Total, HonourPoints.
GetSlots()1: your fleet slotsInUse, Total, ExpInUse, ExpTotal.
ServerURL()2: an address and an errorYour server's address, with no trailing slash. A Kepler addition.
me = GetCachedPlayer()           // ONE target: this is a struct
Print(me.PlayerName, me.Points, "points, rank", me.Rank, "of", me.Total)

slots = GetSlots()               // ONE target as well
if slots.InUse >= slots.Total { Print("not one free slot left") }
if slots.IsAllSlotsInUse(EXPEDITION) { Print("expeditions all in use") }

addr, err = ServerURL()          // TWO targets
Print(addr)                      // https://s123-fr.ogame.gameforge.com

Rank is your place in the highscore, Total the number of ranked players.

A record from memory, not a reading. GetCachedPlayer asks the game for nothing, and therein lies its value as well as its limit: it returns what the bot retained from the last overview page it opened, and from no other. The nickname and the ID do not move, the points and the rank may be several minutes old.

Zero means "the reading failed". GetSlots queries the game, but returns only one value: it has no way of signalling a failure to you. With no session, or if the game answers oddly, it returns a struct that is entirely zero and writes the reason to the bot's log. The same holds for GetCachedPlayer. Zero free slots out of zero reads as "full" to any test, IsAllSlotsInUse included, so that no script will go and launch a fleet on that. It is deliberate.

Reading a system

FunctionReturns
GalaxyInfos(galaxy, system)2: the reading and an errorThe fifteen positions of a system.
GalaxyInfosUsing(galaxy, system, celestial)2: the reading and an errorThe same, standing on a celestial.
GetPlanetInfo(coordinate)2: the position and an errorA single position.
GetPlanetInfoUsing(coordinate, celestial)2: the position and an errorThe same, standing on a celestial.
CoordinatesAvailableForDiscoveryFleet(origin, galaxy, system)2: the list and an errorThe positions a lifeform expedition can be sent to, empty ones included. Empty list when there is none.
sys, err = GalaxyInfos(1, 224)         // TWO targets
if err != nil { LogError(err); return }

for i = 1; i <= 15; i++ {
	p = sys.Position(i)
	if p == nil { continue }           // position entirely empty
	if p.Player.ID == 0 {
		// nobody: a lone debris field, or a destroyed planet
		Print(i, "unowned", p.Debris.Metal)
		continue
	}
	Print(p.Coordinate, p.Name, p.Player.Name, p.Activity)
}
// [P:1:224:8] Colony III Xylo 15

The reading now goes through the version 13 door, in the same shape as before. The game closed the page the library was querying: it answered "error in retrieve galaxy infos", and GalaxyInfos was the last of the bot's readings left behind. It now borrows the same reading as the workers, then translates it back into the original shape, field for field. Not a line of your existing scripts has to change, except that the fields version 13 no longer serves come back empty rather than as an error: see the pitfalls below.

What a position holds

FieldHolds
.ID, .NameThe planet. Empty on a position that holds nothing but a debris field.
.CoordinateWritten [P:1:224:8]. The type is always P, even when the position holds a moon.
.Activity0: nothing to report. 15: active right now. 16 to 59: the minutes elapsed, copied exactly as the game counts them.
.Player.ID, .Player.Name, .Player.RankThe owner and his place in the highscore.
.Inactive, .Vacation, .Banned, .Newbie, .StrongPlayer, .HonorableTarget, .AdministratorThe player's states.
.Player.IsBandit, .Player.IsStarlordThe two ends of the honour rank.
.Debris.Metal, .Debris.Crystal, .Debris.Deuterium, .Debris.RecyclersNeededThe debris field, with the number of recyclers the game announces.
.Moonnil, or .ID, .Diameter, .Activity. .Moon.Name exists and stays empty.
.Alliancenil, or .Tag.
.DestroyedExists, and is always false. See below.

The pitfalls of the reading

nil means "nothing at all", not "no planet". A position holding nothing but a debris field returns a perfectly real record, with empty player fields. Test p.Player.ID == 0 for "nobody", p == nil for "deserted position". One edge case: a field that would hold deuterium alone, with neither metal nor crystal, is set aside and its position comes back nil.

A destroyed planet keeps a name, and .Destroyed does not say so. The game attaches it to a system account instead of removing it. Kepler wipes the player, the rank, the alliance and every state, but .Player.Name may still carry the game's replacement name. As for .Destroyed, it exists in the returned shape and is never filled: it is false on a destroyed planet just as on the others. A player ID of zero is the only test that does not mislead.

An activity of zero is not an absence of activity. It is an absence of display, and the game displays nothing past an hour. A player gone for three days and a player gone for sixty-one minutes return the same zero.

The alliance has nothing left but its tag. Version 13 no longer gives the name, the rank or the member count on this page. .Alliance.Name, .Alliance.ID, .Alliance.Rank and .Alliance.Member therefore still exist, and return the empty string or zero instead of an error, which is the lesser of two evils.

Long inactivity is not carried over. The game's reading tells the inactive player from the long-inactive one, but the shape returned to your script carries only an .Inactive, copied from the short flag alone. To tell the two apart, read the Status that PlayersData returns and look there for the capital I.

The reading exposes only three things: Position(i), Galaxy() and System(). The other members of the original shape are out of reach: sys.ExpeditionDebris stops on no member named 'ExpeditionDebris' for struct, and sys.Events and sys.Relocations do the same. It is a clean error, at run time, naming the field. Expedition debris, which sits on position 16, is not in the reading anyway.

A reading queues behind the workers. It goes through the account's queue, at ordinary priority, but not through the bot's rate pacer: it is up to your script to space its calls out if it sweeps a hundred systems.

Standing on a celestial, and why it matters

c = GetCachedCelestial("1:2:3")
sys, err = GalaxyInfosUsing(4, 116, c.GetID())

The celestial does not change what the reading says: a system holds the same planets seen from anywhere. It matters as soon as you want to act from that reading.

The game ties the action to the current planet, and the token it hands out with the reading is valid for that one. Reading from one and sending from another ends in a refusal — which nothing in the reading announces.

A single position

p, err = GetPlanetInfo("4:116:6")
if err != nil {
	Print("nobody there:", err)      // no planet at [P:4:116:6]
} else {
	Print(p.Name, p.Player.Name, p.Activity)
}

GetPlanetInfo reads the whole system and hands you one position: it therefore costs exactly one request, the same as GalaxyInfos. If you want several in the same system, call GalaxyInfos once and walk its result.

An empty position returns an error, not a zeroed record. That is deliberate: an empty record would read as a nameless planet belonging to player number zero, and a script comparing names or ranks would see a real target.

Going through several systems

FunctionReturns
GetSystemsInRange(origin, radius)1: a list of systemsIn order of number.
GetSystemsInRangeAsc(origin, radius)1: a list of systemsNearest to farthest.
GetSystemsInRangeDesc(origin, radius)1: a list of systemsFarthest to nearest.
Print(GetSystemsInRange(3, 2))       // [1 2 3 4 5]
Print(GetSystemsInRangeAsc(3, 2))    // [3 2 4 1 5]
Print(GetSystemsInRangeDesc(3, 2))   // [5 1 4 2 3]

for s in GetSystemsInRangeAsc(224, 15) {
	sys, err = GalaxyInfos(1, s)
	if err != nil { LogWarn(s, err); continue }
	SleepRandMs(500, 1200)           // space them out, nothing will do it for you
}

These three functions ask the game for nothing: they compute. The galaxy wraps around, so a radius that overflows comes back from the other side, GetSystemsInRange(3, 3) starts at 499. The number of systems is your universe's; with no session, the computation falls back on 499 without saying so.

A difference with Ninja. Our Desc reverses the whole list, theirs reverses only the distances. Both return the same systems, farthest to nearest, but at equal distance the order of the two neighbours is swapped: we return [5 1 4 2 3] where their documentation announces [1 5 2 4 3]. Of no consequence for a sweep, awkward if you compare two logs.

The players

FunctionReturns
PlayersData()2: the list and an errorEvery player in the universe.
PlayerDataByID(id)2: a player and an errorClean error if the ID is unknown.
PlayerDataByName(name)2: a player and an errorWhole nickname, case is ignored.

What a player holds

FieldHolds
ID, NameThe ID and the nickname, as the game publishes them.
StatusThe game's string of letters. It is the only complete source.
AllianceThe ID of his alliance, 0 when he has none.
IsInactive()True if the status carries an i or an I: short inactivity as well as long.
IsVacation()True if the status carries a v.
IsBanned()True if the status carries a b.
DefensivePoints()His defence points, which the game does not publish. Rich record only.
OffensivePoints()His fleet, that is military minus defence. Rich record only.
players, err = PlayersData()          // TWO targets
if err != nil { LogError(err); return }

n = 0
for p in players {
	if p.IsInactive() { n++ }         // the parentheses matter, see below
}
Print(n, "inactive out of", len(players))

target, err = PlayerDataByName("Xylo")
if err != nil { Print("no such player") } else { Print(target.ID) }

The parentheses trap, and it is a silent one. IsInactive, IsVacation and IsBanned are functions here, checkboxes in Ninja. Written without parentheses, if p.IsInactive { ... } raises no error at all: the condition is simply always false, and your script finds zero inactive players in a universe that holds two thousand.

if p.IsInactive  { ... }   // ✗ never true, never reported
if p.IsInactive() { ... }  // ✓

The other field names differ too. Their short record carries IsAdmin, IsLongInactive, Vacation and AllianceID; ours carries Status and Alliance. A script copied over therefore stops on no member named 'Vacation' for struct, which at least shows up straight away. IsInactive is the only name common to both, and it is precisely the one that says nothing when written their way.

The full record, and what it costs. PlayerDataByID and PlayerDataByName return the rich record, with Ninja's names: PointsTotal, PointsEconomy, PointsResearch, PointsMilitary, PointsMilitaryBuilt, PointsMilitaryDestroyed, PointsMilitaryLost, PointsHonor, PointsLifeform, the nine matching Position..., MilitaryShips, Celestials and Alliance. PlayersData keeps the short record.

Alliance is nil when the player has none, and otherwise carries ID, Name and Tag. This is the only place in the bot that joins an alliance id to its tag: the version 13 galaxy reading only gives the tag.

Celestials carries the player's planets and their moons, each moon right after its own planet, with ID, Name and Coordinate. It is the only way to learn where someone lives without sweeping whole systems.

These two functions cost one request per player, kept for an hour, where PlayersData costs none. A script walking two thousand players should therefore start from PlayersData and ask for the full record only for the ones it keeps.

DefensivePoints() and OffensivePoints() split defence from fleet, which the game publishes nowhere: its military score mixes the two.

p, err = PlayerDataByID(id)
Print(p.DefensivePoints())        // his defence points
Print(p.OffensivePoints())        // his fleet, so what can actually fly

The calculation rests on one identity: defence is the only thing the game counts in full in both economy and military. The categories therefore overshoot the total by exactly the defence.

defence = economy + research + military + lifeform - total fleet = military - defence

Neither method exists in 1.82.239 or earlier: a bot that does not know them stops the script on the call.

The formula came from a client on 2026-09-22; Ninja's reference declares both functions and never gave the calculation. Checked on two accounts independently: on his, and here by pricing every single defence unit across the twenty celestials of an account. 168,476 points announced, 168,476 counted, to the unit.

Anti-ballistic missiles do count as defence, which was not obvious: they defend against no fleet, only against other missiles. The game's accounting puts them there anyway. Measured on a second account carrying 490 of them:

without the missiles: 2,695,268 -> off by 4,900 with the missiles: 2,700,168 -> exact

which is precisely their cost. Interplanetary missiles could not be measured: no account at hand has a single one. They sit on the same page and in the same silo, so they probably count too - but probably is not measured.

One caveat remains: a player whose file is a few hours old gives the figures of that moment, not of now. The game only regenerates it once a day.

DefensivePoints() never goes below zero: the integer score files round, and the subtraction can land at -1. The player file itself is fractional and does not trigger that case.

The military points are not in the order Ninja declares. Its list gives Built, Destroyed, Lost; the game orders types 4, 5 and 6 as lost, built, destroyed. Measured on forty players on 21/09/2026, through an identity that depends on no documentation: what a player has built equals what they still have plus what they have lost. The names are Ninja's, the values come from the right types.

The file is big, so it is kept for six hours. The game only regenerates it once a day. The first call downloads several hundred kilobytes, the following ones cost nothing, and every script on the account shares the same copy. A player who changes nickname can therefore stay known under the old one for six hours. PlayerDataByID and PlayerDataByName read that same copy; the second compares the whole nickname, not a fragment.

These readings go through the account's connection (so through its proxy), but not through its queue: they hold no worker back.

The highscore

FunctionReturns
HighscoreData(category, typ)2: the whole highscore and an errorThrough the game's API. IDs, not names.
GetHighscore(category, typ, page)2: a page and an errorNo longer works in version 13.

Use HighscoreData. GetHighscore read the highscore page, and version 13 rewrote it: it now stops on failed to find site. It stays defined so that a script coming from elsewhere does not hit an unknown symbol, but it will return an error on every call.

HighscoreData queries the API the game publishes to be read. It returns the whole highscore in a single call, where the page gave a hundred, and will not break at the next version. In exchange, it gives nothing but IDs: the name is taken from PlayersData. Like PlayersData, it borrows the account's connection without going through its queue.

ranks, err = HighscoreData(1, 0)      // players, general highscore
if err != nil { LogError(err); return }

players, err = PlayersData()
if err != nil { LogError(err); return }

names = {}
for p in players { names[p.ID] = p.Name }

for r in ranks {
	if r.Position <= 10 { Print(r.Position, names[r.ID], r.Score) }
}

Each entry carries ID, Position, Score and Ships, the last of these being filled only by the military highscores.

category is 1 for players and 2 for alliances. typ picks the highscore: 0 the total, 1 the economy, 2 research, 3 military, then 4, 5 and 6 for losses, constructions and destructions. Beyond that, Ninja's documentation contradicts itself: its list names 9 "economy" when its own example gives that name to 1. Kepler does not interpret them, it copies them as they are into the API address: a test call settles it in a second, a bet is paid for in hours.

An alliance highscore will return alliance IDs, which PlayersData cannot name: it knows players only.

Each category/type pair is kept for an hour, separately from the others, because the game only recomputes the ranks once an hour. Comparing the economy with the military therefore does not make one re-download the other.

Reading a game page as it comes

Two additions Ninja does not have. They are there to show what the game really sends, when a parsing error does not say how the page has changed.

FunctionReturns
PageContent(query)2: the text and an errorA game page, through the account's session.
FetchURL(url)2: the text and an errorAny address, through the same session.
HttpRequest(method, url, headers, body)3: the text, the code and an errorA full request, through the same session.
html, err = PageContent("page=ingame&component=overview")
if err != nil { LogError(err); return }
Print(len(html), "bytes")

regexp = import("regexp")
r = regexp.MustCompile(`var site = (\d+)`)
Print(r.FindStringSubmatch(html))

PageContent takes a string of parameters, not an address: what follows the ? in index.php. The request leaves through the account's session, so through its proxy, its cookies and its fingerprint, and through its queue.

An inexact page name holds the account up for several minutes. The game answers "An error has occured!", which the library takes for a disconnection: it reconnects and retries, ten times, doubling the wait up to a minute, and during that time neither the workers nor the screen get a turn. One spelling is corrected for you automatically, resourceSettings having become resourcesettings in version 13, and the correction can be read in the log. The others are yours.

FetchURL downloads a full address. ServerURL() gives you the root. FetchURL is capped at 4 MB, which is plenty for an interface file. Two things to know: it does not look at the response code, so that a server error page comes back to you as ordinary text, with no error; and a long read is interrupted if you stop the script.

HttpRequest is the same thing with the three pieces FetchURL lacks: the method, the headers and the response code.

headers = {}
headers["Authorization"] = "Bearer " + token
headers["Content-Type"] = "application/json"

payload, err = JsonEncode({"celestial": 33628068})
if err != nil { LogError(err); return }

body, code, err = HttpRequest("POST", "https://example.net/api", headers, payload)
if err != nil { LogError(err); return }       // ← THREE targets

if code == 429 {
    Sleep(60)                                 // too fast: we will retry
    return
}
if code == 401 {
    LogError("token refused")                 // no point retrying
    return
}
Print(code, len(body))

The code is what you were missing for a backoff ladder. A 429 is retried after a pause, a 401 will never succeed on a retry, and without the code the two look alike: FetchURL would hand you the error page as ordinary text.

Headers are written as an ordinary table, nil if you have none. An empty body is not sent. The method is free - GET, POST, PUT, DELETE, PATCH - and an empty method means GET.

The response code is never an error: a 500 comes back with err at nil and code at 500. err is only filled when the request could not leave or could not come back. Test both.

Like FetchURL, it goes through the account's session - same proxy, same cookies, same fingerprint - which lets it post to the game's own endpoints, which a bare GET cannot reach. Talking to a third-party service carries none of your session: the cookie jar only serves the domain being addressed. The response is capped at 4 MB, like FetchURL.

Writing to a player

FunctionReturns
SendMessage(playerID, message)1: an errorA private message, through the game's chat.
target, err = PlayerDataByName("Xylo")
if err != nil { LogError(err); return }

err = SendMessage(target.ID, "Hello, I come in peace.")
if err != nil { LogError("message refused:", err) }

It takes an ID, not a nickname: go through PlayerDataByName or through the .Player.ID of a galaxy reading.

Sending goes through the version 13 form, the very one the bot uses for its own alerts. The chat token is kept from one message to the next, and found again on its own if it is refused: a campaign of three hundred messages does not reload three hundred pages. The errors, for their part, are clean: they say in what shape the token was read, and the start of what the game answered.

SendMessageAlliance does not exist yet.

Reports and the message box

This is the raw material of a hand-written farming script: without it, a script sees targets through the galaxy but never what they carry.

FunctionReturns
GetEspionageReportMessages()2: the list and an errorThe summary of each report on the first page.
GetEspionageReportMessagesPages(pages)2: the list and an errorThe same, across several pages.
GetEspionageReport(msgID)2: the report and an errorThe detailed report of a message.
GetEspionageReportFor(coordinate)2: the report and an errorThe latest report on that position.
GetCombatReportSummaryFor(coordinate)2: the summary and an errorThe latest fight at that position.
DeleteMessage(msgID)1: an errorDeletes one message.
DeleteAllMessagesFromTab(tab)1: an errorEmpties a whole tab.
summaries, err = GetEspionageReportMessages()
if err != nil { LogError("box:", err) }
for m in summaries {
  Print(m.ID, m.Target, "loot", m.LootPercentage)
}

report, err = GetEspionageReportFor("1:2:3")
if err == nil {
  Print(report.Username, "inactive:", report.IsInactive)
  Print("metal:", report.Metal, "crystal:", report.Crystal)
}

detail, err = GetEspionageReport(summaries[0].ID)
Print(detail.Username, err)

fight, err = GetCombatReportSummaryFor("1:2:3")
if err == nil { Print("loot:", fight.Loot, fight.AttackerName) }

older, err = GetEspionageReportMessagesPages(3)
Print("over three pages:", len(older))

DeleteMessage(summaries[0].ID)
DeleteAllMessagesFromTab(20)

The summary carries ID, Target, Type, From and LootPercentage — not the resources: those are in the detailed report, which GetEspionageReport and GetEspionageReportFor return. That one also carries Username, IsInactive, LastActivity, and building levels when you sent enough probes to see them.

The tab numbers are the game's own: 20 espionage, 21 combat reports, 22 expeditions, 23 transport and unions, 24 the rest. Any other number is refused here rather than passed to the game, which would answer with its overview page, that is to say with nothing.

One page by default. GetEspionageReportMessages reads what a player sees when opening the box. Going through the whole history would cost one request per page without the script asking for it: that is what the variant taking a page count is for.

Counting your colonies

FunctionReturns
CountColonies()2: the count and the maximumWhat you have, what you may have.
count, possible = CountColonies()
Print(count, "colonies out of", possible)

No error is returned: the count is read from the bot's memory, it costs no request. With no session open, two zeros and a line in the log.