Redis Data Structure

Real-time game state storage in Redis.

Key Naming Convention

Key Pattern Type Description
ship:{ship_id} Hash Ship real-time state
station:{station_id} Hash Station real-time state
body:{name} Hash Celestial body state
game:tick String Current tick number
game:time String Current game time (ISO 8601)
game:total_spawns String Total spawn counter
game:paused String Pause state (“true”/”false”)
game:tick_rate String Current tick rate (ticks per second)
game:time_scale String Current time scale multiplier (1.0 = real-time)
game:time_sync String Time sync enabled (“true”/”false”)
game:time_drift String Current UTC drift in seconds (positive = game behind)
game:registration_open String Registration open (“true”/”false”, default open if absent)
maneuver:{ship_id} Hash Active maneuver state for a ship
automation:{ship_id}:rules Set Rule IDs for a ship
automation:{ship_id}:{rule_id} Hash Automation rule definition
connections:online Set Player IDs with active WebSocket connections
system:{system_id}:bodies Set Body names belonging to a star system
system:{system_id}:ships Set Ship UUIDs in a star system
system:{system_id}:stations Set Station UUIDs in a star system
system:{system_id}:jumpgates Set Jump gate UUIDs in a star system

Ship State Hash

Key: ship:{ship_id}

Field Type Description
player_id string Owner’s player UUID
name string Ship display name (editable, default = player username at spawn)
ship_class string Ship class identifier (“fast_frigate” or “cargo_hauler”), default “fast_frigate”
position JSON array [x, y, z] meters
velocity JSON array [x, y, z] m/s
attitude JSON array [w, x, y, z] quaternion
angular_velocity JSON array [x, y, z] rad/s
rotation_input JSON array [pitch, yaw, roll] -1.0 to 1.0, current player input
thrust_level string 0.0 to 1.0
attitude_hold string “true” or “false”, rate damping mode
attitude_mode string “none”, “hold”, “prograde”, “retrograde”, “normal”, “antinormal”, “radial”, “antiradial”, “target”, “target_retrograde”
attitude_target_w/x/y/z string Target quaternion components (when attitude_mode requires target)
attitude_target_id string Target entity ID (body name, ship_id, station_id, or lagrange key) for target modes
attitude_target_type string “body”, “ship”, “station”, or “lagrange”
fuel string kg remaining
fuel_capacity string kg maximum capacity
dry_mass string kg (mass without fuel)
wheel_momentum JSON array [x, y, z] N·m·s
system_id string Star system identifier (default “sol”)

Example:

HSET ship:550e8400-e29b-41d4-a716-446655440000 \
  player_id "660e8400-e29b-41d4-a716-446655440001" \
  name "PlayerOne" \
  ship_class "fast_frigate" \
  position "[1.5e11, 0, 0]" \
  velocity "[0, 29780, 0]" \
  attitude "[1, 0, 0, 0]" \
  angular_velocity "[0, 0, 0]" \
  rotation_input "[0, 0, 0]" \
  thrust_level "0.0" \
  attitude_hold "false" \
  fuel "30000" \
  wheel_momentum "[0, 0, 0]"

Celestial Body Hash

Key: body:{name} (e.g., body:Earth)

Field Type Description
position JSON array [x, y, z] meters
velocity JSON array [x, y, z] m/s
system_id string Star system identifier (default “sol”)

Static properties (mass, radius, type, color) are loaded from configuration, not stored in Redis.

Example:

HSET body:Earth \
  position "[-2.627e10, 1.445e11, -1.038e4]" \
  velocity "[-2.983e4, -5.220e3, 0]"

Station State Hash

Key: station:{station_id}

