Payloads vs Snapshots (Intent)
This guide defines the intended meaning of payloads vs snapshots in GridPlacement. It exists to prevent three common failure modes:
- Treating an event payload as authoritative internal state.
- Polluting a payload object with service/session-only diagnostics.
- Accidentally exposing internal state objects (breaking test isolation and portability).
Boundary
- Core (C#)
- Owns authoritative state and domain events.
- Exposes payloads and snapshots as immutable/read-only views.
- Engine glue (Godot)
- Translates engine input into Core service calls.
- Applies Core outputs (effects, transforms, visuals).
- UI / diagnostics
- Should never depend on internal state objects.
- Should consume snapshots/payloads only.
Border conversion rule (Godot)
When data crosses the engine boundary, it must be converted to the boundary contract:
- Signals/events should emit small EventData payload objects.
- Internal state containers remain private to the owning service.
- Snapshots are pulled explicitly for diagnostics/tests/front-end read requests.
GDScript-first signal signatures
In Godot (GDScript), we intentionally keep signal signatures GDScript-first (often untyped) to avoid load-order parse failures and warnings-as-errors.
Contract rule:
- Emit an EventData payload instance from the signal.
- Do not require typed signal annotations to enforce the contract.
Compatibility note (5.1 GDScript track)
The 5.1 GDScript runtime does not have a Core boundary to convert into. It can only emit GDScript-native payload objects. The 6.0 C# track can emit Core payloads/snapshots directly.
Definitions (6.0 intent)
Payload
A payload is the data that is deliberately emitted across a boundary (events/signals/effects).
- Authoritative for the event that carried it.
- Not authoritative for the service’s internal state over time.
- May be mutable internally while the service is processing, but treat it as ephemeral once emitted.
Example (C#): event payload data associated with a manipulation update.
Snapshot
A snapshot is a read-only projection of an internal state container at a point in time.
- Intended for diagnostics/UI/tests.
- Must not be used as a state container.
- If you need additional information later, ask the service again for a new snapshot.
Example (C#): ManipulationSnapshot record struct created from ManipulationState.
Identity (what identifies “a manipulation”)
A manipulation is identified by a manipulation ID, not by object identity of a payload or snapshot.
- C# canonical identity:
ManipulationId(string) - Payload identity rule: if a payload contains
ManipulationId, treat that as the identity. - Snapshot identity rule: snapshots must include
ManipulationIdso observers can correlate.
Lifetime
| Thing | Lifetime | Who owns it | Notes |
|---|---|---|---|
| Authoritative internal state | session / interaction | service | mutable, private |
| Snapshot | point-in-time | caller | discard/recreate |
| Payload | per-event | event emitter | should be treated as immutable by listeners |
Testing guidance
- Prefer Core unit/integration tests to validate snapshot/payload correctness.
- Use Godot tests only to validate engine glue (signal wiring, input translation).
Recommended tests to reference
- GDScript service signal contract (5.1 track):
demos/grid_building_dev/godot/test/grid_building/unit/core/logger_free_services_test.gd
Mapping: C# Core vs GDScript 5.1 (compatibility note)
GridPlacement 6.0 (C#) already models the separation explicitly:
- C# internal state:
GridPlacement.Core.State.Manipulation.ManipulationState - C# snapshot:
GridPlacement.Core.Services.Manipulation.ManipulationSnapshot - C# event payloads:
GridPlacement.Core.Services.Manipulation.*Event(carryManipulationSnapshot)
GDScript 5.1 uses similar concepts but with different constraints:
- GDScript payload:
ManipulationData(status/action/message/results) - GDScript service-owned state:
ManipulationService2D(authoritative; emits signals) - GDScript diagnostics snapshot:
ManipulationSnapshot2D(projection from service)
Targeting: 5.1 and 6.0 (this topic)
In the modern architecture (5.1+ and especially 6.0), the targeting boundary follows the same rule:
- Signals/events emit payload objects, not internal state objects.
- Snapshots exist for diagnostics/tests, not as “the thing you wire your UI to.”
In the 5.1 (GDScript) track specifically:
- Event payloads:
TargetingEventData2D.*(emitted byTargetingService2Dsignals) - Diagnostics snapshot:
TargetingSnapshot2D(projection fromTargetingState2D)
Why this exists:
- Prevents accidentally treating a mutable service state object as public API.
- Keeps event contracts small and stable (no “bag-of-state” signals).
- Keeps tests deterministic by snapshotting state at a point in time.
Legacy note (5.0)
GridBuilding 5.0 predates this separation and commonly exposed state directly to listeners.
- Signals were often wired to public state containers.
- There was no separate snapshot type for read-only diagnostic/test consumption.
The 5.1 and 6.0 lines improve this by explicitly separating:
- service-owned state (private)
- event payloads (public boundary)
- snapshots (read-only projections)
What we intentionally do NOT do
- Do not expose internal state objects across boundaries.
- Do not “just reuse the payload” as the internal state container.
- Do not attach engine nodes/refs to Core snapshots.