Skip to main content

Area lifecycle

Area lifecycle — two independent axes​

An Area instance has two independent state axes that must not be conflated:

AxisDriver
Loaded / UnloadedAny player inside (spectators count), any linked area loaded, or AreaForceActivateService
Activated / DeactivatedAt least one non-spectator player inside (AreaPlayerComputingService.countActiveCurrentPlayers > 0)

A spectator alone produces (Loaded, Deactivated): the Area instance stays in memory and components remain visible, but LaserTask / victory checks / scheduled actions do not run.

Orchestration​

Transitions are owned by AreaLifecycleService:

  • activateArea
  • deactivateArea
  • unloadAreaAndLinkedAreas

They are driven by AreaPeriodicUpdateController — a 10-tick AreaTask orchestrator running 7 services in a fixed order:

PLAYER_COMPUTING
→ EXIT_NOTIFICATION
→ LIFECYCLE
→ ENTRY_NOTIFICATION
→ PLAYER_COUNT_NOTIFICATION
→ VICTORY_CONDITION
→ LASER_AMBIENT_SOUND

The AreaTask cadence is the only reason most area-scoped state changes happen at all.

Cleanup discipline at unload​

Every singleton holding state keyed by areaId must be purged when an area unloads, otherwise the detached Area graph stays GC-pinned.

Two existing paths cover this:

  • AreaLifecycleService.unloadAreaAndLinkedAreas → cleanupAreaSingletons(area) — purges:
    • AreaComponentManager (entity-UUID → component map)
    • AreaPlayerComputingService (player tracking maps)
    • ComponentSyncService.unloadSyncsForArea for sync state
  • AreaResetController.reset(area) (called by deactivateArea, runs on every deactivation including spectator-only) — purges:
    • AreaVictoryManager
    • ComponentCounterUpdateService
    • TemporaryBlockService
    • PortalManager
    • the per-area part of BonusRegistry

Adding a new singleton with per-area state​

Adding a new singleton that keys state by areaId requires registering it in one of these two paths:

  • AreaResetController.OrderedStep — for state that should reset on deactivation.
  • cleanupAreaSingletons — for state that must survive the deactivated/loaded gap and only drop on unload.

If you skip this, the area graph leaks: components disappear from the world but their per-area state stays in memory until the JVM dies.