Field Type Description
station_id string Station UUID
name string Human-readable name
position_x string X position in meters
position_y string Y position in meters
position_z string Z position in meters
velocity_x string X velocity in m/s
velocity_y string Y velocity in m/s
velocity_z string Z velocity in m/s
attitude_w string Quaternion W (fixed, always 1)
attitude_x string Quaternion X (fixed, always 0)
attitude_y string Quaternion Y (fixed, always 0)
attitude_z string Quaternion Z (fixed, always 0)
mass string Mass in kg (default 420,000)
radius string Proximity envelope in meters (default 50)
parent_body string Reference body name (e.g., “Earth”)
system_id string Star system identifier (default “sol”)

Station position/velocity fields use individual keys (not JSON arrays) unlike ships. Attitude is fixed at identity and never updated.

Game State Keys

game:tick

Current tick number as a string.

SET game:tick "123456"
GET game:tick  # Returns "123456"

game:total_spawns

Total number of spawns ever (for spawn position calculation).

INCR game:total_spawns  # Atomically increment and return new value
GET game:total_spawns   # Returns current count

game:time

Current game time as ISO 8601 string. Updated each tick.

SET game:time "2000-01-01T13:00:00Z"
GET game:time  # Returns "2000-01-01T13:00:00Z"

game:paused

Whether tick processing is paused.

SET game:paused "true"
SET game:paused "false"

game:tick_rate

Current tick rate in ticks per second.

SET game:tick_rate "1.0"
GET game:tick_rate  # Returns "1.0"

Updated by tick-engine when admin changes tick rate. Used by api-gateway for welcome message.

game:time_scale

Current time scale (game speed multiplier).

SET game:time_scale "1.0"
GET game:time_scale  # Returns "1.0"

Updated by tick-engine when admin changes time scale. Used by api-gateway for welcome message. Default 1.0 (real-time). Range 0.1 to 100.0.

game:time_sync

Whether game time synchronization to wall-clock UTC is enabled.

SET game:time_sync "true"
GET game:time_sync  # Returns "true" or "false"

Default “true”. When enabled and admin time_scale ≈ 1.0, the tick engine adjusts effective time scale to converge game time to UTC.

game:time_drift

Current drift between game time and wall-clock UTC, in seconds. Positive means game is behind UTC.

SET game:time_drift "15.3"
GET game:time_drift  # Returns drift in seconds

Updated each tick by tick-engine. Used by admin dashboard to display sync status.

game:registration_open

Whether new player registrations are allowed. Absent or “true” means open; “false” means closed.

SET game:registration_open "false"
GET game:registration_open  # Returns "true", "false", or nil (treated as open)

Set by api-gateway admin endpoint. Checked during player registration.

Listing All Ships

To find all active ships, use SCAN (non-blocking cursor-based iteration):

SCAN 0 MATCH ship:* COUNT 100

Do not use KEYS ship:* — the KEYS command scans the entire keyspace and blocks the Redis server until complete. As the number of keys grows, this blocks all Redis operations for all services during the scan. Always use SCAN (or scan_iter() in the Python client) instead.

Listing All Bodies

SCAN 0 MATCH body:* COUNT 100

Listing All Stations

SCAN 0 MATCH station:* COUNT 100

System Index Sets

Per-system index sets enable efficient lookups of entities by star system.

Key: system:{system_id}:bodies (Set), system:{system_id}:ships (Set), system:{system_id}:stations (Set), system:{system_id}:jumpgates (Set)

Operations:

SADD system:sol:bodies "Earth" "Luna" "Mars"     # Add bodies to system
SMEMBERS system:sol:ships                         # List all ships in Sol
SREM system:sol:ships {ship_id}                   # Remove ship from system index

Maintained by physics service on entity create/delete/clear. Rebuilt by tick-engine on snapshot restore.

Online Status Tracking

Key: connections:online

Tracks which players have active WebSocket connections.

Operations:

SADD connections:online {player_id}     # Player connected
SREM connections:online {player_id}     # Player disconnected
SISMEMBER connections:online {player_id} # Check if online
SMEMBERS connections:online              # List all online players
SCARD connections:online                 # Count online players

Lifecycle:

