Skip to content

Persistence (SessionManager & save/load)

Overview

Game state is persisted in localStorage. Two layers:

  1. SessionManager – session-based storage (one save per session UUID). Used by the current game flow.
  2. GameStateManager – legacy name-based storage (simu-game-save-<gameName>). Defines the GameState shape; loading into the map uses GameStatePersistence.

SessionManager

Path: src/client/ui/utils/SessionManager.ts

Storage keys

Key Content
current-session-id UUID of the active session
session-data-<uuid> Full session JSON
session-list Array of session UUIDs

Session storage format (what is written to localStorage)

The value under session-data-<uuid> is a JSON object with the following shape. It is kept compact to avoid huge payloads (e.g. 256×256 map, thousands of objects).

Field Stored as Description
sessionId, gameName, seed, mapWidth, mapHeight, timestamp as-is Session metadata
Map tiles mapCompact (not mapData) See below
objects array Non-colonist only (trees, resources, buildings, etc.)
colonists array Colonist objects only; not duplicated in objects
zones ZoneData[] Storage/sleeping zones
cameraX, cameraY, zoomLevel numbers Camera state

mapCompact (replaces 65k+ tile objects):

  • mapCompact: { w, h, types: string[], t: number[], e: number[] }
  • types – unique tile type names (e.g. ["deep_water","grass"])
  • t – type index per tile, row-major, length w*h
  • e – elevation per tile, row-major

Object/colonist entry (each item in objects or colonists):

  • Top level: type, x, y, uuid (no duplication).
  • data (optional): only extra fields from serialize() (e.g. quantity for resources, data: { name, age, skills, ... } for colonists). Base fields uuid, type, x, y are not repeated inside data.

Backward compatibility:

  • Old saves with mapData (no mapCompact) are expanded on load.
  • Old saves with all entities in objects (no separate colonists) are loaded from objects only when colonists is missing or empty.

Dump to file: Use SessionManager.getStoredSessionJson(sessionId) and write that string; it is already compact (one line). After load, loadSessionData() returns the same logical shape with mapData expanded and both objects and colonists populated for the rest of the app.

API (static)

Method Purpose
createSession(gameName, seed, mapWidth, mapHeight) Create session, set as current, return sessionId
getCurrentSessionId() / setCurrentSessionId(id) Current session
loadSessionData(sessionId) Load SessionData or null
saveSessionData(sessionData) Write full session
updateMapData(sessionId, mapData) Update only map tiles
updateObjects(sessionId, objects) Update only objects
updateColonists(sessionId, colonists) Update colonists subset
updateCamera(sessionId, x, y, zoom) Update camera
updateZones(sessionId, zones) Update zones
getAllSessionIds() / listSessions() List sessions (metadata)
hasSession(id) / deleteSession(id) Existence / delete
getStoredSessionJson(sessionId) Raw stored JSON string (compact; for dump to file)

ZoneData

id, type (e.g. storage, sleeping), start, end, tiles?, color?, name?.


Save flow

  1. IsometricMap triggers save via debounced debouncedSaveGameState() (e.g. on zone change, camera change, job cancel) – at most one run per 5 seconds.
  2. The debounce callback schedules the actual write with requestIdleCallback (or setTimeout(0) fallback) so the timer handler returns immediately and avoids long-task violations; the real save runs when the browser is idle or after a short timeout.
  3. When the scheduled save runs, GameStatePersistence.save():
  4. Drops carried objects at colonist position (haul state not persisted).
  5. Serializes all objects via object.serialize().
  6. Calls SessionManager.updateMapData, updateObjects, updateColonists, updateCamera, updateZones with current state.

So save is session-based and updates the current session in place.


Load flow

  1. New game: SessionManager.createSession(...) after map is generated; map + initial objects written to session.
  2. Load existing: GameScreen/IsometricMap gets currentSessionId and calls loadFromSession(currentSessionId).
  3. loadFromSession (IsometricMap):
  4. SessionManager.loadSessionData(sessionId)
  5. Set map dimensions, camera, zoom from session.
  6. Build Map<string, GeneratedTile> from sessionData.mapData, pass to MapView.
  7. loadSessionObjects(sessionData) – clear ObjectManager, for each sessionData.objects create object via ObjectFactory and add to ObjectManager.
  8. zoneManager.setZones(sessionData.zones ?? []), update MapView zones.

GameStatePersistence.loadGameState() is used when you already have a GameState object (same shape as SessionData for map/objects): it clears ObjectManager, sets map via callback, recreates objects from gameState.objects with ObjectFactory. Used if loading from a GameState source other than SessionManager (e.g. GameStateManager or tests).


GameStateManager

Path: src/client/ui/utils/GameStateManager.ts

  • GameState type: gameName, seed, mapWidth, mapHeight, mapData, objects, timestamp.
  • Storage key: simu-game-save-<sanitizedGameName>.
  • saveGameState(gameName, ...) – writes full GameState to localStorage.
  • loadGameState(gameName) – reads and returns GameState or null.
  • listSaves(), hasSave(), deleteSave() – list/check/delete by game name.

This is a game-name-based save API; the main flow uses SessionManager (session-ID-based) instead.


Debouncing

IsometricMap uses debouncedSaveGameState (e.g. 5 s) so frequent camera/zone changes don’t hammer localStorage. Manual save (e.g. menu) calls save() immediately.