Fleets, flights and combat
What flies, what is heading for you, how long a trip will take, what a fleet can carry and what it is worth in combat.
Eighteen functions. Four query the game, fourteen compute from what the bot already holds in memory. That is the first thing to know, because a script that loops over a hundred targets makes a hundred requests or none at all, depending on which one it calls.
What costs a request, and what does not
| Function | What it costs |
|---|---|
GetFleets, GetAttacks, IsUnderAttack | one request per call |
FlightTime | one request per call, even on a target you have just run the numbers for |
| The other fourteen | nothing: they read the bot's memory |
The case of FlightTime is worth a close look. Before computing, it asks the game which empty or inactive systems are skipped along the route, and that question goes out on every call, on every universe, including those that skip nothing. In a loop over a hundred targets, that is a hundred requests.
for target in targets {
if Distance(origin, target) > 5000 {
continue // free, no request
}
// we only run the numbers on the finalists
secs, fuel = FlightTime(origin, target, HUNDRED_PERCENT, fleet, TRANSPORT)
if secs > 0 && secs < 3600 {
Print(target, ShortDur(secs), "deut", fuel)
}
}
One exception to know about. The calculation functions read three things from the bot's memory: your researches, your lifeform bonuses and your alliance class. If that memory is still empty, just after a reconnection, the first read goes and fetches them, and that can cost up to three pages. The hold calculations also read the fleet dispatch screen, one more page, when the bot has not seen it for a quarter of an hour. The following ones are free. A ShipCargo(LARGECARGO) at startup warms everything at once and gets the question out of your way.
Zero means "I could not read"
No function in this family returns an error. That is deliberate, and you need to know why: anko packs multiple return values into a slice, so an if IsUnderAttack() on a function that returned (bool, error) would be true on every pass, empty sky included. See the first pitfall.
When the read fails you therefore get the zero value: 0, an empty list, slots at zero. Nothing in your script tells it apart from a real zero, but it leaves a trace: the bot writes a Warn-level line to the log, api de flotte : lecture impossible, naming the function and the reason. If a number strikes you as absurd, that is where to look.
Two practical consequences:
FlightTimereturning0seconds does not announce an instant trip: no real flight lasts zero seconds. It is a failure, or an empty fleet.GetFleetsfailing returns slots that are all zero, for whichInUse < Totalis false andIsAllSlotsInUsereturns true. A careful script therefore refrains from sending, the right reaction when you know nothing.
What is flying, and what is incoming
| Function | Returns | |
|---|---|---|
GetFleets() | 2: the flights and the slots | Your fleets in the air, not the ones aimed at you. |
GetAttacks() | 1: a list | The hostile fleets on their way to your account. |
IsUnderAttack() | 1: a boolean | True if there is at least one. |
fleets, slots = GetFleets() // TWO targets
Print(len(fleets), "flights,", slots.InUse, "/", slots.Total)
for f in fleets {
if f.ReturnFlight { continue }
Print(f.Origin, "to", f.Destination, "in", f.ArriveIn, "s")
}
A flight carries Mission, Origin, Destination, Ships, Resources, ReturnFlight, InDeepSpace, StartTime, ArrivalTime, BackTime, ArriveIn and BackIn (in seconds), ID, UnionID, TargetPlanetID, and it answers IsCancellable(), which is true as long as the flight is neither a return, nor lost in deep space, nor a missile strike.
On a return flight, ArriveIn is -1. That is not a read failure, it is the game's convention: the countdown that remains is in BackIn. A script that sorts its flights by ArriveIn will therefore put all its returns first.
The slots carry InUse, Total, ExpInUse, ExpTotal, and they answer IsAllSlotsInUse(mission), which also looks at the expedition slots when the mission is one. GetSlots() returns them on their own, without the list of flights (see Account, galaxy, players and messages).
An attack, up close
An attack event carries ID, AttackerName, AttackerID, MissionType, Origin, Destination, DestinationName, ArrivalTime, ArriveIn, Missiles, UnionID and Ships.
Ships can be nil. The game only publishes the composition if you can see it; without that the field is not filled in at all. And when the game shows "?" for a ship type, the quantity is -1, not zero: a script that adds up without looking gets a total smaller than reality.
for a in GetAttacks() {
Print(a.AttackerName, "arriving at", a.DestinationName, "in", a.ArriveIn, "s")
if a.Ships == nil { Print(" composition unknown") }
}
IsUnderAttack makes exactly the same request as GetAttacks. Calling both in the same pass costs twice the price: prefer GetAttacks and test the length.
Difference from Ninja. These two functions do not go through the original reading, which answered "no attack" permanently on version 13 servers. They read the game's event list, the only one that tells the truth on those universes.
Distance and flight time
| Function | Returns | |
|---|---|---|
Distance(origin, target) | 1: an integer | The distance in the game's unit. No request. |
FlightTime(origin, target, speed, fleet, mission, [holdingTime]) | 2: the seconds and the deuterium | One request per call. |
Either coordinate can be written as you like: a string "1:2:3" (with the P:, M: or D: prefix if you want something other than a planet), a coordinate built with NewCoordinate, or a celestial straight away, whose coordinate is taken for you.
origin = GetCachedCelestials()[0]
Print(Distance(origin, "1:2:4")) // the celestial is enough, no need for .GetCoordinate()
Distance takes your universe into account: number of galaxies and systems, and ring wrapping if it is enabled. Two points at the same location return 5. Careful, the celestial type does not enter the calculation: a planet and its moon are at distance 5 from each other, just like the same coordinate twice.
Distance, on the other hand, does not take out the empty or inactive systems the game skips: only the game knows which ones they are. That is the whole price difference with FlightTime, which goes and asks.
The three arguments that mislead
The speed is not a percentage. Use the constants: HUNDRED_PERCENT is 10, FIFTY_PERCENT is 5, TEN_PERCENT is 1, FIVE_PERCENT is 0.5, in steps of five percent. Anything above 10 is brought back to HUNDRED_PERCENT without a warning: writing 50 meaning 50 % sends the fleet at full speed, and FlightTime computes that flight. A zero or negative speed is refused: FlightTime returns 0, 0 and the log says why.
The mission is not decorative: it picks the universe's fleet speed factor. ATTACK, GROUPEDATTACK, DESTROY, MISSILEATTACK, RECYCLEDEBRISFIELD and SPY take the war speed, all the others the peaceful speed. On a universe where the two differ, the wrong mission gives you the wrong duration.
The holding time, sixth argument, is in hours, and does not exist in Ninja's signature. It is accepted on top because the game charges the hold in deuterium: without it, the fuel announced for an expedition is the fuel of the trip alone, so too low. Omitted, it is zero. Only give it for EXPEDITION and PARKINTHATALLY: the supplement is added as soon as it is non-zero, whatever the mission, and on another mission the fuel would come out inflated by a cost the game does not charge.
fleet = NewShipsInfos()
fleet.Set(LARGECARGO, 20)
secs, fuel = FlightTime(origin, "1:2:4", HUNDRED_PERCENT, fleet, EXPEDITION, 2)
Print(ShortDur(secs), fuel) // ShortDur reads seconds
An empty fleet returns 0, 0, exactly like a failure, and still costs the request: the question goes out to the game before the calculation notices there is nothing to fly.
Finding where to send, to come home after a given hour
That is the question behind every hand-written fleet save: where do I send my ships so they come back after I wake up? Four functions answer it, one per kind of destination.
| Function | Returns | |
|---|---|---|
FindDebrisFieldWithMinimumTravelTime(origin, fleet, minimum) | 4 | A debris field. |
FindEmptyPlanetWithMinimumTravelTime(origin, fleet, minimum) | 4 | An empty slot. |
FindInactivePlanetWithMinimumTravelTime(origin, fleet, minimum, withMoon) | 4 | An inactive player. |
FindAbandonedPlanetWithMinimumTravelTime(origin, fleet, minimum, withMoon) | 4 | A destroyed planet. |
The four values are, in order: the destination, the flight time in seconds, the fuel, and an error. Count them: it is trap number two on the front page.
time = import("time")
p = GetCachedPlanets()[0]
fleet, err = p.GetShips()
dest, secs, deut, err = FindDebrisFieldWithMinimumTravelTime(p.GetCoordinate(),
fleet, 7 * time.Hour)
if err != nil {
LogWarn("no destination:", err)
} else {
Printf("%v, %.1f h of flight, %d deuterium", dest, secs / 3600, deut)
}
The minimum time is written either way
FindDebrisFieldWithMinimumTravelTime(o, f, 7 * time.Hour) // ✓
FindDebrisFieldWithMinimumTravelTime(o, f, 25200) // ✓ the same
The reference tool passes a Go duration; Kepler counts in seconds everywhere else. Both are accepted.
Anko does not keep the type of a multiplied duration, and that is worth knowing well beyond this page:
Printf("%T", time.Hour) // time.Duration
Printf("%T", 1 * time.Hour) // int64 ← the multiplication loses the type
The integer that comes out carries nanoseconds. These functions recognise it: beyond a year, a bare integer can no longer be seconds — nobody asks their fleet to stay out for a year — whereas in nanoseconds the shortest duration anyone writes, a minute, is already sixty billion. Nothing sensible falls between the two.
So you can copy a script from the reference tool without touching it. If you write the duration yourself, write it in seconds: that is the unit of this whole reference.
The time returned is always in seconds too, like FlightTime.
Everything is computed at the slowest speed
Ten per cent, that is, the longest possible flight. That is the reference tool's definition, and the only one that makes sense here: the point is to stay out for a long time.
Once the destination is found you leave at whatever speed you like — but a flight launched at 100% comes home ten times earlier than the figure returned.
No request to the game, and what follows from that
These four functions read only the galaxy readings already in the database, the ones the Scanner brought back. So you can call them in a loop without costing the account anything.
In exchange: what the Scanner has never seen does not exist for them.
FindDebrisFieldWithMinimumTravelTime: no position of that kind in the
readings
That error does not say the galaxy is empty: it says your Scanner has not been there yet. The other possible error is finer:
FindInactivePlanetWithMinimumTravelTime: nothing far enough in the
readings, the farthest is below the minimum
There, targets do exist, but none far enough to cover your night. Widen the Scanner's area, or lower the minimum.
What exactly comes back
The destination carries the right kind: DEBRIS for a debris field, PLANET for the other three. Pass it to NewFleet as it is, the kind follows.
Among the destinations that reach the minimum, the nearest is returned: it is the cheapest in fuel, and the one that comes home soonest after the hour you asked for.
The time is the library's computation, empty systems not skipped. It therefore slightly overestimates the real flight — which is the right direction for the error in a fleet save: you come home earlier than planned, never later.
What "abandoned" means
A planet destroyed by a death star. The game leaves it in the galaxy, with no player. The bot recognises it by exactly that: a planet name, and nobody on it.
withMoon keeps only the positions that carry a moon, for the two functions that accept it.
What a fleet carries, and how fast
| Function | Returns | |
|---|---|---|
Speed(fleet) | 1: an integer | The speed of the slowest ship, the one that dictates the flight. |
Cargo(fleet) | 1: an integer | The total cargo capacity, all resources together. |
Both accept the fleet by value or by pointer, which is not the case with Ninja: its examples write Speed(*s1), and Speed(s1) works just as well here. A nil pointer is read as an empty fleet, without a crash.
f = NewShipsInfos()
f.Set(LARGECARGO, 10)
Print(Speed(f), Cargo(f))
Speed returns zero on a fleet with no flying ship, a stock of solar satellites or crawlers for example. The library would return the largest integer there is, a number that would cross an entire flight calculation without being noticed: the guard is ours.
Your technologies, your player class, your lifeform bonuses and the universe settings are already in these numbers. So is the alliance class, but it weighs in only one case: only the Trader class counts, and only on the small and the large cargo, which it makes ten percent faster. Warrior and Researcher change nothing in these numbers, neither here nor in FlightTime.
A single ship
What one unit is worth on your account, all bonuses included.
| Function | Returns | |
|---|---|---|
ShipSpeed(id) | 1: an integer | Its speed. |
ShipFuel(id) | 1: an integer | Its reference consumption, the one that enters the fuel calculation. Never less than 1. |
ShipCargo(id) | 1: an integer | Its hold. |
Print(ShipCargo(LARGECARGO)) // 25000 at base, more with hyperspace
The hold is the one the game applies. The fleet dispatch screen publishes each ship's hold, bonuses included. ShipCargo, Cargo, a fleet's Cargo() method, CalcCargo, CalcFastCargo and CalcPreferredCargo return the smaller of the two, the computed one and the game's: a computation that saw bigger than the game would give too few transporters, and the game would refuse the dispatch (insufficient cargo capacity, code 140028). The dispatch itself follows the hold of the screen it has just loaded. The bot reads that screen again with every dispatch; right after startup, or when its last read is more than a quarter of an hour old, the first of these calculations loads it once, except in manual mode, where it goes without it.
Three of the game's thresholds are properly accounted for, and they surprise you when you compare them with the base value:
- Small cargo, impulse drive 5. It changes engine: its base speed goes from 5,000 to 10,000, and its consumption doubles.
- Recycler. At impulse 17 its consumption doubles, at hyperspace 15 it triples. The second threshold replaces the first, it does not add to it.
- General. It halves consumption, on all of your ships.
An espionage probe returns a hold of zero if your universe does not let probes raid. That is not a read error, it is the server setting.
An id that is not a ship (a defence, a building) returns 0 and a line in the log.
Putting a transport together
| Function | Returns | |
|---|---|---|
CalcCargo(total) | 2: large cargos, OR small ones | Two separate solutions, not the two halves of one fleet. |
CalcFastCargo(lcAvail, scAvail, total) | 3: large ones, small ones, and the cargo obtained | The composition your ships allow. |
res = NewResources(10000, 20000, 30000)
lc, sc = CalcCargo(res.Total())
Print("either " + lc + " large cargos, or " + sc + " small ones")
lc, sc, capacity = CalcFastCargo(10, 8, res.Total())
Print(lc, "large and", sc, "small, for", capacity, "of cargo")
The count is rounded up: one resource unit too many calls for one more ship.
"Fast" means as few large cargos as possible. The rule is "the smallest number of large ones such that the available small ones finish the load", and it is applied exactly as written, never consulting the real speed of your ships.
It assumes the small cargo flies faster than the large one, which is only true from impulse drive 5 on. Before that threshold it is the other way round: the large cargo goes exactly one and a half times faster than the small one, whatever your combustion level. On an account that does not have impulse 5 yet, the composition returned by CalcFastCargo is therefore the slower of the two, and CalcCargo is the one you want.
When your ships are not enough, CalcFastCargo does not refuse: it returns the whole stock, and the third number, smaller than the total asked for, is what tells you. Test it, otherwise you leave loot behind without knowing.
lc, sc, capacity = CalcFastCargo(lcAvail, scAvail, total)
if capacity < total { Print("that leaves", total - capacity, "behind") }
A zero or negative total returns 0, 0, 0, and negative availabilities are read as zero.
Beware of Ninja's examples: the ones announcing "0 and 9" then "1 and 4" for 60,000 resources assume a small cargo of 7,000, that is, a Collector with hyperspace 3. At the base hold the same rule returns "1 and 7" in both cases, and that is correct. Do not take those numbers for a test.
Combat strength
The three values of an engagement, for one unit, ship or defence alike.
| Function | Returns | |
|---|---|---|
AttackStrength(id, researches) | 1: an integer | Firepower, plus 10% of the base value per weapons level. |
ShieldPower(id, researches) | 1: an integer | Shield, plus 10% per shielding level. |
StructuralIntegrity(id, researches) | 1: an integer | Structure, plus 10% per armour level. |
The researches are an argument, and not yours by default: that is how you weigh a target's defence from an espionage report. Pass nil to use your own.
Print(AttackStrength(LIGHTFIGHTER, nil)) // with your researches
Print(AttackStrength(LIGHTFIGHTER, targetResearches)) // with theirs
With researches supplied, these three functions touch neither the game nor your account: they work even with no session open.
DBGetResearches does not exist here, and Ninja's examples call it. Pass nil, or GetResearch(), which returns your researches in one value and costs a request.
An id that is neither a ship nor a defence returns 0.
Prices and construction times
These three are not about fleets, but they are cut from the same cloth: local arithmetic, without a single request.
| Function | Returns | |
|---|---|---|
GetPrice(id, n) | 1: resources | n is a level for a building or a research, a number of units for a ship or a defence. |
ConstructionTime(id, n, facilities) | 1: a duration | Construction time, from the facilities supplied. |
TechnologyConstructionTime(id, level, lab) | 1: a duration | Time for a research, with the lab level given plainly. |
p = GetPrice(METALMINE, 20)
Print(p.Metal, p.Crystal, p.Deuterium, "total", p.Total())
GetPrice returns the price of THAT level, not the running total from level zero. Total() adds metal, crystal and deuterium; Value() weights them 1, 2 and 3.
Neither of them counts energy, which the price nonetheless carries in p.Energy. The extreme case is Graviton technology, which costs nothing but energy: GetPrice(GRAVITONTECHNOLOGY, 1).Total() returns 0. A script that sorts build jobs by cost will put it first, free of charge.
ConstructionTime takes the celestial's facilities: robotics factory and nanite factory for a building, shipyard and nanite factory for a ship or a defence, research lab for a research. There is no facilities constructor: you have to read them, with GetFacilities(celestial), which returns two values. A value that is not a set of facilities returns a zero duration and a line in the log.
fac, err = GetFacilities(celestial)
if err == nil {
Print(ShortDur(ConstructionTime(LARGECARGO, 20, fac)))
Print(ShortDur(TechnologyConstructionTime(ESPIONAGETECHNOLOGY, 10, fac.ResearchLab)))
}
CalculateIrnLabLevel, which Ninja uses to find the lab level to pass in, does not exist here: take fac.ResearchLab, and correct it yourself if you make use of the intergalactic research network.
Difference from Ninja. A research passed to ConstructionTime gives a duration there that differs from the one TechnologyConstructionTime gives, because the library applies only the universe's economy speed to it, whereas a research is also divided by the research speed. Here both functions take the same path and return the same number. If you are comparing with a script from elsewhere, that is where the gap is.
And to actually send
Nothing in this family sends a fleet: everything here serves to prepare and to check. Sending goes through a single function.
| Function | Returns | |
|---|---|---|
NewFleet() | 1: a builder | To be chained. nil with no session open. |
It returns a builder that you chain calls on, and whose SendNow() returns two values, the flight and an error.
f = NewFleet()
f.SetOrigin(origin)
f.SetDestination("1:2:4")
f.SetMission(TRANSPORT)
f.SetSpeed(HUNDRED_PERCENT)
f.AddShips(LARGECARGO, lc)
f.SetAllResources()
flight, err = f.SendNow() // TWO targets
if err != nil { LogError("send:", err) }
With no session open, NewFleet() returns nil and the script stops on the first method called, rather than letting you believe it sent something. The log says why.
Recalling automatically
f.SetRecallIn(seconds) before SendNow() recalls the fleet that many seconds after it leaves. The recall goes through the same checks as CancelFleet: it waits while you play by hand, it never reconnects an account that sleeps, and it queues with the workers. It still happens after your script has ended, so a script can send and stop. If the fleet lands before the recall could go out, the log says it was given up.
secs, fuel = f.FlightTime()
f.SetRecallIn(secs * 45 / 100) // turns around at 45% of the outbound flight
flight, err = f.SendNow()
The phalanx
| Function | Returns | |
|---|---|---|
Phalanx(moon, target) | 2: the flights and an error | Reads what flies around a position. |
It works from one of your moons carrying a sensor, and aims at a position within its range. Each call costs deuterium and the game enforces a delay between two scans: this is not a free read, it is an action.
moon = GetCachedCelestial("M:1:2:3")
flights, err = Phalanx(moon.GetID(), "1:2:8")
if err != nil {
LogError("phalanx:", err)
} else {
for f in flights {
Print(f.ID, f.Mission, f.Origin, f.Destination, f.ArrivalTime)
}
}
A refusal from the game arrives as an error, never as an empty list. That is this function's trap, and Kepler keeps it away from you: no planet at that position, not enough deuterium, no sensor on the moon — in the raw answer all of these look like a scan that saw nothing. You get the game's own wording, which names exactly what is missing.
An empty list with no error therefore means what it says: the position is quiet.
Each flight carries its own id. The library's extractor loses them, and without them every flight in the scan would share one number: a script filing them by id would keep only one. Kepler puts them back from the raw scan.
Two refusals worth knowing in advance
Measured in game on 05/09/2026, because neither can be guessed.
A scan too soon after the last one returns "Invalid parameter". The game enforces a delay of a few seconds between scans, and its refusal does not say so: it blames your arguments, which are not at fault. Three scans fired in a row give one success and two of these messages. Space them about four seconds apart — a SleepSec(4) between calls is enough.
Each scan costs deuterium on the moon, and when it runs out the game says so plainly: "Not enough deuterium available!". A loop sweeping a whole system therefore drains the moon, then fails on that message.
moon = GetCachedCelestial("M:3:193:8")
sys, err = GalaxyInfos(3, 193)
for i = 1; i <= 15; i++ {
p = sys.Position(i)
if p == nil { continue }
flights, err = Phalanx(moon.GetID(), p.Coordinate)
if err != nil { LogWarn(i, ":", err) } else { Print(i, len(flights), "flights") }
SleepSec(4)
}
Recalling a fleet
| Function | Returns | |
|---|---|---|
CancelFleet(fleet) | 1: an error | Turns a flight in progress around. |
A recall is not an instant return. The fleet turns around and takes exactly as long to come back as it has already flown: recalling halfway therefore costs as much as going all the way. The function returns as soon as the game has accepted the order, not when the fleet is home. A nil does not say "it is with you", it says "it is on its way back".
It accepts three spellings, because three are natural: the id as GetFleets returns it, the whole flight record, or the bare integer you may have kept in the script's storage.
flights, slots = GetFleets()
for flight in flights {
err = CancelFleet(flight.ID)
if err != nil { LogError("recall:", err) }
}
A fleet already home, or already returning, makes the game return an error. That is not a script failure: it is an order that no longer had an object.
The jump gate
| Function | Returns | |
|---|---|---|
JumpGate(origin, destination, ships) | 1: an error | Jumps ships from one moon to another. |
JumpGateDestinations(origin) | 3: the moons, the recharge, an error | What can be reached, and in how long. |
JumpGate2(origin, destination, ships) | 3: success, the recharge, an error | The same jump, with the delay as a number. |
Both want moons. A planet passed in their place makes them return an error that names it, rather than the game's silent refusal.
The gate recharges after each jump. JumpGate on a gate that is not ready does not return a silent false: it returns an error stating the time left, the only thing a script can act on.
To wait for the gate, use JumpGate2. It performs exactly the same gesture, but returns the recharge in seconds, as a number: a script that wants to sleep until the next window does not have to read a sentence to find a figure in it. There the recharge is not an error — it is the answer.
sent, recharge, err = JumpGate2(moon.GetID(), destinations[0], ships)
if err != nil { LogError("jump:", err); return }
if !sent {
LogInfo("gate busy, coming back in", ShortDur(recharge * 1000000000))
SleepSec(recharge)
}
moon = GetCachedCelestial("M:1:2:3")
destinations, recharge, err = JumpGateDestinations(moon.GetID())
if err != nil { LogError("gate:", err) }
Print("reachable:", len(destinations), "recharge:", recharge)
ships = NewShipsInfos()
ships.Set(LARGECARGO, 100)
err = JumpGate(moon.GetID(), destinations[0], ships)
if err != nil { LogError("jump:", err) }
The jump uses no fleet slot and costs no deuterium: that is what makes it useful to move a mobile defence or empty a threatened moon. In exchange it carries ships only, never resources.
The Lifeform Expedition, one position at a time
| Function | Returns | |
|---|---|---|
SendDiscoveryFleet(origin, destination) | 1: an error | One expedition to ONE position. |
Not to be confused with the Discovery worker, which covers a whole system in one click and handles the slot reserve itself. This one aims at a single position and counts nothing: it is up to your script to check there is room left. GetSlots and GetFleetSlotsReserved are there for that.
slots = GetSlots()
if slots.Total - slots.InUse <= GetFleetSlotsReserved() {
return // leaving room for the Defender
}
err = SendDiscoveryFleet("1:64:8", "1:64:12")
if err != nil { LogError("discovery:", err) }
The destination is given however you have it at hand. A string "1:64:12", a coordinate, one of your celestials or its ID, or any list element that carries a Coordinate field: a position from GalaxyInfos or GetPlanetInfo, a planet from PlayerDataByID. A report summary from GetEspionageReportMessages is accepted too, for its Target. The most common case is an element of the list returned by CoordinatesAvailableForDiscoveryFleet: it is already a coordinate, pass it as is. el.Coordinate does not exist on it and stops the script.
origin = GetCachedPlanets()[0]
coords, err = CoordinatesAvailableForDiscoveryFleet(origin, 1, 64)
if err != nil { LogError("galaxy:", err); return }
for el in coords {
Print(SendDiscoveryFleet(origin, el)) // <nil>: sent
SleepRandSec(20, 60)
}
An empty position from GalaxyInfos is nil. sys.Position(i) returns nil on an empty slot, which is exactly where a discovery often goes, and the function refuses nil with a sentence saying so. Pass NewCoordinate(g, s, i, PLANET_TYPE) instead, or better, loop over CoordinatesAvailableForDiscoveryFleet, which lists only open positions, empty ones included.
A fleet is not a destination: it carries an origin AND a destination, and it is up to you to say which one (f.Destination). Nor is a combat report, for the same reason. Anything that carries no coordinate is refused with a sentence naming the type received, and no request leaves; so is a position outside 1 to 15.
What Print shows of it. The function returns an error and nothing else: nil when the game accepted the send, which Print writes as <nil>, as in the reference tool. A refusal prints with the game's own sentence, preceded by discovery impossible:.
Two requests per call. The function first reads the system from the origin, then sends: the game only hands out the send token with that reading, and accepts it once. A position the reading says is resting is refused without a send. In a loop, space the calls as above: fifteen departures to the second look like no player.
What it really costs. None of your ships leave, but the send takes a fleet slot and resources from the origin celestial. It also requires the Envoys technology, and the game enforces a per-position cooldown: a position visited recently refuses the next one.
The refusal comes from the game and names itself. A script sweeping a whole system should expect some positions to refuse, and that is not a failure — it is the cooldown running.
The flight backwards: starting from the arrival time
| Function | Returns | |
|---|---|---|
GetDepartureTime(arrival, origin, destination, ships, speed, mission) | 1: a time | When to leave to arrive at the stated time. |
LowestSpeed(arrival, origin, destination, ships) | 4: the speed, the fuel, the departure time, an error | The cheapest speed that can still leave. |
Why this direction matters. A fleet save is thought of by its end: "I want everything home by eight". The forward direction — I leave now, I arrive when — forces the script to guess, and to guess with requests, since every FlightTime costs one.
ships = NewShipsInfos()
ships.Set(LARGECARGO, 10)
departure = GetDepartureTime("08:00:00", "1:64:8", "1:64:12", ships, TEN_PERCENT, TRANSPORT)
<-ExecAtCh(departure, func() {
LogInfo("time to leave")
})
LowestSpeed looks for the SLOWEST that can still leave, which is the question actually being asked. The slower you go, the earlier you must take off: at 5 %, departure has usually already passed. The useful speed is therefore the cheapest in deuterium among those you can still take. If even 100 % leaves too late, the wanted arrival is out of reach, and the error says so — rather than returning a speed that cannot be held.
speed, deut, departure, err = LowestSpeed("08:00:00", "1:64:8", "1:64:12", ships)
if err != nil { LogError(err); return }
Printf("leave at %s at %d%%, %d deuterium", departure.Format("15:04"), speed * 10, deut)
A departure exactly now does not count. By the time you compose the fleet and click, the second has passed: the next speed up is kept, and you arrive on time.
What it costs. GetDepartureTime makes one request, the flight computation. LowestSpeed makes six: one for the upper bound, then at most five for the search. Flight time falls as speed rises, so the answer is found by binary search over the account's speeds (twenty for a General, the ten whole steps for the other classes) rather than by sweeping them — measured, five tries are enough in the worst case. Six requests is not something to put in a loop over a hundred targets: it is meant to be called once, when planning.
The arrival time is written either way: "08:00:00" names the next time it will be that hour, like ParseNextDatetimeAt, or a date computed with time.
Handing the watch to the bot
| Function | Returns | |
|---|---|---|
StartPhalanxSession(moon, target, interval) | 2: an id and an error | Sets up the watch on a position and turns it on. |
StopPhalanxSession(id) | 1: an error | Turns it off. |
Not to be confused with Phalanx, which scans once and returns what it sees. These hand the watch to the bot: it scans at the given interval, until stopped, and the fleets it finds go to the alerts like everything else.
moon = GetCachedCelestial("M:1:2:3")
id, err = StartPhalanxSession(moon.GetID(), "1:2:8", 300) // every 5 min
if err != nil { LogError("phalanx:", err); return }
SleepMin(60)
StopPhalanxSession(id)
The interval is in seconds, and every scan costs five thousand deuterium. Too short an interval drains a moon in a night without learning anything more: the game does not refresh any faster.
A difference from Ninja. There, several watches run at once and each has its own id. Kepler holds only one — one moon, one target, like the panel — and therefore always returns 1. StopPhalanxSession stops the one that is running, whatever id you pass it. A copied script runs, but it will watch one position at a time.
The setting survives being turned off: turning it back on from the panel does not mean retyping the moon and the target.
Cargo, favouring one type
| Function | Returns | |
|---|---|---|
CalcPreferredCargo(preferred, probes, pathfinders, large, small, total, roundUp) | 5: probes, pathfinders, large, small, the cargo | Composes a transport serving the wanted type first. |
Four holds, where CalcFastCargo knows only two. The espionage probe — when the server allows probe raiding — and the pathfinder join the large and small cargo. On a universe that allows them, a probe carries for a fraction of a transporter's deuterium; ignoring them is expensive.
probes, pathfinders, lc, sc, cargo = CalcPreferredCargo(LARGECARGO, 0, 0, 50, 20, 5000000, true)
Printf("%d LC + %d SC carry %d", lc, sc, cargo)
The preferred type is served first, up to what is available; the others fill in, largest to smallest — the goal is the fewest ships, not the most. A preferred type with an empty stock is not an error: the rest gets loaded.
Rounding decides the last ship. True, and a remainder of one unit of resource asks for one more ship and the whole load leaves; false, and that remainder stays behind. A cargo smaller than the load asked for therefore means one or the other: the fleet was not enough, or rounding was off.
The joint attack
| Function | Returns | |
|---|---|---|
CreateUnion(fleet, players) | 2: the union number and an error | Opens a joint attack around a fleet. |
Order matters, and that is the trap. The fleet must have left before anyone can be invited: you send your own fleet on a joint attack first, read back its id, and only then open the union. The number returned is what the guests pass to their own send.
Names are players' names as the game writes them. A mistyped name is not refused here: the game will say so, and only it knows who exists.