# Starmap
Documentation: next (source: develop).

Install the preview: `pnpm add @aephia/atlas-kit@next`. These docs follow develop and may include changes not yet published to npm.

> Star systems, planets, and asteroids — the places everything else happens in.

Markdown source of https://develop.atlas-kit-docs.pages.dev/guides/world/ — see https://develop.atlas-kit-docs.pages.dev/ai/ for the full machine-readable surface.

The world entry covers the map: star systems and the bodies inside them. It is
shared account state that fleets move through and starbases sit in. Even when
topology changes rarely, these are observed snapshots with account freshness
rules, not permanently valid definitions.

```ts
const system = await sage.systems.byId(systemId);
const bodies = await system.celestialBodies.all();
```

:::game[A star system is one point on the map]
The Galia Expanse is divided into regions, and each region contains several
star systems. The game records this galaxy on a flat map with two-dimensional
coordinates. Each star system occupies one point, and its star, planets,
asteroid belts, and starbase all belong to that same point even though the game
interface spreads them out visually. Being in the same region only means being
in the same broad area. A fleet is actually at a system when its coordinates
match that point, or when it is docked at the starbase. Two fleets that merely
look as if they are near the same system can still be at different coordinates
and therefore not be in the same place.
:::

## Systems by id or by address

Systems have a numeric id as well as an account address, and you can read by
either:

```ts
const byId = await sage.systems.byId(3);
const byAddress = await sage.systems.get(systemAddress);
```

The id is the friendlier handle when you already know which system you mean.
The address is what other accounts reference, so it is what you will have when
arriving from a fleet's state.

> **Runnable example — Read a star system.** Look a system up by its numeric id and list what orbits it. Run it in the browser at https://develop.atlas-kit-docs.pages.dev/guides/world/.

## Getting from a fleet to its system

A fleet's location lives inside its state, so narrow first:

```ts
if (fleet.state.kind === 'docked') {
  const system = await sage.systems.get(fleet.state.system.address);
}
```

See [fleets](/guides/fleets/) for why location depends on Fleet state instead of
being one flat field.

## Two-dimensional coordinates

C4 StarSystem coordinates are exactly two-dimensional (2D). The SDK exposes
`WorldCoordinates` as `{ x, y }` in game space, and
`distanceBetweenStarSystems` deliberately computes Euclidean distance using
only those two axes, not screen-space or three-dimensional distance.

In the pinned `@staratlas/dev-sage@0.52.0` binding, both
`getStarSystemEncoder` and `getStarSystemDecoder` fix the coordinate array to
`size: 2` using the I8F56 fixed-point codec. The generated TypeScript
`Array<I8F56>` type alone does not express that length; the codec establishes
the on-chain contract.

A consumer or editor export containing `z: 0` uses presentation-layer padding,
not a third on-chain coordinate. It does not justify adding `z` to
`WorldCoordinates` or changing distance semantics. If a future binding changes the codec,
re-audit the layout before changing this SDK contract; do not anticipate that
layout from presentation metadata.

## Warp lanes

Systems are not floating islands — the starmap is a network, and warp lanes
are its links. Every system lists its own:

```ts
const neighbours = system.connections.map((connection) => connection.systemId);
```

Each connection also carries stored base ATLAS toll inputs by starbase level
(`connection.costs.level1Atlas` through `level5Atlas`, plus the CSS tier).
These are inputs, not the final payable amount. `planFleetWarpLane` does not
choose the applicable endpoint level or calculate relationship and character
adjustments, their order, the payment source, or rounding. The program decides
the final toll.

:::game[Lanes connect systems; the program decides access]
Warp lanes are the third way a fleet travels, alongside subwarp and coordinate
warp — and the only one that jumps system to system. The lane network is also
what makes regions neighbours: two systems are adjacent because a lane
connects them. Connectivity is not permission to travel. When cross-faction
economics is disabled, cross-faction warp is foreclosed while same-faction warp
is unaffected. When it is enabled, the program still decides eligibility from
the current relationship and destination risk; the flag is not blanket
permission. The program calculates any ATLAS toll from the stored level costs
and applicable modifiers. Tested current PTR routes work with empty Fuel tanks.
The SDK does not estimate the final lane costs.
:::

