# 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.

## 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 the lane's toll — an ATLAS cost per starbase
level (`connection.costs.level1Atlas` through `level5Atlas`, plus the CSS
tier) — so "what does this jump cost" is a read, not a guess.

:::game[Lanes are the faction highway]
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. A lane only works when both ends are held by the same faction,
and using one costs an ATLAS toll that depends on the starbase level at the
gate.
:::

Whether a lane is usable is a faction question: both ends must be held by the
same faction, and a system's controller is 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/) — and the fee a character actually
pays can shrink with [Council Rank](/guides/council-rank/) research.

## 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.

:::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

Resource entries live 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. Resolving a `cargoId` to a
cargo definition is a separate [catalog read](/concepts/definitions/) that may
load Game configuration.

These projections reuse the body account's cache and observation. Use account
read options such as `{ refresh: true }` when you need a new observation.
`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.

## 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`.

## Reference

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