Persistence (SessionManager & save/load)¶
Overview¶
Game state is persisted in localStorage. Two layers:
- SessionManager – session-based storage (one save per session UUID). Used by the current game flow.
- 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, lengthw*he– 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 fromserialize()(e.g.quantityfor resources,data: { name, age, skills, ... }for colonists). Base fieldsuuid,type,x,yare not repeated insidedata.
Backward compatibility:
- Old saves with
mapData(nomapCompact) are expanded on load. - Old saves with all entities in
objects(no separatecolonists) are loaded fromobjectsonly whencolonistsis 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¶
- IsometricMap triggers save via debounced
debouncedSaveGameState()(e.g. on zone change, camera change, job cancel) – at most one run per 5 seconds. - 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. - When the scheduled save runs, GameStatePersistence.save():
- Drops carried objects at colonist position (haul state not persisted).
- Serializes all objects via
object.serialize(). - Calls
SessionManager.updateMapData,updateObjects,updateColonists,updateCamera,updateZoneswith current state.
So save is session-based and updates the current session in place.
Load flow¶
- New game:
SessionManager.createSession(...)after map is generated; map + initial objects written to session. - Load existing: GameScreen/IsometricMap gets
currentSessionIdand calls loadFromSession(currentSessionId). - loadFromSession (IsometricMap):
SessionManager.loadSessionData(sessionId)- Set map dimensions, camera, zoom from session.
- Build
Map<string, GeneratedTile>fromsessionData.mapData, pass to MapView. loadSessionObjects(sessionData)– clear ObjectManager, for eachsessionData.objectscreate object via ObjectFactory and add to ObjectManager.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.