Mode Service Architecture (Core)
Overview
This document describes the v6 mode architecture in the GridBuilding Core library.
ModeStateis a pure data container for application, UI, edit, view, and game modes.IModeServiceis the canonical entry point for all mode changes and queries.ModeServiceowns a singleModeStateinstance and raises strongly‑typedModeEvents.- Godot and other engine layers interact with modes only through services, not via direct
ModeStatemutation.
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:
ApplicationModeUIModeEditModeViewModeGameMode
- Provides:
CombinedModeStringfor 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:
ApplicationModeChangedEventUIModeChangedEventEditModeChangedEventViewModeChangedEventGameModeChangedEventMultipleModesChangedEventModeTransitionStartedEventModeTransitionCompletedEvent
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
ApplicationModeChangedUIModeChangedEditModeChangedViewModeChangedGameModeChangedMultipleModesChangedModeTransitionStartedModeTransitionCompleted
- State access
ModeState Current { get; }— live reference owned by the service.ModeState GetSnapshot()— deep copy viaModeState.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
ModeStateinstance. - Applies
SetModes(...)and derives per‑mode changes. - Raises the appropriate
ModeEvents:- Per‑mode events when the corresponding value changes.
MultipleModesChangedEventif any mode changed.ModeTransitionStartedEvent/ModeTransitionCompletedEventfor aggregated transitions.
- Logs transitions using the injected
ILogger(combined mode string before/after).
- Owns and updates a single
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():
ModeStateis created once and registered as a singleton.ModeServiceis created around that instance and registered asIModeService.SystemsContextis 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
IModeServicevia 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
IModeServiceonce and keep it as a dependency. - Subscribe to mode events and use
CurrentorGetSnapshot()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
ModeStatefields on Godot nodes. Injectable.Resolve<ModeState>()in Godot UI components.GBCompositionContainer.GetStates().Modein older systems.- Test‑only mock classes (
MockModeState,MockGBCompositionContainer) to simulate mode changes.
These approaches had several drawbacks:
- UI and systems tightly coupled to the
ModeStatedata 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
IModeServicefor reading and changing modes. - Resolve
IModeServicefrom theServiceRegistryin Godot, or inject it directly in core.
- Depend on
- Tests
- Either mock
IModeServicein unit tests, or - Construct a
ModeServicewith a testModeStateand assert on events/state.
- Either mock
Example Migration (High Level)
Before:
- A UI node holds a
ModeStatefield. - It subscribes directly to
ModeState.ModeChanged(Godot‑side test ModeState) or pulls state via a legacy container.
After:
- The UI node holds an
IModeServicereference. - It subscribes to
IModeService.ApplicationModeChangedorMultipleModesChanged. - It queries
modeService.Currentwhen it needs the latest state.
Anti‑Patterns (What Not To Do)
- Do not introduce new direct
ModeStatefields in Godot nodes. - Do not call
Injectable.Resolve<ModeState>()in new or migrated code. - Do not re‑introduce
GBCompositionContainer‑based access toModeState.
Instead:
- Always depend on
IModeServicefor mode orchestration. - Use the Enhanced Service Registry (
ServiceCompositionRoot+ServiceRegistry) as the DI entry point on the Godot side.