Event Action
WebSocket connection established SADD after token validated
WebSocket close (graceful) SREM immediately
WebSocket close (abrupt) SREM when detected via ping timeout
Player deleted by admin SREM after WebSocket closed

Disconnection detection:

Method Timeout Action
Client ping 30 seconds Client sends ping
Server pong timeout 10 seconds Client should reconnect
Server-side heartbeat 60 seconds Server checks last message time
No activity timeout 60 seconds Server closes connection, SREM

The 60-second server-side heartbeat ensures stale connections are cleaned up even if the client crashes without closing the WebSocket.

Last activity tracking:

The api-gateway tracks last message timestamp per connection in-memory (not in Redis):

Data Storage Rationale
Last activity time api-gateway memory Per-connection, doesn’t need persistence
Online player set Redis (connections:online) Shared across api-gateway instances

On each incoming WebSocket message (including ping), api-gateway updates the in-memory timestamp for that connection. A background task checks all connections every 60 seconds and closes any with no activity.

If api-gateway restarts, all WebSocket connections are already closed (TCP termination), so in-memory tracking loss is not a problem—clients must reconnect anyway.

Fleet (Multi-Ship) Keys

player:{player_id}:active_ship

String key storing the UUID of the player’s currently active (piloted) ship.

SET player:{player_id}:active_ship "5995849b-..."
GET player:{player_id}:active_ship  # Returns ship UUID string

Set by players service on ship switch and as fallback during login. Read during authentication to restore the player’s last-piloted ship.

Important: The players service Redis client does not use decode_responses=True, so reads return bytes. All consumers must decode bytes to str before comparing with UUID strings.

player:{player_id}:ships

Set of ship UUIDs belonging to a player. Rebuilt from DB on each login.

SADD player:{player_id}:ships "ship-uuid-1" "ship-uuid-2"
SMEMBERS player:{player_id}:ships

Data Ownership

Data Owner Service Operations
ship:* physics Create, update, delete (api-gateway may write name field via ship rename)
station:* physics Create, update, delete
body:* physics Update (galaxy initializes)
game:tick tick-engine Read/write
game:time tick-engine Read/write
game:total_spawns physics Increment on spawn
game:paused tick-engine Read/write
game:tick_rate tick-engine Read/write
game:time_scale tick-engine Read/write
game:time_sync tick-engine Read/write
game:time_drift tick-engine Write (updated each tick)
maneuver:* tick-engine Create, update, delete (automation engine)
automation:* api-gateway (create/update/delete), tick-engine (evaluate/disable) Rule CRUD and evaluation
game:registration_open api-gateway Set by admin endpoint, read during registration
connections:online api-gateway Add/remove on connect/disconnect
player:*:active_ship players Set on login/switch, read during auth
player:*:ships players Rebuilt from DB on login
system:*:bodies physics Add on init/restore, clear on reset
system:*:ships physics Add on spawn, remove on delete, clear on reset
system:*:stations physics Add on spawn, remove on delete, clear on reset
system:*:jumpgates physics Add on spawn, remove on delete, clear on reset

Cross-Service Read Access

Some services require read-only access to data owned by other services:

Reader Data Purpose
players connections:online Populate online field in ListPlayers response
api-gateway player:*:active_ship Resolve active ship on WebSocket connect
api-gateway game:tick, game:time, game:paused, game:tick_rate, game:time_scale, game:time_sync, game:time_drift, game:registration_open Build welcome message, /api/status, registration gate
tick-engine All keys Create snapshots

Cross-service reads are acceptable for read-only access. Write operations must go through the owning service.

Persistence

Redis configured with AOF (Append Only File) for durability:

appendonly yes
appendfsync everysec

This provides durability with ~1 second of potential data loss on crash. Full state recovery uses PostgreSQL snapshots as the authoritative source.


Back to top

Galaxy — Kubernetes-based multiplayer space game

This site uses Just the Docs, a documentation theme for Jekyll.