Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions docs/quantum/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Quantum — Overview

Quantum is Stride's graph-based introspection framework. It wraps any .NET object hierarchy into a typed node graph, and that graph is the single source of truth for three things: **property grid display**, **undo/redo**, and **asset override tracking** (the "bold = overridden" behaviour in prefab/archetype workflows).

## Layers

```mermaid
flowchart TD
A["Stride.Core.Quantum<br/>sources/presentation/Stride.Core.Quantum/<br/>Live node graph over .NET objects"]
B["Stride.Core.Assets.Quantum<br/>sources/assets/Stride.Core.Assets.Quantum/<br/>Override tracking, base-asset linking"]
C["Stride.Core.Presentation.Quantum<br/>sources/presentation/Stride.Core.Presentation.Quantum/<br/>INodePresenter — property grid view model"]
D["Stride.Core.Assets.Editor<br/>sources/editor/Stride.Core.Assets.Editor/Quantum/<br/>IAssetNodePresenter, AssetNodePresenterUpdaterBase"]
E["Stride.Assets.Presentation<br/>sources/editor/Stride.Assets.Presentation/NodePresenters/<br/>Concrete updaters per asset type"]

A --> B
B --> C
C --> D
D --> E
```

## Data Flow

1. **Asset opens** — `AssetPropertyGraph` (created via `AssetQuantumRegistry.ConstructPropertyGraph` with the session's `AssetPropertyGraphContainer`) calls `GetOrCreateNode(asset)` on the underlying `NodeContainer` to build the core graph.
2. **Override semantics** — `AssetPropertyGraph` links each node to its counterpart in the base asset graph (if any) and marks inherited vs. overridden values.
3. **Presenter tree** — `AssetNodePresenterFactory` walks the node graph and creates one `IAssetNodePresenter` per node.
4. **Updaters run** — all registered `INodePresenterUpdater` implementations get `UpdateNode()` called per node, then `FinalizeTree()` called once for the full tree.
5. **Property grid binds** — the WPF property grid binds to the presenter tree: `DisplayName` → label, `Value/UpdateValue` → input control, `IsVisible/IsReadOnly` → visibility and editability, `AttachedProperties` → input constraints.

## When You Need Quantum

> **Decision tree:**
>
> - Adding a new asset type with default property grid display?
> → **No Quantum code needed.** `[DataMember]` and `[Display]` attributes on the asset class
> are sufficient. See [asset-system/README.md](../asset-system/README.md).
>
> - Customising visibility, display names, numeric constraints, or computed properties
> for a new or existing asset?
> → **Write an `INodePresenterUpdater`.** See [property-grid.md](property-grid.md).
>
> - Asset class holds members that are references to other content objects (Prefabs, Textures)?
> → **Write an `AssetPropertyGraphDefinition`.** See [asset-graph.md](asset-graph.md).
>
> - Custom override or inheritance behaviour (rare — scenes and prefabs already cover this)?
> → **Subclass `AssetPropertyGraph`.** See [asset-graph.md](asset-graph.md).

## Spoke Files

| File | Covers |
|---|---|
| [graph-model.md](graph-model.md) | `IGraphNode`, `IObjectNode`, `IMemberNode`, `NodeContainer`, mutations, change listeners |
| [asset-graph.md](asset-graph.md) | `AssetPropertyGraph`, override model, `AssetPropertyGraphDefinition`, `AssetQuantumRegistry` |
| [property-grid.md](property-grid.md) | `INodePresenter`, `IAssetNodePresenter`, updater pipeline, `INodePresenterUpdater` cookbook |
121 changes: 121 additions & 0 deletions docs/quantum/asset-graph.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Asset Property Graph

## Role

`Stride.Core.Assets.Quantum` wraps the core Quantum graph with asset-specific semantics: override tracking, base-asset linking, and archetype/prefab inheritance. When a derived prefab overrides a value from its base, the graph records that override and can reset it. This is the layer that makes "this property is bold because it's overridden" possible in GameStudio.

## `IAssetNode` Extensions

`IAssetNode` extends `IGraphNode` with asset-aware members:

| Member | Type | Purpose |
|---|---|---|
| `PropertyGraph` | `AssetPropertyGraph` | The graph that owns this node |
| `BaseNode` | `IGraphNode` | The corresponding node in the base asset, or `null` if none |
| `OverrideChanging` | event | Raised before override state changes |
| `OverrideChanged` | event | Raised after override state changes |
| `ResetOverrideRecursively()` | method | Resets this node and all descendants to inherited values |
| `SetContent(key, node)` | method | Attaches an auxiliary node (used internally) |
| `GetContent(key)` | method | Retrieves an attached auxiliary node |

`IAssetMemberNode` (extends `IAssetNode`, `IMemberNode`) and `IAssetObjectNode` (extends `IAssetNode`, `IObjectNode`) are the concrete asset-aware node types. `IAssetObjectNode` adds per-item override tracking for collections (`IsItemInherited`, `IsItemOverridden`, `OverrideItem`, etc.).

## Override Model

A property in a derived asset is in one of three states:

| State | Meaning | GameStudio visual |
|---|---|---|
| **Inherited** | Value comes from the base asset; any change to the base propagates here | Normal weight, italic |
| **Overridden** | Value was explicitly set on this derived asset, shadowing the base | Bold |
| **No base** | Asset has no base (or this property has no base equivalent) | Normal weight |

**`ResetOverride()`** is a method on `IAssetNodePresenter` (the presenter layer — see `property-grid.md`), not on `IAssetNode` directly. Calling it restores the overridden value to its inherited state, and the graph then re-propagates the base value. The underlying graph node's `ResetOverrideRecursively()` handles the recursive reset; the presenter method is the entry point from the UI.

When a composite node (an object with children) is reset, all descendant nodes are also reset recursively.

> [!NOTE] Just adding a new asset type
> If your asset has no base/derived relationship and you are not implementing archetypes or prefab composition, the override model is invisible to you. `IsInherited` will always be `false` and `HasBase` will always be `false`. You do not need to understand this layer to add a new asset type.

## `AssetPropertyGraph`

`AssetPropertyGraph` wraps a `NodeContainer` and adds override semantics on top:

- Created via `AssetQuantumRegistry.ConstructPropertyGraph(container, assetItem, logger)` where `container` is an `AssetPropertyGraphContainer` (not a raw `NodeContainer`) — this is obtained from the editor session; do not instantiate it manually
- Tied to the editor session — created when an asset is opened, disposed when closed
- Links each node to its counterpart in the base asset graph (if the asset has an archetype)
- Propagates base values to all inherited nodes on load

You rarely interact with `AssetPropertyGraph` directly. The presenter layer reads from it via `IAssetNodePresenter.Asset` and `IAssetNodePresenter.HasBase`.

## `AssetPropertyGraphDefinition`

`AssetPropertyGraphDefinition` tells the graph which member values are **object references** (shared identities, loaded separately by `ContentManager`) vs **inline data** (copied into the asset).

Provide one only when your asset type holds references to other content objects. If you don't, the default definition treats all values as inline — which is correct for most new asset types.

```csharp
// sources/engine/Stride.Assets/YourFeature/YourAssetPropertyGraphDefinition.cs
using Stride.Core.Assets.Quantum;
using Stride.Core.Quantum;

namespace Stride.Assets.YourFeature;

[AssetPropertyGraphDefinition(typeof(YourAsset))]
public class YourAssetPropertyGraphDefinition : AssetPropertyGraphDefinition
{
// Return true when the value stored in 'member' is an object reference
// (i.e. a handle to a separately-loaded content object, not an inline copy).
public override bool IsMemberTargetObjectReference(IMemberNode member, object? value)
{
// Example: treat any Prefab member as an object reference
if (value is Prefab)
return true;

return base.IsMemberTargetObjectReference(member, value);
}

// Return true when a collection item is an object reference.
public override bool IsTargetItemObjectReference(IObjectNode collection, NodeIndex itemIndex, object? value)
{
// Example: treat items in PrefabCollection as object references
if (collection.Descriptor.ElementType == typeof(Prefab))
return true;

return base.IsTargetItemObjectReference(collection, itemIndex, value);
}
}
```

The `[AssetPropertyGraphDefinition(typeof(YourAsset))]` attribute is discovered automatically when the assembly is registered. No manual registration is needed beyond `AssetQuantumRegistry.RegisterAssembly()` in `Module.cs`.

> [!NOTE] Just adding a new asset type
> If all your asset's properties are plain data values (numbers, strings, lists of structs), you do not need an `AssetPropertyGraphDefinition`. Only provide one when your asset class has members that hold references to other content objects (Prefabs, Textures, Materials, etc.) that should remain as references rather than be embedded inline.

## `AssetQuantumRegistry`

| Method | When to call |
|---|---|
| `AssetQuantumRegistry.RegisterAssembly(assembly)` | From `Module.cs` — call once per assembly containing asset graph types |
| `AssetQuantumRegistry.ConstructPropertyGraph(AssetPropertyGraphContainer, AssetItem, ILogger?)` | Called internally by the editor session; do not call manually |
| `AssetQuantumRegistry.GetDefinition(assetType)` | Called internally; do not call manually |

`RegisterAssembly` scans the assembly for `AssetPropertyGraph` subclasses (decorated with `[AssetPropertyGraph(typeof(T))]`) and `AssetPropertyGraphDefinition` subclasses (decorated with `[AssetPropertyGraphDefinition(typeof(T))]`) and registers them.

In `Module.cs` for an assembly that contains both asset classes and graph types:
```csharp
[ModuleInitializer]
public static void Initialize()
{
// AssemblyRegistry.Register is required for assemblies that contain Asset subclasses.
// If the assembly only contains graph types (no Asset subclasses), omit this line.
AssemblyRegistry.Register(typeof(Module).Assembly, AssemblyCommonCategories.Assets);
AssetQuantumRegistry.RegisterAssembly(typeof(Module).Assembly);
}
```

## Assembly Placement

`Stride.Core.Assets.Quantum` — `sources/assets/Stride.Core.Assets.Quantum/`

Concrete `AssetPropertyGraphDefinition` subclasses for engine assets live alongside their asset classes (e.g. `sources/engine/Stride.Assets/`).
96 changes: 96 additions & 0 deletions docs/quantum/graph-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Graph Model

## Role

`Stride.Core.Quantum` builds a live, typed graph over any .NET object hierarchy. Every property and collection item becomes a node. All reads and writes go through the graph, which ensures change notifications fire and undo/redo integrations receive every mutation. This is the foundation layer — it knows nothing about assets, editors, or UI.

## Node Types

| Type | What it represents | Key members |
|---|---|---|
| `IGraphNode` | Base interface for all nodes | `Guid`, `Type`, `Descriptor`, `IsReference`, `Retrieve()`, `Retrieve(NodeIndex)` |
| `IObjectNode : IGraphNode` | An object with named members and/or collection items | `Members`, `Indices`, `IsEnumerable`, `this[string name]`, `Update(object?, NodeIndex)`, `Add()`, `Remove()` |
| `IMemberNode : IGraphNode` | A single named property — child of an `IObjectNode` | `Name`, `Parent`, `Target`, `MemberDescriptor`, `Update(object?)` |

`IObjectNode` is the node for the object itself. `IMemberNode` is the node for each of its properties. Accessing `myObjectNode["MyProperty"]` returns the `IMemberNode` for `MyProperty`. Accessing `memberNode.Target` returns the `IObjectNode` for the referenced object when `IsReference` is true.

## `NodeContainer`

`NodeContainer` is the factory and owner of all nodes. Call `GetOrCreateNode(object)` to enter the graph for any object:

```csharp
var container = new NodeContainer();
IObjectNode rootNode = container.GetOrCreateNode(myAsset);
```

Nodes are keyed by object identity (via `ConditionalWeakTable`). Calling `GetOrCreateNode` on the same object twice returns the same node. Call `GetNode` (non-creating variant) when you only want to look up an existing node.

**Reference vs. value nodes:** When a member holds a reference to another object, `IMemberNode.IsReference` is `true` and `IMemberNode.Target` returns the `IObjectNode` for that object (creating it if needed). When a member holds a value type or a primitive, `IsReference` is `false` and the value is stored inline.

## Mutations

Never set properties on the underlying object directly while the graph is active — change notifications will not fire. Always mutate through the node:

```csharp
// Update a member value
IMemberNode member = rootNode["MyProperty"];
member.Update(newValue);

// Update a collection item
// IMemberNode.Target is non-null when IsReference is true (i.e. the member holds a reference-type collection like List<T>)
IMemberNode listMember = rootNode["MyList"];
IObjectNode list = listMember.Target!; // valid when listMember.IsReference == true
list.Update(newItem, new NodeIndex(0)); // replace item at index 0

// Add to a collection
list.Add(newItem);
list.Add(newItem, new NodeIndex(2)); // insert at index 2

// Remove from a collection
list.Remove(existingItem, new NodeIndex(0));
```

## Observing Changes

`GraphNodeChangeListener` subscribes to all nodes reachable from a root and surfaces four events:

```csharp
var listener = new GraphNodeChangeListener(rootNode);

listener.ValueChanging += (sender, e) => { /* MemberNodeChangeEventArgs: e.Member, e.OldValue, e.NewValue */ };
listener.ValueChanged += (sender, e) => { /* MemberNodeChangeEventArgs: e.Member, e.OldValue, e.NewValue */ };
listener.ItemChanging += (sender, e) => { /* ItemChangeEventArgs: e.Collection, e.Index, e.OldValue, e.NewValue */ };
listener.ItemChanged += (sender, e) => { /* ItemChangeEventArgs: e.Collection, e.Index, e.OldValue, e.NewValue */ };

listener.Initialize(); // walk the graph after subscribing

// Dispose to unsubscribe from all nodes
listener.Dispose();
```

`GraphNodeChangeListener` accepts any `IGraphNode` as its root — not just `IObjectNode`. You can start the listener from a member node if needed.

Call `Initialize()` after subscribing to events — it walks the graph and registers all reachable nodes. Always `Dispose()` the listener when done; failing to do so leaks node subscriptions.

## `NodeIndex`

`NodeIndex` is a `readonly struct`. It cannot be `null`; use `NodeIndex.Empty` as the "no index" sentinel.

`NodeIndex` addresses items in collection nodes:

```csharp
NodeIndex.Empty // non-collection members (the "no index" sentinel)
new NodeIndex(0) // list item at position 0
new NodeIndex("key") // dictionary item with key "key"
```

```csharp
NodeIndex idx = new NodeIndex(2);
idx.IsEmpty // false
idx.IsInt // true
idx.Int // 2
```

## Assembly Placement

`Stride.Core.Quantum` — `sources/presentation/Stride.Core.Quantum/`
Loading