Grid Placement

Mode Service Architecture (Core)

Overview

This document describes the v6 mode architecture in the GridBuilding Core library.

  • ModeState is a pure data container for application, UI, edit, view, and game modes.
  • IModeService is the canonical entry point for all mode changes and queries.
  • ModeService owns a single ModeState instance and raises strongly‑typed ModeEvents.
  • Godot and other engine layers interact with modes only through services, not via direct ModeState mutation.

This follows the same pattern as other core services:

  • Placement: IPlacementService
  • Manipulation: IManipulationService
  • Targeting: IGridTargetingService

Components

ModeState (Core/State/Mode/ModeState.cs)

  • Pure C# class implementing IState.
  • Holds current:
    • ApplicationMode
    • UIMode
    • EditMode
    • ViewMode
    • GameMode
  • Provides:
    • CombinedModeString for diagnostics.
    • Clone() for immutable snapshots.
    • SetModes(...) to update multiple modes at once.
  • No engine dependencies, no events, no DI logic.

Mode Events (Core/State/Mode/ModeEvents.cs)

Event types used by the service layer:

  • ApplicationModeChangedEvent
  • UIModeChangedEvent
  • EditModeChangedEvent
  • ViewModeChangedEvent
  • GameModeChangedEvent
  • MultipleModesChangedEvent
  • ModeTransitionStartedEvent
  • ModeTransitionCompletedEvent

All derive from ModeEvent : ServiceEvent and use the service name "ModeService" for diagnostics.

IModeService (Core/Services/Mode/IModeService.cs)

IModeService is the single abstraction other code depends on:

  • Events
    • ApplicationModeChanged
    • UIModeChanged
    • EditModeChanged
    • ViewModeChanged
    • GameModeChanged
    • MultipleModesChanged
    • ModeTransitionStarted
    • ModeTransitionCompleted
  • State access
    • ModeState Current { get; } — live reference owned by the service.
    • ModeState GetSnapshot() — deep copy via ModeState.Clone().
  • Commands
    • SetApplicationMode(ApplicationMode mode)
    • SetUIMode(UIMode mode)
    • SetEditMode(EditMode mode)
    • SetViewMode(ViewMode mode)
    • SetGameMode(GameMode mode)
    • SetModes(ApplicationMode? appMode = null, UIMode? uiMode = null, EditMode? editMode = null, ViewMode? viewMode = null, GameMode? gameMode = null)

Consumers must not mutate ModeState directly. They always go through IModeService commands.

ModeService (Core/Services/Mode/ModeService.cs)

Default implementation of IModeService:

  • Constructor:
    • ModeService(ModeState state, ILogger? logger = null)
  • Responsibilities:
    • Owns and updates a single ModeState instance.
    • Applies SetModes(...) and derives per‑mode changes.
    • Raises the appropriate ModeEvents:
      • Per‑mode events when the corresponding value changes.
      • MultipleModesChangedEvent if any mode changed.
      • ModeTransitionStartedEvent / ModeTransitionCompletedEvent for aggregated transitions.
    • Logs transitions using the injected ILogger (combined mode string before/after).

The implementation is engine‑agnostic and depends only on Core types.

Service Registration (ServiceCompositionRoot)

The Godot ServiceCompositionRoot registers the mode services as part of RegisterCoreServices():

  • ModeState is created once and registered as a singleton.
  • ModeService is created around that instance and registered as IModeService.
  • SystemsContext is also registered as a core shared singleton.

This makes IModeService and ModeState available through the shared ServiceRegistry to all Godot systems and UIs, while keeping the implementation in Core.

Usage Patterns

Core / Service Consumers

Other core services can depend on IModeService when they need to read or influence modes:

  • Inject IModeService via constructor.
  • Subscribe to events (e.g., ApplicationModeChanged) to react to mode changes.
  • Call SetModes(...) or the single‑mode helpers to request transitions.

Direct references to ModeState in core should typically be limited to ModeService and well‑defined adapter points.

Godot Systems and UI

Godot nodes should resolve IModeService from the ServiceRegistry rather than using legacy DI patterns:

  • Obtain the registry from ServiceCompositionRoot.
  • Resolve IModeService once and keep it as a dependency.
  • Subscribe to mode events and use Current or GetSnapshot() as needed.

Examples of systems which should migrate to IModeService:

  • TargetInformer (show/hide target UI based on mode).
  • GridTargetingSystem (clear targeting state on mode changes).
  • Placeable selection UIs (enter/exit build modes).

Migration Guide

Previous Patterns

Legacy code used several patterns to access mode state:

  • Direct ModeState fields on Godot nodes.
  • Injectable.Resolve<ModeState>() in Godot UI components.
  • GBCompositionContainer.GetStates().Mode in older systems.
  • Test‑only mock classes (MockModeState, MockGBCompositionContainer) to simulate mode changes.

These approaches had several drawbacks:

  • UI and systems tightly coupled to the ModeState data container.
  • No central place to coordinate cross‑service behavior when modes change.
  • Harder to test: each subsystem wired modes slightly differently.

New Pattern

All new code should follow this pattern:

  • Runtime
    • Depend on IModeService for reading and changing modes.
    • Resolve IModeService from the ServiceRegistry in Godot, or inject it directly in core.
  • Tests
    • Either mock IModeService in unit tests, or
    • Construct a ModeService with a test ModeState and assert on events/state.

Example Migration (High Level)

Before:

  • A UI node holds a ModeState field.
  • It subscribes directly to ModeState.ModeChanged (Godot‑side test ModeState) or pulls state via a legacy container.

After:

  • The UI node holds an IModeService reference.
  • It subscribes to IModeService.ApplicationModeChanged or MultipleModesChanged.
  • It queries modeService.Current when it needs the latest state.

Anti‑Patterns (What Not To Do)

  • Do not introduce new direct ModeState fields in Godot nodes.
  • Do not call Injectable.Resolve<ModeState>() in new or migrated code.
  • Do not re‑introduce GBCompositionContainer‑based access to ModeState.

Instead:

  • Always depend on IModeService for mode orchestration.
  • Use the Enhanced Service Registry (ServiceCompositionRoot + ServiceRegistry) as the DI entry point on the Godot side.