Per-System Physics Integration
Overview
Bodies are grouped by system_id during physics integration so each star
system is integrated independently. Ships, stations, and jump gates only
experience gravity from bodies in their own system. This prevents
gravitational cross-talk between systems that are light-years apart.
Currently only the Sol system exists, so these changes are transparent — behavior is identical to the flat-list approach.
Design
Physics service (simulation.py)
In process_tick(), bodies are grouped into a dict[str, list[CelestialBody]]
keyed by system_id (defaulting to "sol" when absent).
- N-body integration —
update_bodies_compute()is called once per system group. Energy and angular momentum are summed across systems. - Reference body snapshots —
ref_bodies(pre-update position snapshots) are built per system for attitude reference lookups. - Ship integration — Each ship’s
system_idselects its system’s bodies and ref_bodies for gravity computation and collision checks. - Station/jump gate integration — Same per-system scoping.
Tick-engine (automation_helpers.py)
_find_reference_body() accepts an optional system_id parameter. When
provided, bodies whose system_id field does not match are skipped during
both the Hill-sphere SOI search and the M/r² fallback.
Tick-engine (automation.py) — per-system body cache
evaluate_all_ships() fetches body positions per system rather than
globally. For each ship with automation rules, get_ship_system_id()
(a single hget on the ship hash) determines which system the ship
belongs to. Unique system IDs are collected and
get_system_body_positions() is called once per system. The resulting
dicts are cached in a system_bodies: dict[str, dict] map so that
ships in the same system share a single body-position dict.
This replaces the previous get_all_body_positions() call (global
SCAN body:*) which passed every body in the universe to every ship.
_build_context() and _evaluate_ship() pass the ship’s system_id
through to _find_reference_body().
Tick-engine (state.py) — per-system queries
Redis index sets (system:{id}:bodies, system:{id}:ships, etc.) written
by the physics service are consumed by new query methods:
get_ship_system_id(ship_id)— singlehgetfor a ship’ssystem_id.get_system_body_positions(system_id)— pipeline-fetch bodies in a system.get_system_ship_ids(system_id)— ship IDs in a system.get_system_station_ids(system_id)— station IDs in a system.get_system_jumpgate_ids(system_id)— jump gate IDs in a system.get_available_systems()— discover systems by scanningsystem:*:bodies.
Tick-engine (state.py) — tick.completed broadcast
publish_tick_completed() accepts an optional systems list. When provided,
the tick.completed event includes a comma-separated systems field listing
all active star systems.
Tick-engine (tick_loop.py) — system discovery
Available systems are discovered once during initialization via
get_available_systems() and cached in _available_systems. The list is
passed to publish_tick_completed() each tick.
Tick-engine (automation_helpers.py) — context enrichment
_build_context() includes _ship_system_id in the returned dict so that
maneuver modules can pass it to _find_reference_body() without re-reading
the ship hash.
Tick-engine maneuver modules — system-scoped reference body
maneuver_transfer.py and maneuver_approach.py extract _ship_system_id
from the context dict and pass it to _find_reference_body() at all call
sites. This ensures cross-SOI detection only considers bodies within the
ship’s star system.
Galaxy service (service.py)
_load_fallback_ephemeris() loads the primary ephemeris-j2000.json and
then scans for additional ephemeris-{system_id}-j2000.json files, merging
their bodies. Each body already carries a system_id field in the JSON.
API gateway (websocket.py) — enriched galaxy:systems response
The request_systems WebSocket handler calls ListSystems via gRPC, then
enriches each system with dynamic counts from Redis:
body_count:SCARD system:{id}:bodiesplayer_count:SCARD system:{id}:ships
Response shape:
{
"type": "galaxy:systems",
"systems": [
{
"id": "sol",
"name": "Sol",
"star_body": "Sun",
"position": {"x": 0, "y": 0, "z": 0},
"body_count": 12,
"player_count": 3
}
]
}
API gateway (websocket.py) — switch_system placeholder
The switch_system message handler returns an error until inter-system
travel mechanics are implemented (#736). Response:
{
"type": "error",
"code": "NOT_IMPLEMENTED",
"message": "Inter-system travel not yet implemented (#736)"
}
API gateway (admin.py) — enriched GET /api/admin/systems
Same enrichment as the WebSocket galaxy:systems response: each system
includes body_count and player_count from Redis index sets.
API gateway (admin.py) — POST /api/admin/ship/{ship_id}/system
Admin endpoint to move a ship between star systems (for testing multi-system infrastructure). Request body:
{"system_id": "alpha_centauri"}
Steps:
- Validate ship exists (
EXISTS ship:{ship_id}) - Read current
system_idfrom ship hash (default"sol") - Remove ship from old system index (
SREM system:{old}:ships) - Update ship hash
system_idfield (HSET ship:{ship_id} system_id) - Add ship to new system index (
SADD system:{new}:ships)
Returns {"success": true, "previous_system": "sol", "new_system": "alpha_centauri"}.
Invariants
- A body with no
system_idfield defaults to"sol". - A ship/station/jumpgate with no
system_iddefaults to"sol". - With only Sol bodies present, output is bit-identical to the previous flat-list integration.
- Cross-system gravity is never computed.