From 78421220213d0730d6c3192ce6360420d17963c9 Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Sat, 4 Apr 2026 14:52:53 +0200 Subject: [PATCH 1/7] docs: add runtime-type.md for asset system contributor docs --- .../contributors/asset-system/runtime-type.md | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 docs/contributors/asset-system/runtime-type.md diff --git a/docs/contributors/asset-system/runtime-type.md b/docs/contributors/asset-system/runtime-type.md new file mode 100644 index 0000000000..a4851e1fce --- /dev/null +++ b/docs/contributors/asset-system/runtime-type.md @@ -0,0 +1,78 @@ +# Runtime Type + +## Role + +The runtime type is the engine-facing class that `ContentManager` loads at runtime. It is the +compiled output of the build pipeline: when a compiler processes a design-time asset (the `Asset` +subclass), its `DoCommandOverride` method constructs an instance of the runtime type and writes it +to disk by calling `contentManager.Save(url, runtimeInstance)`. At game runtime, `ContentManager.Load(url)` +deserializes that stored data and returns an instance of this type. The runtime type is **not** the +asset class — the asset class is the editor-only, design-time counterpart that lives in the +`sources/editor/` or `sources/engine/*Assets*` layer and is never loaded by the game executable. + +## Required Attributes + +| Attribute | Required? | Purpose | +|-----------|-----------|---------| +| `[DataContract]` | Yes | Marks the type for YAML/binary serialization. Without this attribute the serialization system ignores the type entirely. | +| `[ContentSerializer(typeof(DataContentSerializer))]` | Yes | Selects the serializer used when loading/saving this type via `ContentManager`. Use `DataContentSerializerWithReuse` when instances should be reused across prefab instantiation rather than cloned. | +| `[ReferenceSerializer, DataSerializerGlobal(typeof(ReferenceSerializer), Profile = "Content")]` | Yes | Registers a reference serializer for the `"Content"` profile, used when another content object references this one by URL rather than embedding a copy inline. (`[ReferenceSerializer]` is shorthand for `[ReferenceSerializerAttribute]` in `Stride.Core.Serialization.Contents`; the second part names the generic `ReferenceSerializer` class.) | +| `[DataSerializerGlobal(typeof(CloneSerializer), Profile = "Clone")]` | Recommended | Registers a clone serializer for the `"Clone"` profile, needed for undo/redo and prefab instantiation. Omit only if the type is never cloned in the editor. For types whose assembly is already covered by `EntityCloner`'s centralized registrations (see `sources/engine/Stride.Engine/Engine/Design/EntityCloner.cs`), placing this attribute on the type would duplicate that registration — check `EntityCloner.cs` first. For types in new assemblies not covered there, placing the attribute on the type is the correct modern pattern. | + +> **Decision:** Use `DataContentSerializerWithReuse` instead of `DataContentSerializer` when +> the type should be shared by reference across multiple prefab instances (e.g. a shared sprite sheet, +> a shared material). Use `DataContentSerializer` when each consumer should receive its own +> independent copy. + +## Serialization Constraints + +- All member types must themselves carry `[DataContract]` if they are classes or structs. +- Use `[DataMemberIgnore]` on members that must not be serialized (e.g. computed caches, event handlers). +- Ordered collections (`List`) are supported; unordered collections (sets, bags) are not. +- Dictionaries are supported when the key type is a primitive (`string`, `int`, `Guid`, `enum`, etc.). +- Arrays are **not** supported — use `List` instead. +- Nullable value types (`int?`) are not supported. +- When a runtime type needs to reference another content object (one loaded by `ContentManager`), + store the URL string and call `ContentManager.Load()` at runtime to resolve it. Do not embed + the referenced object inline, as this prevents the content system from deduplicating loads and + tracking dependencies correctly. Recording the URL during compilation is the responsibility of the + compiler (via `AttachedReferenceManager.GetUrl()`), not the runtime type. + +## Assembly Placement + +Runtime types live in the same assembly as the engine feature they represent, typically under +`sources/engine/`. They must be in an assembly registered with `AssemblyCommonCategories.Assets` or +`AssemblyCommonCategories.Engine` (see [registration.md](registration.md)). They must **not** be placed in an +editor-only assembly (such as one under `sources/editor/`), because they are loaded at runtime by +the game executable and editor-only assemblies are not shipped with game builds. + +## Template + +Placeholder used below: + +- `%%AssetName%%` — PascalCase name without the `Asset` suffix (e.g. `SpriteSheet`, `Texture`, `MyEffect`) + +```csharp +using Stride.Core; +using Stride.Core.Serialization; +using Stride.Core.Serialization.Contents; +using Stride.Engine.Design; + +namespace Your.Namespace; + +/// Runtime representation of . +[DataContract] +[ContentSerializer(typeof(DataContentSerializer<%%AssetName%%>))] +[ReferenceSerializer, DataSerializerGlobal(typeof(ReferenceSerializer<%%AssetName%%>), Profile = "Content")] +[DataSerializerGlobal(typeof(CloneSerializer<%%AssetName%%>), Profile = "Clone")] +public class %%AssetName%% +{ + // Add runtime properties here. + // All member types must be [DataContract]-annotated. +} +``` + +> [!NOTE] Game projects +> Game-project runtime types follow the same pattern. The assembly containing them is discovered +> automatically because the compiler app scans all assemblies referenced by the game project. +> No explicit `AssemblyRegistry.Register` call is required. From cdd59058532b76ade6adf78ff05b9ff0f854910b Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Sat, 4 Apr 2026 15:02:14 +0200 Subject: [PATCH 2/7] docs: add asset-class.md for asset system contributor docs --- docs/contributors/asset-system/asset-class.md | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 docs/contributors/asset-system/asset-class.md diff --git a/docs/contributors/asset-system/asset-class.md b/docs/contributors/asset-system/asset-class.md new file mode 100644 index 0000000000..3daada5bc4 --- /dev/null +++ b/docs/contributors/asset-system/asset-class.md @@ -0,0 +1,109 @@ +# Asset Class + +## Role + +The asset class is the design-time representation of an asset. An instance is serialized to a `.sdXXX` YAML file on disk and loaded by GameStudio, where it holds all the properties an author sets in the editor. During the build pipeline the compiler reads an instance of this class and transforms it into the runtime type. The asset class is never loaded at runtime — only the compiled output is. + +## Choose a Base Class + +| Base class | Use when | Example | +|---|---|---| +| `Asset` | The asset's data is entirely defined inside GameStudio (no external source file). | `MaterialAsset`, `SpriteSheetAsset` | +| `AssetWithSource` | The asset imports data from an external file (e.g. `.fbx`, `.png`). Provides a `Source` property of type `UFile`. | `TextureAsset`, `SoundAsset`, `HeightmapAsset` | +| `AssetComposite` | The asset is composed of named sub-parts that can be individually referenced and overridden in derived assets. | `SceneAsset`, `PrefabAsset` | +| `AssetCompositeHierarchy` | Like `AssetComposite` but with a parent/child hierarchy among parts. Used for scenes and prefabs. Prefer `AssetComposite` unless you need a tree structure. | `SceneAsset`, `PrefabAsset` | + +> **Decision:** Start with `Asset`. Upgrade to `AssetWithSource` if the asset's primary content +> comes from a file on disk that Stride did not produce. Use `AssetComposite` only if your asset +> must support per-part archetype inheritance. + +## Required Attributes + +| Attribute | Required? | Purpose | +|---|---|---| +| `[DataContract("Name")]` | Yes | YAML serialization name. Must be unique across all assets. Use a short, stable PascalCase string (e.g. `"SpriteSheet"`, `"Texture"`). | +| `[AssetDescription(".sdXXX")]` | Yes | Registers the file extension(s) for this asset type. Multiple extensions: `".sdm3d;pdxm3d"` (only the first extension has a leading dot — subsequent extensions omit it). The primary extension must be unique across all asset types. | +| `[AssetContentType(typeof(RuntimeType))]` | Yes | Maps this design-time class to its runtime type. Used by `AssetRegistry` and the build pipeline. | +| `[AssetFormatVersion(packageName, currentConst, initialVersion)]` | Yes | Declares the current serialization version and the initial version (used to trigger upgraders). `packageName` is `StrideConfig.PackageName` (`"Stride"`) for engine assets. | +| `[AssetUpgrader(...)]` | When bumping the version | See the Versioning section below. | + +## Property Conventions + +- Use `[DataMember(N)]` on every public property that should be serialized, where `N` is an integer that determines the YAML field order. Leave gaps (e.g. `10`, `20`, `30`) to allow future insertion without renumbering. +- Use `[DataMemberIgnore]` on properties that must not be serialized (computed values, caches). +- Properties on the asset class are the editor-facing settings. Keep the asset class free of engine types that are not available at design time (shader objects, GPU resources, etc.). +- When the asset references another asset at design time, the member type should be the runtime type (e.g. `Texture`), not the asset type (`TextureAsset`). The `AttachedReferenceManager` records the asset reference as a URL on the runtime object during compilation. + +## File Extension Naming + +- Engine asset extensions use the `.sd` prefix followed by a short abbreviation: `.sdtex`, `.sdmat`, `.sdm3d`, `.sdsheet`, `.sdscene`, `.sdprefab`. +- Extensions are case-insensitive and must be globally unique across the engine. +- Register new extensions in `[AssetDescription]`. Verify uniqueness by searching: + ``` + grep -rn 'AssetDescription("\.sd' sources/ --include="*.cs" + ``` + +## Versioning and Upgraders + +Any change to a serialized property name or type that would make existing `.sdXXX` files unreadable requires a version bump. If a change is purely additive — a new optional property with a default value — a bump is not strictly required, but it is good practice because it makes the migration boundary explicit and allows the upgrader to set a sensible default for older files. + +Bump sequence: + +1. Update the `CurrentVersion` constant to a new version string (e.g. `"2.0.0.0"`). +2. Add an `[AssetUpgrader]` attribute pointing to a new upgrader class. +3. Implement the upgrader. + +```csharp +[AssetUpgrader(StrideConfig.PackageName, "1.0.0.0", "2.0.0.0", typeof(%%AssetName%%V2Upgrader))] +public sealed class %%AssetName%%Asset : Asset { ... } + +internal class %%AssetName%%V2Upgrader : AssetUpgraderBase +{ + protected override void UpgradeAsset( + AssetMigrationContext context, + PackageVersion currentVersion, + PackageVersion targetVersion, + dynamic asset, + PackageLoadingAssetFile assetFile, + OverrideUpgraderHint overrideHint) + { + // Modify the asset dynamic object to match the new schema. + // e.g. asset.NewPropertyName = asset.OldPropertyName; + // asset.OldPropertyName = DynamicYamlEmpty.Default; + } +} +``` + +Prefer `AssetUpgraderBase` over implementing `IAssetUpgrader` directly — it handles the YAML plumbing and exposes a simpler `UpgradeAsset` override with a dynamic view of the node. Implementing `IAssetUpgrader` directly requires working with the raw `YamlMappingNode` and manually calling `SetSerializableVersion`. + +## Assembly Placement + +Engine asset classes live in `sources/engine/Stride.Assets/` (for core engine assets) or in a feature-specific assembly such as `sources/engine/Stride.Assets.Models/`. The assembly must be registered with `AssemblyCommonCategories.Assets` in its `Module.cs` initializer (see [registration.md](registration.md)). Asset classes must not live in editor-only assemblies — the build pipeline runs outside the editor process and must be able to load and instantiate the asset class without any editor dependency. + +## Template + +```csharp +using Stride.Core; +using Stride.Core.Assets; +using Stride.Core.Assets.Compiler; + +namespace Your.Namespace; + +[DataContract("%%AssetName%%")] +[AssetDescription(FileExtension)] +[AssetContentType(typeof(%%AssetName%%))] // runtime type +[AssetFormatVersion(StrideConfig.PackageName, CurrentVersion, "1.0.0.0")] +// Add [AssetUpgrader(...)] here when you bump CurrentVersion — see the Versioning section above +public sealed class %%AssetName%%Asset : Asset // or AssetWithSource +{ + private const string CurrentVersion = "1.0.0.0"; + public const string FileExtension = ".sd%%shortname%%"; // choose a unique extension + + [DataMember(10)] + public SomeType SomeProperty { get; set; } +} +``` + +> [!NOTE] Game projects +> Replace `StrideConfig.PackageName` with a string literal matching your game's package name +> (e.g. `"MyGame"`). The rest of the pattern is identical. From 725542ce6b161c0c45ef2007b88b0e3df1f1229b Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Sat, 4 Apr 2026 15:12:13 +0200 Subject: [PATCH 3/7] docs: add compiler.md for asset system contributor docs --- docs/contributors/asset-system/compiler.md | 164 +++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 docs/contributors/asset-system/compiler.md diff --git a/docs/contributors/asset-system/compiler.md b/docs/contributors/asset-system/compiler.md new file mode 100644 index 0000000000..437cb73111 --- /dev/null +++ b/docs/contributors/asset-system/compiler.md @@ -0,0 +1,164 @@ +# Compiler + +## Role + +The compiler transforms a design-time `Asset` instance into compiled runtime content stored in +the content database. It is invoked during the game build and by the editor for live preview. A +compiler is registered by decorating the compiler class with `[AssetCompiler]`. The compiler runs +on a background thread and must not access any editor UI state. + +## Register the Compiler + +```csharp +[AssetCompiler(typeof(%%AssetName%%Asset), typeof(AssetCompilationContext))] +public class %%AssetName%%Compiler : AssetCompilerBase { ... } +``` + +`AssetCompilationContext` is the standard context for game-asset compilation. Other contexts exist +for thumbnail generation and template expansion, but `AssetCompilationContext` is always the right +choice for new engine assets. + +## Implement `Prepare` + +`Prepare` is called once per asset. It must populate `result.BuildSteps` with one or more +`AssetCommand` instances that do the actual work. The commands are executed later, possibly in +parallel with commands from other assets. + +```csharp +protected override void Prepare( + AssetCompilerContext context, + AssetItem assetItem, + string targetUrlInStorage, + AssetCompilerResult result) +{ + var asset = (%%AssetName%%Asset)assetItem.Asset; + result.BuildSteps = new AssetBuildStep(assetItem); + result.BuildSteps.Add(new %%AssetName%%Command(targetUrlInStorage, asset, assetItem.Package)); +} +``` + +`targetUrlInStorage` is the URL under which the compiled output must be saved in the content +database. Pass it to `ContentManager.Save` inside `DoCommandOverride`. + +## Implement the Build Command + +The command does the actual compilation work. It extends `AssetCommand` where +`TParameters` is the asset type (or a dedicated parameters struct for complex conversions). + +```csharp +public class %%AssetName%%Command(string url, %%AssetName%%Asset parameters, IAssetFinder assetFinder) + : AssetCommand<%%AssetName%%Asset>(url, parameters, assetFinder) +{ + protected override Task DoCommandOverride(ICommandContext commandContext) + { + var contentManager = new ContentManager(MicrothreadLocalDatabases.ProviderService); + + // Build the runtime object from Parameters (the asset). + var runtimeObject = new %%AssetName%% + { + // Map asset properties → runtime properties. + }; + + contentManager.Save(Url, runtimeObject); + return Task.FromResult(ResultStatus.Successful); + } +} +``` + +`Parameters` is the typed asset instance passed from `Prepare`. `Url` is the +`targetUrlInStorage` value passed to the constructor. +`MicrothreadLocalDatabases.ProviderService` provides the content database to the +`ContentManager` on the compiler thread. + +## Declare External File Dependencies (`GetInputFiles`) + +Override `GetInputFiles` when the compiler reads external files (e.g. a `.png` or `.fbx`). This +allows the build system to detect changes and invalidate the cache correctly. + +The pattern requires **two parts**: overriding `GetInputFiles` on the compiler class, and wiring +it to the build command via `InputFilesGetter` in `Prepare`. Without the wiring, the build cache +will never invalidate when source files change. + +**Step 1 — Override `GetInputFiles` on the compiler:** + +```csharp +public override IEnumerable GetInputFiles(AssetItem assetItem) +{ + var asset = (%%AssetName%%Asset)assetItem.Asset; + if (!string.IsNullOrEmpty(asset.Source)) + yield return new ObjectUrl(UrlType.File, GetAbsolutePath(assetItem, asset.Source)); +} +``` + +Check that `asset.Source` is not null or empty before calling `GetAbsolutePath` — `GetAbsolutePath` +throws an `ArgumentException` if passed a null or empty path. `GetAbsolutePath` is a helper on +`AssetCompilerBase` that resolves a `UFile` source path relative to the asset's location on disk. + +**Step 2 — Wire it to the command in `Prepare`:** + +```csharp +result.BuildSteps.Add( + new %%AssetName%%Command(targetUrlInStorage, asset, assetItem.Package) + { InputFilesGetter = () => GetInputFiles(assetItem) }); +``` + +`InputFilesGetter` is a `Func>` delegate on `Command`. The build engine +calls it when computing the command hash; without it, file changes are invisible to the cache. + +> [!NOTE] +> Alternatively, override `GetInputFiles()` (no parameters) directly on the command class itself. +> `Command.GetInputFiles()` calls `InputFilesGetter` by default, but you can override it instead +> to keep the logic self-contained on the command — this avoids the delegate wiring entirely. +> `TextureConvertCommand` uses this approach. + +## Declare Asset Dependencies (`GetInputTypes`) + +Override `GetInputTypes` when the compiler needs another asset to be compiled first, or needs to +read another asset's compiled output during compilation. + +```csharp +public override IEnumerable GetInputTypes(AssetItem assetItem) +{ + // Require GameSettingsAsset to be compiled (to read platform settings during compilation). + yield return new BuildDependencyInfo( + typeof(GameSettingsAsset), + typeof(AssetCompilationContext), + BuildDependencyType.CompileAsset); +} +``` + +`BuildDependencyType` is a `[Flags]` enum: + +| Value | Meaning | +|---|---| +| `Runtime` | The compiled output of the dependency is needed at runtime (embedded reference). | +| `CompileAsset` | The uncompiled `Asset` object of the dependency is read during compilation. | +| `CompileContent` | The compiled output of the dependency is read during compilation. | + +Use `CompileAsset` when you only need to read the asset class properties — this is cheap and +imposes no hard build-order constraint beyond "loaded". Use `CompileContent` when you need the +compiled binary output, which requires the dependency to be fully compiled first. Use `Runtime` +for runtime references that are embedded in the compiled output, not accessed at compile time. + +> [!WARNING] +> Do not create circular dependencies via `GetInputFiles` or `GetInputTypes`. Asset A depending +> on B while B depends on A will deadlock the build. + +## Assembly Placement + +Compiler classes live in the same assembly as the asset class. For engine assets in +`Stride.Assets`, the compiler also lives in `Stride.Assets`. The `[AssetCompiler]` attribute is +discovered at startup when the assembly is registered with `AssemblyCommonCategories.Assets`. +Compilers must not reference editor assemblies. + +## Template + +The `Prepare` method and build command shown above form the complete starting template for a new +compiler. Copy both blocks, replace `%%AssetName%%` with your asset's PascalCase name, and fill +in the property mapping inside `DoCommandOverride`. + +> [!NOTE] Game projects +> For game-project custom assets, the compiler class lives in the game project itself. +> Add `` +> to the game project's `.csproj` — this brings in the infrastructure that discovers and invokes +> the compiler; it does not change where the compiler class lives. From bd3be93a9acc24c3d1a54245c4cba3e9047125ac Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Sat, 4 Apr 2026 15:26:04 +0200 Subject: [PATCH 4/7] docs: add editor.md for asset system contributor docs --- docs/contributors/asset-system/editor.md | 188 +++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 docs/contributors/asset-system/editor.md diff --git a/docs/contributors/asset-system/editor.md b/docs/contributors/asset-system/editor.md new file mode 100644 index 0000000000..9c75f56015 --- /dev/null +++ b/docs/contributors/asset-system/editor.md @@ -0,0 +1,188 @@ +# Editor Support + +## Role + +The editor layer determines how an asset is presented and edited in GameStudio. Three tiers +exist with increasing complexity. Choose the lowest tier that meets your needs — Tier 1 +requires no editor code at all. + +## Tier 1: Automatic Property Grid (No Code Required) + +Every asset automatically gets a property grid in GameStudio at no cost: + +- Quantum introspects the asset class and builds a node graph from its `[DataContract]` / + `[DataMember]`-annotated properties. +- The property grid renders each property with an appropriate template based on its type. +- `[DataMember(N)]` controls display order. `[Display("Label", "Category")]` customises the + label and groups properties under a collapsible category header. +- `[DataMemberIgnore]` hides a property from the grid entirely. +- Collection items are shown as expandable sub-lists with inline add/remove controls. + +This tier is active automatically for every asset that has no custom `AssetViewModel`. No +additional classes, attributes, or registrations are needed. + +> **Decision: choose Tier 1 when** the asset's properties can be expressed as plain data +> members with standard types (primitives, references to other assets, known collection types). +> Most new engine assets start here. + +## Tier 2: Custom `AssetViewModel` + +> **Decision: choose Tier 2 when** you need custom commands in the property grid, computed +> display properties, cross-property validation, or custom drag-and-drop handling for the asset +> in the asset browser — but you don't need a dedicated editor panel or window. + +### Implementation + +Create a class that inherits `AssetViewModel` and decorate it with +`[AssetViewModel]`. The framework discovers and instantiates it automatically when +the asset is selected in GameStudio. + +```csharp +using Stride.Core.Assets.Editor.Annotations; +using Stride.Core.Assets.Editor.ViewModel; + +namespace Stride.Assets.Presentation.ViewModel; + +[AssetViewModel<%%AssetName%%Asset>] +public class %%AssetName%%ViewModel : AssetViewModel<%%AssetName%%Asset> +{ + public %%AssetName%%ViewModel(AssetViewModelConstructionParameters parameters) + : base(parameters) + { + } + + // Override or extend as needed. + // Access the typed asset via: Asset (returns %%AssetName%%Asset) +} +``` + +What can be overridden or extended: + +- Add computed properties (read-only, no Quantum backing node required). +- Override `UpdateAssetFromSource` (`protected internal virtual Task UpdateAssetFromSource(Logger logger)`) as a general refresh/sync hook: the editor calls it whenever the asset needs to be re-synchronized with external state (e.g. a source file change, a manual reload request, or another editor-triggered refresh). +- Attach commands by creating `AnonymousCommand` instances and surfacing them as properties; + command buttons can then be bound from XAML templates. +- Access editor services through the inherited `ServiceProvider`. + +Keep the ViewModel thin — business logic belongs in the asset class itself, not here. + +### Assembly Placement + +`Stride.Assets.Presentation` (`sources/editor/Stride.Assets.Presentation/`). This assembly +is editor-only and must not be referenced by runtime or compiler assemblies. + +## Tier 3: Full Custom Editor + +> **Decision: choose Tier 3 when** your asset requires a dedicated editing environment beyond +> a property grid — a canvas, a node graph, a timeline, a sprite editor, etc. +> Examples: `SpriteSheetEditorViewModel`, `GraphicsCompositorEditorViewModel`, +> `SceneEditorViewModel`. + +### ViewModel + +Create a class inheriting `AssetEditorViewModel`, decorated with +`[AssetEditorViewModel]` where `TViewModel` is your Tier 2 ViewModel (or +`AssetViewModel` directly if you have no Tier 2 class). + +```csharp +using Stride.Core.Assets.Editor.Annotations; +using Stride.Core.Assets.Editor.ViewModel; + +namespace Stride.Assets.Presentation.AssetEditors.%%AssetName%%Editor.ViewModels; + +[AssetEditorViewModel<%%AssetName%%ViewModel>] +public class %%AssetName%%EditorViewModel : AssetEditorViewModel +{ + public %%AssetName%%EditorViewModel(%%AssetName%%ViewModel asset) + : base(asset) + { + } + + /// + public override async Task Initialize() + { + // Perform async initialisation (load preview data, set up subscriptions, etc.) + // Return false to abort opening the editor. + return true; + } + + /// + public override bool PreviewClose(bool? save) + { + // Return false to cancel closing (e.g. prompt the user to save unsaved changes). + return true; + } +} +``` + +Key members inherited from `AssetEditorViewModel`: + +- `Asset` — the `AssetViewModel` passed to the constructor (cast it to your typed subclass as + needed). +- `UndoRedoService` — the undo/redo stack; use it for all mutating operations so that + Ctrl+Z/Ctrl+Y work correctly. +- `ServiceProvider` — access editor services such as `IEditorDialogService`. +- `Session` — the current `SessionViewModel`, providing access to the full asset database. + +### View (XAML) + +Create a WPF `UserControl` (partial class) that implements `IEditorView` and is decorated +with `[AssetEditorView]`. The view is the control shown inside the editor +tab when the asset is opened. + +```csharp +using Stride.Core.Assets.Editor.Annotations; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Assets.Editor.ViewModel; + +namespace Stride.Assets.Presentation.AssetEditors.%%AssetName%%Editor.Views; + +[AssetEditorView<%%AssetName%%EditorViewModel>] +public partial class %%AssetName%%EditorView : IEditorView +{ + private readonly TaskCompletionSource editorInitializationNotifier = new(); + + public %%AssetName%%EditorView() + { + InitializeComponent(); + } + + /// + public Task EditorInitialization => editorInitializationNotifier.Task; + + /// + public async Task InitializeEditor(IAssetEditorViewModel editor) + { + var result = await editor.Initialize(); + if (!result) + editor.Destroy(); + editorInitializationNotifier.SetResult(); + return result; + } +} +``` + +Matching XAML stub (`%%AssetName%%EditorView.xaml`): + +```xml + + + +``` + +### Assembly Placement + +Both the ViewModel and View live in `Stride.Assets.Presentation`. Place the ViewModel under +`AssetEditors/%%AssetName%%Editor/ViewModels/` and the View under +`AssetEditors/%%AssetName%%Editor/Views/`, following the convention used by `SpriteEditor`, +`GraphicsCompositorEditor`, `SceneEditor`, etc. + +### How the Editor Opens + +When a user double-clicks an asset whose `AssetViewModel` type has a registered +`[AssetEditorViewModel]` attribute, GameStudio's `AssetEditorsManager` finds the editor +ViewModel type, instantiates it, then finds the matching `[AssetEditorView]` view, creates +it, and calls `InitializeEditor`. No additional registration code is needed beyond the +attributes. From c3d12df95da101a4efb9723a0956c62699aad00b Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Sat, 4 Apr 2026 15:35:08 +0200 Subject: [PATCH 5/7] docs: add registration.md for asset system contributor docs --- .../contributors/asset-system/registration.md | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 docs/contributors/asset-system/registration.md diff --git a/docs/contributors/asset-system/registration.md b/docs/contributors/asset-system/registration.md new file mode 100644 index 0000000000..2b90cd4501 --- /dev/null +++ b/docs/contributors/asset-system/registration.md @@ -0,0 +1,106 @@ +# Registration and Discovery + +## Role + +All asset types, compilers, and editor ViewModels are discovered automatically at startup through a chain of attribute scanning triggered by assembly registration. No central registry file needs to be edited when adding a new asset type — decorating classes with the correct attributes is sufficient, provided the assembly is registered. + +## Discovery Chain + +The full chain from module load to asset type registration: + +1. **`[ModuleInitializer]`** — Stride's source generator calls the `Initialize()` method in `Module.cs` when the assembly is first loaded. +2. **`AssemblyRegistry.Register(..., AssemblyCommonCategories.Assets)`** — registers the assembly. `AssetRegistry` subscribes to the `AssemblyRegistry.AssemblyRegistered` event. +3. **`AssetRegistry.RegisterAssetAssembly(assembly)`** — triggered automatically by the event. Scans the assembly for types inheriting `Asset` (via `[AssemblyScan]` on the `Asset` base class), reads their `[AssetDescription]`, `[AssetContentType]`, `[AssetFormatVersion]`, and `[AssetUpgrader]` attributes, and populates the internal registry dictionaries. Compiler registration is separate — `AssetCompilerRegistry` scans registered assemblies for types implementing `IAssetCompiler` and reads `[AssetCompiler]` from those types. +4. **Editor discovery** — `Stride.GameStudio` registers `StrideDefaultAssetsPlugin` (and other plugins) via `AssetsPlugin.RegisterPlugin()` at startup. Each plugin scans its own assembly for `[AssetViewModel]`, `[AssetEditorViewModel]`, and `[AssetEditorView]` attributes to wire up the editor tier for each asset type. For engine assets, all ViewModels and editor views live in `Stride.Assets.Presentation`, which is covered by `StrideDefaultAssetsPlugin`. + +## Module Initializer Pattern + +Every assembly that contains engine asset types must have a `Module.cs` (by convention — the file name doesn't matter, but `Module.cs` is universal across the codebase): + +```csharp +// sources/engine/Your.Assembly/Module.cs +using Stride.Core; +using Stride.Core.Reflection; + +namespace Your.Assembly; + +internal class Module +{ + [ModuleInitializer] + public static void Initialize() + { + AssemblyRegistry.Register(typeof(Module).Assembly, AssemblyCommonCategories.Assets); + // If the assembly also contains runtime types needed by the serializer: + // AssemblyRegistry.Register(typeof(SomeRuntimeType).Assembly, AssemblyCommonCategories.Assets); + } +} +``` + +`[ModuleInitializer]` here is `Stride.Core.ModuleInitializerAttribute` (namespace `Stride.Core`), **not** the .NET BCL `System.Runtime.CompilerServices.ModuleInitializerAttribute`. At build time, `Stride.Core.CompilerServices` (a Roslyn source generator) collects all methods tagged with this attribute and generates a single BCL `[ModuleInitializer]` entry point that calls them all in the correct order. Using the BCL attribute directly would bypass this dispatch. + +If the assembly also contains runtime types in a separate assembly (e.g. `Stride.Assets` registers types from `Stride.Rendering`, `Stride.Graphics`, `Stride.Shaders`), register those assemblies here too, as shown in `sources/engine/Stride.Assets/Module.cs`. + +## `AssemblyRegistry` vs `AssetRegistry` + +| | `AssemblyRegistry` | `AssetRegistry` | +|---|---|---| +| What it tracks | Which assemblies are loaded and under which category | Asset types, file extensions, upgraders, serializer factories, content-type mappings | +| Populated by | `AssemblyRegistry.Register(...)` calls in `Module.cs` | Automatically, in response to `AssemblyRegistry.AssemblyRegistered` event (for `Assets` category) | +| Direct use | Only in `Module.cs` | Rarely needed directly; mainly queried by the build pipeline and GameStudio | + +`AssetRegistry.RegisterAssetAssembly` is `private static` — it can only be triggered via `AssemblyRegistry.Register`. + +## YAML Serializer Factories + +Most asset types do not need a custom YAML serializer factory — the default handles all `[DataContract]` types. A custom `IYamlSerializableFactory` is only needed when the asset class uses an abstract or interface member type that cannot be resolved by the default serializer (e.g. a custom polymorphic type hierarchy that Stride's YAML system doesn't know about). + +To register a factory, create a class that implements `IYamlSerializableFactory` and decorate it with `[YamlSerializerFactory("Default")]`. `AssetRegistry` discovers and instantiates it automatically when the assembly is registered — no manual registration call is needed. This is rare — check existing factories for examples before writing one. + +## `.sdtpl` Template Files (New-Asset Menu) + +A `.sdtpl` file adds an entry to the **Add Asset** menu in GameStudio. It is a YAML file: + +```yaml +!TemplateAssetFactory +Id: 00000000-0000-0000-0000-000000000000 # Replace with a new unique GUID +AssetTypeName: %%AssetName%%Asset # Must match the C# class name (namespace optional) +Name: %%Display Name%% +Scope: Asset +Description: %%Short description%% +Group: %%Category in Add Asset menu%% +Order: 0 # Lower numbers appear first within the group +DefaultOutputName: %%DefaultFileName%% +``` + +Field reference: + +- **`Id`** — must be globally unique. Generate with Visual Studio (**Tools > Create GUID**) or any online GUID generator. +- **`AssetTypeName`** — the simple class name of the `Asset` subclass. Namespace is not required and is ignored. +- **`Scope`** — always `Asset` for standard asset templates. +- **`Group`** — the menu group in GameStudio's Add Asset dialog. Existing groups (exact strings): `Animation`, `Font`, `Material`, `Media`, `Miscellaneous`, `Model`, `Physics`, `Physics-Bepu`, `Scene`, `Script`, `Sprite`, `Texture`, `UI`. Use an existing group or introduce a new one. +- **`Order`** — controls sort position within the group. Inspect existing `.sdtpl` files in `sources/editor/Stride.Assets.Presentation/Templates/Assets/` for reference values. + +Place the `.sdtpl` file in: + +``` +sources/editor/Stride.Assets.Presentation/Templates/Assets/%%Group%%/%%AssetName%%.sdtpl +``` + +The directory name under `Assets/` does not have to match the `Group` string exactly — the directory is just for organisation. The file is embedded automatically via the wildcard include already present in `Stride.Assets.Presentation.csproj` — no manual `.csproj` edit is needed for engine assets. + +> [!NOTE] Game projects +> For game-project custom assets, place the `.sdtpl` file anywhere under the project's +> `Templates/` folder, then register it in the `.sdpkg` file: +> +> ```yaml +> TemplateFolders: +> - Path: !dir Templates +> Group: Assets +> Files: +> - !file Templates/%%AssetName%%.sdtpl +> ``` +> +> No `Module.cs` or `AssemblyRegistry.Register` call is needed for game-project assets. Compiler +> discovery is handled by the `Stride.Core.Assets.CompilerApp` plugin mechanism — the compiler +> class is found automatically as long as the game project references +> `Stride.Core.Assets.CompilerApp` as a build-only dependency. From d59b95f4b6c9585e2069a222b75f745ceab8b55b Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Sat, 4 Apr 2026 16:04:38 +0200 Subject: [PATCH 6/7] docs: add overview.md, completing asset system contributor docs --- docs/contributors/asset-system/overview.md | 121 +++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 docs/contributors/asset-system/overview.md diff --git a/docs/contributors/asset-system/overview.md b/docs/contributors/asset-system/overview.md new file mode 100644 index 0000000000..e09d6f75d1 --- /dev/null +++ b/docs/contributors/asset-system/overview.md @@ -0,0 +1,121 @@ +# Asset System — Contributor Overview + +This document explains the Stride asset system for engine contributors. It covers the complete +pipeline from design-time authoring through build compilation to runtime loading, and shows +how all parts are wired together. + +> **For game-project custom assets** (not engine contributions), the same pipeline applies +> with minor differences noted in each spoke file. See also the external guide: +> [Creating custom assets](https://doc.stride3d.net/latest/en/manual/scripts/custom-assets.html). + +## The Three-Phase Pipeline + +```mermaid +flowchart LR + A[".sdXXX file
(YAML on disk)"] + B["Asset class
Asset subclass"] + C["Compiler
AssetCompilerBase"] + D["Content database
(binary)"] + E["Runtime type
ContentManager.Load"] + + A -->|"deserialized into"| B + B -->|"transformed by"| C + C -->|"writes to"| D + D -->|"loaded into"| E +``` + +**Design time:** The author edits asset properties in GameStudio. Properties are persisted to a +`.sdXXX` YAML file. The file is deserialized into an instance of the asset class. + +**Build time:** The build pipeline invokes the compiler registered for the asset type. The +compiler reads the asset class instance and produces compiled binary content, which is written +to the content database. + +**Runtime:** The game calls `ContentManager.Load(url)`. The engine reads the +compiled binary and returns a typed instance. + +## Assembly Map + +| Layer | Key Type | Assembly | Location | +|---|---|---|---| +| Runtime type | `DataContract`-decorated class | Engine feature assembly (e.g. `Stride.Graphics`) | `sources/engine/` | +| Asset class | `Asset` subclass | `Stride.Assets` or feature assembly (e.g. `Stride.Assets.Models`) | `sources/engine/` | +| Compiler | `AssetCompilerBase` subclass | Same assembly as the asset class | `sources/engine/` (same folder as asset class) | +| Editor Tier 2 | `AssetViewModel` subclass | `Stride.Assets.Presentation` | `sources/editor/Stride.Assets.Presentation/ViewModel/` | +| Editor Tier 3 VM | `AssetEditorViewModel` subclass | `Stride.Assets.Presentation` | `sources/editor/Stride.Assets.Presentation/AssetEditors/` | +| Editor Tier 3 View | `IEditorView` XAML control | `Stride.Assets.Presentation` | `sources/editor/Stride.Assets.Presentation/AssetEditors/` | +| Template | `.sdtpl` YAML file | `Stride.Assets.Presentation` | `sources/editor/Stride.Assets.Presentation/Templates/Assets/` | + +## Choose a Base Class for the Asset + +> **Decision tree:** +> +> - Does the asset import its primary content from an external file (`.fbx`, `.png`, `.wav`...)? +> → **`AssetWithSource`** (provides a `Source: UFile` property) +> - Does the asset have named sub-parts with parent/child hierarchy (like scenes or prefabs)? +> → **`AssetCompositeHierarchy`** +> - Does the asset have named sub-parts in a flat, unordered collection? +> → **`AssetComposite`** +> - Otherwise → **`Asset`** + +See [asset-class.md](asset-class.md) for the full base-class selection table. + +## Choose an Editor Tier + +> **Decision tree:** +> +> - Does the asset need a dedicated editor panel (canvas, node graph, timeline...)? +> → **Tier 3** — custom `AssetEditorViewModel` + XAML view +> - Does the asset need custom commands, computed display properties, or custom drag-and-drop? +> → **Tier 2** — custom `AssetViewModel` +> - Otherwise the property grid is sufficient. +> → **Tier 1** — no editor code needed + +See [editor.md](editor.md) for implementation details for each tier. + +## Implementation Checklist + +Use this checklist when adding a new asset type. Steps marked **optional** are only needed +in specific circumstances. + +### Always required + +- [ ] **Runtime type** — `[DataContract]`, `[ContentSerializer]`, `[ReferenceSerializer]`, + `[DataSerializerGlobal]` → see [runtime-type.md](runtime-type.md) +- [ ] **Asset class** — `[DataContract]`, `[AssetDescription]`; add `[AssetContentType]` and + `[AssetFormatVersion]` when the asset has a compiled runtime output → see [asset-class.md](asset-class.md) +- [ ] **Compiler** — `[AssetCompiler(typeof(%%Asset%%), typeof(AssetCompilationContext))]` on + the compiler class; implement the protected `Prepare` override → see [compiler.md](compiler.md) +- [ ] **Module initializer** — `AssemblyRegistry.Register(...)` in the assembly's `Module.cs` + → see [registration.md](registration.md) + +### Recommended + +- [ ] **`.sdtpl` template** — adds the asset to the **Add Asset** menu in GameStudio → + see [registration.md](registration.md#sdtpl-template-files-new-asset-menu) + +### Conditional + +- [ ] **`AssetViewModel` (Tier 2)** — only if custom editor commands or display logic is + needed → see [editor.md](editor.md#tier-2-custom-assetviewmodelt) +- [ ] **`AssetEditorViewModel` + XAML View (Tier 3)** — only if a dedicated editor panel is + needed → see [editor.md](editor.md#tier-3-full-custom-editor) +- [ ] **`GetInputFiles` override** — only if the compiler reads external files → + see [compiler.md](compiler.md#declare-external-file-dependencies-getinputfiles) +- [ ] **`GetInputTypes` override** — only if compilation depends on another asset being + compiled first → see [compiler.md](compiler.md#declare-asset-dependencies-getinputtypes) +- [ ] **Version upgrader** — only when bumping `[AssetFormatVersion]` on an existing asset → + see [asset-class.md](asset-class.md#versioning-and-upgraders) + +## Key Terms + +| Term | Definition | +|---|---| +| Asset class | C# class inheriting `Asset`. Design-time representation. Serialized to a `.sdXXX` YAML file. | +| Runtime type | C# class loaded by `ContentManager` at runtime. Compiled binary form of an asset. | +| `AssetItem` | Wrapper pairing an `Asset` instance with its on-disk location and owning package. | +| Content database | Binary storage produced by the build pipeline and consumed by `ContentManager`. | +| `AssetRegistry` | Static registry mapping asset types to file extensions, compilers, runtime types, and factories. Populated automatically from attributes during assembly registration. | +| Quantum | Stride's introspection framework (`Stride.Core.Quantum`). Builds a node graph from asset properties for use in the property grid and undo/redo. See also the [asset introspection doc](https://doc.stride3d.net/latest/en/manual/engine/asset-introspection.html). | +| `.sdXXX` | The file extension used by Stride asset files. Format is YAML. The extension is registered via `[AssetDescription]`. | +| `AssetCompilationContext` | The standard compilation context for game assets. Pass as the second argument to `[AssetCompiler]`. | From 22cf980a02f6b1264f9822a5175c58ddf3d2e044 Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Sun, 5 Apr 2026 11:59:17 +0200 Subject: [PATCH 7/7] chore(docs): rename overview.md to README.md --- docs/contributors/asset-system/{overview.md => README.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/contributors/asset-system/{overview.md => README.md} (100%) diff --git a/docs/contributors/asset-system/overview.md b/docs/contributors/asset-system/README.md similarity index 100% rename from docs/contributors/asset-system/overview.md rename to docs/contributors/asset-system/README.md