Skip to content
Next — unreleased

Starmap

View as Markdown

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.

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

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

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.

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

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

See fleets for why location depends on Fleet state instead of being one flat field.

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

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.

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:

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

Travelling a lane is a fleet move — planFleetWarpLane in Warp & subwarp — and the fee a character actually pays can shrink with Council Rank research.

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

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.

The inspected C4 client generates star visuals procedurally. In the deployed game client, 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 explicitly lists 20 numeric IDs, 0–19, with display labels. Its system operations 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 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 for inspection anchors and source hashes. Body presentation and Region colors remain separate research subjects.

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 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 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 generates spacing, random angles and type-based scales; its export code 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 records the capture counts, fresh read slots, frontend source hash, and limits; the editor provenance record preserves the original producer evidence. Both source URLs and deployment semantics can change.

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:

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.

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:

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 for the two API styles:

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 entries live inside each body snapshot:

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

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.

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.

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.

  • world — every export in this entry
  • starbases — player state at a starbase
  • mining — what asteroids are for