Connectivity alone does not establish whether a fleet may travel. The pinned
[`warpLane` instruction](https://unpkg.com/@staratlas/dev-sage@0.55.0/src/generated/instructions/warpLane.ts)
states that disabling cross-faction economics forecloses cross-faction warp
while leaving same-faction warp unaffected. Enabling the flag delegates the
relationship and toll decision to the program; it does not guarantee every
cross-faction lane is eligible. The program also enforces the destination's
effective risk tier. A system's controller remains visible on its shared
starbase data:

```ts
const controller = system.starbase?.owner; // 'mud' | 'oni' | 'ustur' | …
```

Travelling a lane is a fleet move — `planFleetWarpLane` in
[Warp & subwarp](/guides/moving-a-fleet/) — with a program-calculated ATLAS toll.
Current PTR tests on two routes succeeded with empty Fuel tanks, contrary to the
Fuel-consumption description in the pinned
[`warpLane` IDL](https://unpkg.com/@staratlas/dev-sage@0.55.0/idl.json).
The confirmed Eternity–Sastri round trip also consumed no Fuel. These observations
do not establish every lane's cost rules. Character effects such as
[Council Rank](/guides/council-rank/) research can change the toll; its exact
arithmetic remains program-authoritative.

## Bodies: planets and asteroids

Celestial bodies are the things inside a system worth interacting with:

```ts
const planets = await system.planets.all();
const asteroids = await system.asteroids.all();
```

Both are projections over the same underlying body accounts, filtered to the
kind you asked for. Asteroids are where mining happens; planets are where claim
stakes go. A planet's `details.claimStakePlots` table is indexed by Starbase
level row and Claim Stake tier column; `getPlanetClaimStakePlots` turns it, the stakes placed in each level row and the system's Starbase level into per-tier availability. See [Claim stakes](/guides/claim-stakes/#reading-them).

:::note[There is no separate Star account]
C4 does not model stars as their own accounts. A system's star is part of the
system, not a body you can read independently. If you are porting logic from an
earlier generation of the game that expected one, that is why it is missing.
:::

## Star presentation metadata

**The inspected C4 client generates star visuals procedurally.** In the
[deployed game client](https://sage.staratlas.com/assets/index-yY8hE1Xh.js),
`getVisualStarCount` and `createStarVisualProfile` use system identity and
gameplay properties to seed visual generation, including visual companion
stars. This is evidence for the inspected rendering path, not a universal
statement about on-chain storage or a supported rendering contract.

Separately, the official map editor authors star `type`, display `name`, and
`scale` in exported JSON. The initial #368 audit checked that producer on
**2026-09-09**, using code rather than treating the reported 2026-06-09 export
as a chain schema. Editor authorship alone does not establish on-chain absence.

The editor's
[`STAR_TYPES` table](https://ses.staratlas.com/SAGE%20Map%20Editor/js/models.js)
explicitly lists 20 numeric IDs, **0–19**, with display labels. Its
[system operations](https://ses.staratlas.com/SAGE%20Map%20Editor/js/system-operations.js)
include a random generator that chooses a table entry, constructs a star name
from the system name and type label, and assigns a rounded random scale. The
editing controls can change the star's name, type, and scale. The
[import/export code](https://ses.staratlas.com/SAGE%20Map%20Editor/js/file-operations.js)
converts a legacy singular `star` into a `stars` array and exports the system
records in `mapData` as JSON.

This is **not a stable SDK input contract**: a separate `addStar` path in the
same producer still creates a string type (`"G"`) with `size` and `color` rather
than `scale`. Do not assume that every editor record follows the numeric table,
that a system always has exactly one visual star, or that scale is a physical
radius. The producer code establishes authorship, not uniform export validation.

The checked **`@staratlas/dev-sage@0.52.0`** `StarSystem` codec has Game and system
identity, a system name, region, coordinates, sequence metadata, connections,
body addresses and nested Starbase data, but no star visual fields.
**`StarSystem.name` names the system**, not a separately decoded visual star.
The pinned `CelestialBodyType` is `Planet | Asteroid`, and the generated
account/type audit found no separate Star account or owning star-visual field.
That **does not prove universal on-chain absence** or rule out future bindings.
Fresh reads of three StarSystem accounts and their 17 bodies consumed all bytes
and re-encoded identically. This checks those layouts; it cannot exclude
additional semantics in existing fields or data stored in other accounts.

Keep editor visuals in an **application-owned overlay** if your UI needs them.
Verify a **deployment-specific mapping** from exported systems to C4 system
identities, and manage the overlay's version and refresh separately from SDK
account caching. This audit establishes neither that mapping nor a supported
live metadata endpoint or schema version. The SDK does not fetch the editor
export or add these fields to `StarSystemSnapshot`. Any later proven on-chain
source needs a separate implementation issue with its owning codec,
Game/system relationship and captured or live account verification.

See the dated
[star provenance record](https://github.com/Aephia/atlas-kit/blob/develop/docs/research/star-visual-provenance.md)
for inspection anchors and source hashes. Body presentation and Region colors
remain separate research subjects.

## Body presentation metadata

**Base planet categories have an on-chain subtype representation.** The
`Planet.providedTags` set within the generated `CelestialBody` account contains
numeric tags that the
[deployed C4 game client](https://sage.staratlas.com/assets/index-yY8hE1Xh.js)
maps to planet categories through `PLANET_TYPE_TAG_NAMES` and
`getPlanetSubtypeLabel`. The `Planet | Asteroid` account variant is a separate
classification. Checking only field names in **`@staratlas/dev-sage@0.52.0`**
missed the meaning of these existing numeric values; the original #369
editor-only conclusion is superseded by this evidence.

The exact editor field remains distinct: its
[`PLANET_TYPES` table](https://ses.staratlas.com/SAGE%20Map%20Editor/js/models.js)
defines **0–31** faction/category IDs; the older 2026-06-09 export reported
0–23. The table repeats eight categories for ONI, MUD, USTUR, and Neutral.
The observed `type % 8` grouping is **not an SDK parsing contract**. We have not
verified a direct chain field or **deployment-specific mapping** for that full
editor ID; do not reconstruct it from planet subtype and current ownership.

For **`orbit`, `angle`, and `scale`**, two producers were inspected:

- The editor's
  [body creation code](https://ses.staratlas.com/SAGE%20Map%20Editor/js/system-operations.js)
  generates spacing, random angles and type-based scales; its
  [export code](https://ses.staratlas.com/SAGE%20Map%20Editor/js/file-operations.js)
  serializes those presentation values as JSON.
- The C4 client uses `getBodyOrbitIndex` to parse a `-P<number>` suffix in
  on-chain body names, with a fallback. Its inspected detail renderer computes
  orbital spacing from sorted body index and rendering constants. `createPlanet`
  uses seeded randomness and subtype-dependent sizes for visual angle and size.
  This is a frontend convention, not authoritative orbital data or a guaranteed
  body-name format. It does not demonstrate consumption of the editor export.

Raw PTR account checks found no unread suffix in the checked layouts. That
**does not prove that no other on-chain source exists** or exclude further
semantics in numeric fields. No editor dataset, orbital field, name-parsing
rule, or renderer is added to SDK snapshots.

The corrected
[chain audit](https://github.com/Aephia/atlas-kit/blob/develop/docs/research/world-metadata-chain-audit.md)
records the capture counts, fresh read slots, frontend source hash, and limits;
the [editor provenance record](https://github.com/Aephia/atlas-kit/blob/develop/docs/research/celestial-body-visual-provenance.md)
preserves the original producer evidence. Both source URLs and deployment
semantics can change.

## Planet subtypes and building your map

The SDK translates the verified category tags to `planet.details.subtype`:
`terrestrial`, `volcanic`, `barren`, `gas-giant`, `ice-giant`, `dark`, or
`oceanic`. The original numeric `providedTags` remain available for gameplay
rules and future interpretation.

Runnable with the `sage` client from [Your first read](/start-here/first-read/):

```ts
const bodies = await sage.celestialBodies.all();
for (const body of bodies) {
  if (body.kind === 'planet') {
    console.log(body.name, body.details.subtype ?? 'Unclassified');
  }
}
```

The value is `undefined` when category tags are absent, conflict, or include
category tag 3 (the frontend's Asteroid Belt entry, not a supported planet
subtype). Unrelated unknown tags are ignored. This conservative projection does
not guess from a display name or convert a Planet account to an Asteroid.

Subtype uses the **same body account cache** and freshness as other planet
fields. It requires no extra RPC request, Game definition load, or separate
catalog cache. This follows #398's account-projection approach; #372's Game
section registry is not involved. A future deployment may change tag semantics;
the mapping is pinned to the evidence recorded above, not guaranteed by the
raw u16 tag codec alone.

For an application map, use system coordinates for system positions, account
addresses for identity, and body names, subtypes, and resources for labels and
styling. Choose visual spacing, sizes, and animation in your application. The
SDK does not promise the current game's exact rendering or authoritative
per-body orbital parameters. If you use an editor export instead, verify its
deployment mapping and version that presentation dataset separately.

## Discovering bodies across the galaxy

For a starmap or resource survey covering every system, use the galaxy-wide
collection. This example runs with the `sage` client created in
[Your first read](/start-here/first-read/):

```ts
const bodies = await sage.celestialBodies.all();
console.log(bodies.length, bodies[0]?.name);
```

The functional equivalent uses a `ctx` context from `createSageContext` and
returns loaded snapshots without the root client's relation methods. See
[How it thinks](/start-here/how-it-thinks/) for the two API styles:

```ts
import { readMeta } from '@aephia/atlas-kit/client';
import { getAllCelestialBodies } from '@aephia/atlas-kit/world';

const bodies = await getAllCelestialBodies(ctx, { commitment: 'confirmed' });
console.log(bodies.length, readMeta(bodies));
```

This discovers Celestial Bodies belonging to the context's Game in one
operation, without first listing systems or requesting each system's bodies.
A configured indexer takes precedence; otherwise the SDK uses targeted RPC
discovery. Indexer candidates still require authoritative reads and validation,
so one discovery operation does **not** guarantee one network request for every
provider. `readMeta` reports the selected strategy and available observation
metadata. Reading the bodies does not load Game definition catalogs.

The result is one materialized, readonly array. The context's
`maxDiscoveryResults` ceiling defaults to 10,000 entries per RPC or indexer
response. An oversized response rejects with `RESOURCE_LIMIT_EXCEEDED`; it is
not silently truncated or split into pages. Raising this context option allows
a larger response and increases potential transfer, decoding, and memory costs.
Prefer `system.celestialBodies.all()` when you only need one system.

Galaxy-wide discovery and direct body reads share the same address-keyed cache.
Resource projections therefore use the same validated body snapshots and
freshness rules described below. Repeating discovery can still perform network
work; cached bodies are not a permanent catalog of galaxy membership. The
finder accepts read options such as `{ refresh: true }` and
`{ policy: 'no-store' }`, while choosing its own filters and discovery strategy.

## Resource values and freshness

Raw resource entries remain available inside each body snapshot:

```ts
const bodies = await system.celestialBodies.all();
for (const body of bodies) {
  if (body.kind === 'planet') {
    for (const resource of body.details.resources) {
      console.log(resource.cargoId, resource.richness.value);
    }
  } else if (body.kind === 'asteroid') {
    for (const resource of body.details.resources) {
      console.log(resource.cargoId, resource.amountMined, resource.miners);
    }
  }
}
```

Planets expose resource richness. Asteroids expose richness plus `amountMined`
and `miners`, which can change with mining activity. These raw projections do
not load Game configuration.

Planet richness gameplay use: unverified. The SDK decodes richness as observed
Planet account state, but version-matched program evidence does not establish a
consuming activity or formula. Do not apply the Asteroid richness behavior used
by Fleet mining or combine Planet richness with the separately decoded Claim
Stake building `resourceExtraction` rates.

When you need names, mints, categories, and storage costs instead of cargo ids,
use the resolved readers for each body kind:

```ts
import { getPlanetResources } from '@aephia/atlas-kit/world';
import { getResourceDeposit } from '@aephia/atlas-kit/mining';

const planet = await getPlanetResources(ctx, bodyAddress);
console.log(
  planet.resources.map(({ cargo, richness }) => ({
    name: cargo.name,
    richness: richness.value,
    richnessRaw: richness.raw,
  })),
);

const asteroid = await getResourceDeposit(ctx, depositAddress);
console.log(asteroid.resources[0]?.cargo.name);
```

`getPlanetResources` requires a canonical Planet account in the configured
Game. It resolves every cargo id through the shared definitions registry and
rejects the whole read when any id is unknown; it never drops a row or returns
a partially resolved snapshot. An empty Planet resource list is valid. Planet
richness is observed state, not an extraction rate or yield calculation.

Cargo definitions load lazily and are cached separately from the Planet
account. The returned snapshot preserves the Planet observation metadata, but
does not claim that the body and Game catalog were observed atomically. Plain
`getPlanets`, `getCelestialBody`, and galaxy discovery keep their raw resource
rows and do not unconditionally load Game definitions.

These projections reuse the body account's cache and observation. Use account
read options such as `{ refresh: true }` with `getPlanetResources` when you need
a new body observation. `{ policy: 'no-store' }` bypasses the completed body
cache without replacing it. The resolved result is attached to that body
observation rather than retained in a competing resource cache.
`systemSequenceId` identifies a system generation; it is not a verified revision
counter for every change to an individual body's mining state. A stable
sequence or apparently stable richness does not justify keeping the entire
body forever. See [Caching and provenance](/concepts/caching/).

## Starbases appear in two places

Shared starbase data — where it is, what level it is — belongs to the world. A
_player's own_ state at that starbase is separate and lives in
[starbases](/reference/starbases/).

```ts
const bases = await system.playerStarbases.all();
```

The split exists because the two have different lifetimes and different
readers: the starbase itself is shared infrastructure, while your cargo sitting
in it is yours.

The shared side is already on the loaded system as `system.starbase`: its
owner, level, hull and shield points, and the full level table. To read it as
one status with maximums, service availability and next-level upgrade progress,
use `getStarbaseStatus(ctx, systemAddress)` or derive it from the system you
hold:

```ts
const status =
  system.starbase === undefined
    ? undefined
    : deriveStarbaseStatus(system.starbase);
console.log(
  status?.level,
  status?.current.hullPoints,
  status?.upgrade?.toLevel,
);
```

[Starbases](/guides/starbases/#reading-the-shared-starbase) walks through the
fields.

## Gotchas

**`systems.all()` discovers systems, not every body in the galaxy.** It uses
configured discovery and can materialize a large result. Prefer known system
reads when they answer your question. Use `sage.celestialBodies.all()` for bodies
across the galaxy or `system.celestialBodies.all()` for one system. Definition
preload does not perform either discovery operation.

**Bodies are not evenly distributed.** A system may have no asteroids, or many.
Write for both.

**Coordinates are game-space, not screen-space.** StarSystem coordinates locate
systems in the galaxy; they do not supply a body’s editor `orbit` or `angle`.

## Discovering systems by region

Use `systems.all()` when you need discovery rather than one known system. Pass
`regionId` to ask the provider for only that region, or omit it to keep the
galaxy-wide behavior:

```ts
const allSystems = await sage.systems.all();
const regionalSystems = await sage.systems.all({ regionId: 3 });
```

The functional equivalent is `getStarSystems(ctx, { regionId: 3 })` from
`@aephia/atlas-kit/world`. Region ids are integers from 0 through 65535; invalid
values reject before discovery. The SDK always keeps the StarSystem
discriminator and configured-Game filters, and adds the tagged region filter so
region 0 remains distinct from a system with no region.

A configured indexer still takes precedence and returns address candidates
only. The SDK reads those accounts through the authoritative provider and
rejects a candidate whose current region no longer matches the request. This
finder does not load Game definitions or read Celestial Bodies.

## Reference

- [`world`](/reference/world/) — every export in this entry
- [`starbases`](/reference/starbases/) — player state at a starbase
- [`mining`](/reference/mining/) — what asteroids are for
