Teleport a Fleet into Orbit
This is the one where we play god a little.
Instead of flying a ship somewhere, we’re going to teleport it: pluck it up and set it down in a clean, exact orbit of our choosing. One vessel, or a tidy little formation of them strung out along the same orbit. Around Earth, around Luna, around whatever body you point it at.
And because we want to reuse this: different bodies, different heights, circular one day and a
stretched-out ellipse the next: we’ll wrap it in a proper command-line program with argparse, so
setting up a scenario is one line in the terminal.
Before you start
Section titled “Before you start”-
Reference Frames: we place the orbit in CCI, the parent body’s own frame. You just need the one idea from that page: CCI’s origin sits at the body’s center, and its X–Y plane is the body’s equator.
-
Basic gatOS I/O Toolkit: we import
read,read_scalar, andwrite_batchfromgatos_io.py. If you built the toolkit already, you’ve got them (and if you built it a while ago, notewrite_batchis a recent addition: grab the latest version of that file).
The big idea: an orbit is just a point and a push
Section titled “The big idea: an orbit is just a point and a push”Here’s the thing that makes this whole program short. To put a ship in an orbit, you don’t feed the game a list of orbital elements: “apoapsis this, periapsis that, inclination the other.” You hand it one thing: a state vector. That’s just where the ship is and how fast it’s going, right now: a position and a velocity, six numbers in total.
From those six numbers the game rebuilds the entire orbit. The whole ellipse: its size, its shape, where it’s high (apoapsis) and where it’s low (periapsis): all falls out of a single point and a single push. That’s Kepler’s insight, and it’s the trick everything below rests on: if we can pick a position and a velocity that belong to the orbit we want, the shape takes care of itself.
The teleport control file wants that state vector in CCI, the parent body’s frame:
/sim/debug/vessels/<id>/teleport←px py pz vx vy vz
(position in meters, velocity in m/s). So our real job is just: compute those six numbers.
Picking the six numbers
Section titled “Picking the six numbers”We get to choose where on the orbit to drop the ship, so we make life easy on ourselves. We lay the orbit flat in CCI’s X–Y plane: the body’s equator: with its lowest point (periapsis) sitting on the +X axis, going the normal way round (prograde). With that setup, the textbook orbit equations hand us the state vector directly, no rotating anything:
- How far out we are at an angle around the orbit ( is the true anomaly: fancy name for “how far past the low point we are”):
- Position: : flat in the equatorial plane.
- Velocity: where .
Two ingredients feed those: e, the eccentricity (0 for a circle; bigger stretches the far side
out), and p, the semi-latus rectum: a size number we get from our altitude.
Altitude means the low point
Section titled “Altitude means the low point”We treat --altitude as the height of the orbit’s lowest point (periapsis). It’s the most
intuitive anchor: “the closest I ever get to the surface is this high.” Two nice consequences:
- The low point is (body radius + altitude), which is always above the surface: so cranking up eccentricity can never accidentally bury your periapsis underground. It only pushes the far side higher.
- When , everything collapses into a plain circle at that altitude, and the eccentric formulas above become the circular ones automatically ( becomes , and becomes the circular speed ). One code path covers both cases: there’s no “if circular else ellipse” branch to write.
The size number is then just .
Spacing a formation: meters, not degrees
Section titled “Spacing a formation: meters, not degrees”The last flourish is --spread. You give it a repeatable --vessel, and each extra ship gets nudged
a few meters ahead along the orbit so they don’t all spawn on top of each other. But you think in
meters (“space them 25 m apart”), while the orbit math thinks in angle. The bridge is the
oldest trick in geometry:
arc length , so angle
So vessel i starts at your chosen true anomaly plus radians. Tiny distances over a multi-million-meter radius means a tiny angle: a gentle along-track shuffle, exactly what a formation wants.
The program
Section titled “The program”Save this as ~/tutorials/teleport-into-orbit.py, next to your toolkit modules:
#!/usr/bin/env python3# Teleport one or more vessels into a shared circular/eccentric orbit around a body.# e.g. python3 teleport-into-orbit.py --parent Earth --vessel Hunter --vessel Polaris --altitude 120000import argparse, math, sysfrom gatos_io import read, read_scalar, write_batch
ap = argparse.ArgumentParser(description="Place vessels into an orbit via debug teleport.")ap.add_argument("--parent", required=True, help="body they orbit, e.g. Earth (must already be their parent)")ap.add_argument("--vessel", required=True, action="append", metavar="ID", help="a vessel id; repeat for a formation")ap.add_argument("--altitude", required=True, type=float, help="periapsis (low-point) height above the surface, meters")ap.add_argument("--eccentricity", type=float, default=0.0, help="0 = circular (default); 0..1 stretches the far side out")ap.add_argument("--true-anomaly", type=float, default=0.0, help="where to start on the orbit, degrees (0 = the low point)")ap.add_argument("--spread", type=float, default=25.0, help="along-track spacing between vessels, meters (default 25)")args = ap.parse_args()
# Sanity-check the inputs before we touch the game.if not (0.0 <= args.eccentricity < 1.0): sys.exit(f"--eccentricity must be in [0, 1); got {args.eccentricity}")if args.altitude <= 0.0: sys.exit(f"--altitude must be positive; got {args.altitude}")
# The body's gravity and size come straight from /sim: no hard-coded constants.mu = read_scalar(f"/sim/bodies/{args.parent}/mu") # gravity parameter, m^3/s^2R = read_scalar(f"/sim/bodies/{args.parent}/radius") # surface radius, m
# Describe the orbit with two numbers: its low point, and how stretched it is.e = args.eccentricityr_pe = R + args.altitude # periapsis radius: --altitude is the LOW point, always above groundp = r_pe * (1.0 + e) # semi-latus rectum (size). For e=0 this is just r_pe.k = math.sqrt(mu / p) # speed constant √(mu/p); for e=0 it's the circular speed
# Radius at our starting angle: we space the formation in meters, so we need it to# convert meters-of-arc into radians (angle = meters / radius).th0 = math.radians(args.true_anomaly)r0 = p / (1.0 + e * math.cos(th0))
# One true anomaly -> one CCI state vector. Orbit lies in the equatorial (X-Y) plane,# low point on +X, going prograde (+Z is north). Textbook, no rotations needed.def state_at(th: float): r = p / (1.0 + e * math.cos(th)) # how far out we are at this angle px, py = r * math.cos(th), r * math.sin(th) # position in the equatorial plane vx, vy = -k * math.sin(th), k * (e + math.cos(th)) # velocity, prograde return (px, py, 0.0, vx, vy, 0.0) # px py pz vx vy vz
# --- PLAN: do all the thinking now -------------------------------------------# Work out every vessel's endpoint + state vector up front. Every read and every# calculation lives here, so the actuation step below can be pure writes.plan = [] # a list of (teleport_path, state_vector) pairs, one per vesselfor i, vessel in enumerate(args.vessel): # Teleport sets state about the vessel's CURRENT parent, so it must already be --parent. have = read(f"/sim/vessels/by-id/{vessel}/parent") if have != args.parent: sys.exit(f"{vessel} orbits '{have}', not '{args.parent}'. Teleport can't move a vessel between " f"bodies: get it into {args.parent}'s sphere of influence first.") th = th0 + (i * args.spread) / r0 # nudge vessel i ahead by i*spread meters plan.append((f"/sim/debug/vessels/{vessel}/teleport", state_at(th))) print(f"planned {vessel}: {i * args.spread:.0f} m along-track", file=sys.stderr)
# --- ACTUATE: fire the whole formation in ONE physics tick -------------------# write_batch sends every teleport as a single atomic group to /sim/ctl/batch,# so they all take effect in the SAME tick: no vessel drifts before the next# is placed. `plan` is already the (path, state) pairs write_batch wants.write_batch(plan)
shape = "circular" if e == 0.0 else f"e={e:g} elliptical"print(f"placed {len(plan)} vessel(s) in a {args.altitude:,.0f} m {shape} orbit of {args.parent}", file=sys.stderr)The program falls into two clearly separated halves, and that split is the whole point:
- Plan: read the two constants, check each vessel’s parent, and compute every state vector into
the
planlist. All the reads and all the math happen here, before a single ship moves. - Actuate: one
write_batch(plan)call that hands the whole formation to the game as a single group, applied together in one physics tick.
The heart of the math is still the tiny state_at function: four lines turning “how far around the
orbit” into the six numbers the game wants, with no special-casing for circular vs. eccentric (e = 0
just flows through and comes out a circle). But the structure around it: think everything through,
then place the fleet in one atomic stroke: is what makes the formation land cleanly, and here’s why
that matters.
Run it
Section titled “Run it”# see who's aroundls /sim/vessels/by-id
# one ship, plain 120 km circular orbit of Earthpython3 teleport-into-orbit.py --parent Earth --vessel Hunter --altitude 120000
# a stretched orbit: 120 km at the low point, e=0.3 bulging out the far sidepython3 teleport-into-orbit.py --parent Earth --vessel Hunter --altitude 120000 --eccentricity 0.3
# a three-ship formation, 50 m apart, starting a quarter way round the orbitpython3 teleport-into-orbit.py --parent Earth \ --vessel Hunter --vessel Polaris --vessel Kestrel \ --altitude 200000 --true-anomaly 90 --spread 50You’ll see the ships blink into place. Open the map view and there’s your orbit: a clean circle, or a tidy ellipse, exactly the size you asked for, with the formation strung out along it like beads on a wire.
What’s next
Section titled “What’s next”You can now stage a scenario: drop ships exactly where you want them and start from a known setup instead of flying everything up from the pad every time. That’s the perfect launchpad (pun intended) for the next rung: now that a formation exists, point them at each other, hold a relative position, or schedule a burn to nudge one onto a slightly different orbit: and watch the geometry you just built come to life.