Grid Placement

User Scope Roots (C#)

User Scope Roots (C#)

This guide defines the C# foundation for integrating GridPlacement 6.0 into a game that may already have its own “user scope” or “user scope” orchestration.

Problem statement

GridPlacement needs:

  • A per-user service scope (services and state that belong to one user/user).
  • A per-user input provider (the single place that consumes raw engine input and routes it into Core).

Final games often already have:

  • One UserScopeRoot (or similar) that owns service wiring for all systems.

So GridPlacement must support:

  • A standalone scope root for quick-start demos.
  • An adapter mode that plugs into a game’s existing scope root.

Canonical concepts

User identity policy (6.0)

  • Canonical UserId for 6.0 integration is GameUserSessions.Core.UserId.
  • Any internal GridPlacement/Core-only identity wrapper (e.g. GBUserId) is an internal implementation detail and must not leak into public APIs or engine adapters.

GBUserScopeRoot (plugin-provided)

A Godot C# node that acts as the scope origin for GridPlacement.

This is the single required node per user/context for 6.0 C# integration.

Responsibilities:

  • Create or receive a ServiceRegistry for this user.
  • Register GridPlacement services into that registry.
  • Own the single Godot input capture point (_UnhandledInput) for this user.
  • Ensure exactly one Core input pipeline exists for this user.

Tested behavior contract (enforced by GoDotTest in the 6.0 demo harness):

  • Configure(ServiceRegistry registry)
    • must throw when registry is null.
    • must throw if called more than once.
    • when configured, must use the injected registry instance as GBUserScopeRoot.Registry.
  • IUserScope registration
    • if no IUserScope is registered, must register exactly one.
    • if an IUserScope is already registered, must not overwrite it.
  • IUserScopeProfileProvider usage
    • if present, must use it to select UserScopeProfile.
    • if UserScopeProfile.UserId is empty, must generate a new non-empty user id.
    • must prefer UserScopeProfile.Name when provided (fall back to UserScopeName).

Non-responsibilities:

  • Owning the entire game’s service graph.
  • Acting as a global singleton across players.

GBUserScopeRootAdapter (plugin-provided adapter)

A small adapter surface intended to sit under a game’s existing UserScopeRoot.

Responsibilities:

  • Bridge between a game’s scope root and GridPlacement’s expectations.
  • Provide a stable integration surface for:
    • service registry access
    • per-user input routing
    • lifecycle events

Minimal contract:

  • GBUserScopeRoot.Configure(ServiceRegistry registry)

Non-responsibilities:

  • Defining the game’s overall scope model.

Game-owned UserScopeRoot (game-provided)

A game’s primary scope root node that orchestrates multiple systems:

  • Inventory
  • Abilities
  • UI
  • GridPlacement
  • etc.

GridPlacement should integrate into this root rather than forcing a new one.

Option A — Standalone plugin scope (fastest)

Use GBUserScopeRoot directly.

Best for:

  • demos
  • prototypes
  • single-user setups
  • validating GridPlacement in isolation

Done when:

  • GBUserScopeRoot registers services and owns the per-user input capture.

Game owns UserScopeRoot. GridPlacement provides GBUserScopeRootAdapter that is attached as a child.

Best for:

  • production games
  • multiplayer
  • teams with existing DI/service orchestration

Done when:

  • Game root calls an adapter method such as:
    • GBUserScopeRoot.Configure(ServiceRegistry registry)

and GridPlacement registers its services into the provided registry.

Option C — Game implements the interface directly (most integrated)

Game scope root implements an interface that GridPlacement can consume.

Example interface shape:

  • IUserScopeRoot
    • ServiceRegistry Registry { get; }
    • Node ScopeNode { get; }
    • int? PlayerId { get; }

Best for:

  • larger projects that want to avoid extra adapter nodes

Input routing per user

Rule: one input provider per user

Within a single user scope, there should be exactly one component that:

  • consumes raw InputEvent
  • translates to Core InputEventData
  • calls CoreInputService.Handle(...)

In the minimal-node setup, that component is GBUserScopeRoot.

Where the input provider lives

For 6.0 C#-heavy architecture:

  • Input provider is a per-scope node created/owned by the scope origin.
  • The provider resolves the registry-owned Core pipeline:
    • CoreInputService
    • interpreters (PositioningInputInterpreter, PlacementInputInterpreter, ManipulationInputInterpreter)

Consumers (e.g. targeting/cursor controller, placement UI) subscribe to interpreter events.

Practical wiring checklist

  • Ensure a scope origin exists per user/user.
  • Ensure that scope origin creates/owns a ServiceRegistry.
  • Register GridPlacement services into that registry.
  • Ensure exactly one input provider node per scope.
  • Ensure gameplay nodes are consumers (subscribe), not raw input owners.

Notes on parity with GDScript

  • GDScript parity for cursor/positioning should use GridPositioner2D as the consumer of positioning intents.
  • C# parity uses the equivalent targeting/cursor controller node as the consumer.

Where the contract is tested

The integration contract above is enforced in the demo C# test harness:

  • demos/grid_building_dev/godot_cs/InputPipelineSmokeGoDotTests.cs
  • demos/grid_building_dev/godot_cs/UserScopeRootIntegrationBehaviorTests.cs