Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: User-defined assets support (ScriptableObject like data types and more) #2527

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
17 changes: 17 additions & 0 deletions sources/assets/Stride.Core.Assets/AssetCloner.cs
Original file line number Diff line number Diff line change
@@ -234,6 +234,23 @@ public static object Clone(object asset, AssetClonerFlags flags, HashSet<IIdenti
return newObject;
}

/// <summary>
/// Clones the specified asset using asset serialization.
/// </summary>
/// <param name="asset">The asset.</param>
/// <param name="flags">Flags used to control the cloning process</param>
/// <param name="externalIdentifiable"></param>
/// <returns>A callback to build a clone of the asset.</returns>
public static Func<object> DelayedClone(object asset, AssetClonerFlags flags, HashSet<IIdentifiable> externalIdentifiable)
{
if (asset == null)
{
return () => null;
}
var cloner = new AssetCloner(asset, flags, externalIdentifiable);
return () => cloner.Clone(out _);
}

/// <summary>
/// Clones the specified asset using asset serialization.
/// </summary>
Original file line number Diff line number Diff line change
@@ -182,7 +182,7 @@ protected AssetViewModel([NotNull] AssetViewModelConstructionParameters paramete
[NotNull]
public Asset Asset => AssetItem.Asset;

public AssetPropertyGraph PropertyGraph { get; }
public AssetPropertyGraph PropertyGraph { get; private set; }

/// <summary>
/// Gets the <see cref="ThumbnailData"/> associated to this <see cref="AssetViewModel"/>.
@@ -444,6 +444,23 @@ private void Rename(string newName)
}
}

public void ReplaceAsset(AssetItem newAssetItem, ILogger loggerResult)
{
package.Assets.Remove(AssetItem);
package.Assets.Add(newAssetItem);
AssetItem = newAssetItem;
PropertyGraph?.Dispose();
Session.GraphContainer.UnregisterGraph(assetItem.Id);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's an implicit assumption here that previous asset item Id is equal to the new one. Would be good to verify it explicitly (at least with Debug.Assert).

PropertyGraph = Session.GraphContainer.InitializeAsset(assetItem, loggerResult);
if (PropertyGraph != null)
{
PropertyGraph.BaseContentChanged += BaseContentChanged;
PropertyGraph.Changed += AssetPropertyChanged;
PropertyGraph.ItemChanged += AssetPropertyChanged;
PropertyGraph.Initialize();
}
}

private bool UpdateUrl(Package newPackage, DirectoryBaseViewModel newDirectory, string newName, bool updateParentDirectory = true)
{
if (updatingUrl)
Original file line number Diff line number Diff line change
@@ -3,43 +3,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Stride.Core.Assets;
using Stride.Core;
using Stride.Core.Annotations;
using Stride.Core.Presentation.Services;
using Stride.Core.Quantum;
using Stride.Core.Reflection;

namespace Stride.Editor.EditorGame.ContentLoader
{
public class LoaderReferenceManager
{
private struct ReferenceAccessor
{
private readonly IGraphNode contentNode;
private readonly NodeIndex index;
public readonly IGraphNode ContentNode;
public readonly NodeIndex index;

public ReferenceAccessor(IGraphNode contentNode, NodeIndex index)
{
this.contentNode = contentNode;
this.ContentNode = contentNode;
this.index = index;
}

public void Update(object newValue)
{
if (index == NodeIndex.Empty)
{
((IMemberNode)contentNode).Update(newValue);
((IMemberNode)ContentNode).Update(newValue);
}
else
{
((IObjectNode)contentNode).Update(newValue, index);
((IObjectNode)ContentNode).Update(newValue, index);
}
}

public Task Clear([NotNull] LoaderReferenceManager manager, AbsoluteId referencerId, AssetId contentId)
{
return manager.ClearContentReference(referencerId, contentId, contentNode, index);
return manager.ClearContentReference(referencerId, contentId, ContentNode, index);
}
}

@@ -53,6 +55,35 @@ public LoaderReferenceManager(IDispatcherService gameDispatcher, IEditorContentL
{
this.gameDispatcher = gameDispatcher;
this.loader = loader;
AssemblyRegistry.AssemblyUnregistered += AssemblyUnregistered;
}

private void AssemblyUnregistered(object sender, AssemblyRegisteredEventArgs e)
{
// This method is mostly there for user-defined assets, the nodes for fields and properties pointing to those assets in these collections
// have to be manually purged on assembly reload otherwise ReplaceContent fails as the nodes still use the old assembly type
var assets = new List<(AbsoluteId referencerId, AssetId contentId, IGraphNode contentNode, NodeIndex index)>();
foreach (var (id, referenced) in references)
{
foreach (var (contentId, accessors) in referenced)
{
foreach (var accessor in accessors)
{
if (accessor.ContentNode is IMemberNode member && member.MemberDescriptor.Type.Assembly == e.Assembly && member.MemberDescriptor.Type.GetCustomAttribute(typeof(Core.Serialization.Contents.ReferenceSerializerAttribute)) is not null)
{
assets.Add((id, contentId, accessor.ContentNode, accessor.index));
}
}
}
}

gameDispatcher.InvokeTask(async () =>
{
foreach (var(referencerId, contentId, contentNode, index) in assets)
{
await ClearContentReference(referencerId, contentId, contentNode, index);
}
});
}

public async Task RegisterReferencer(AbsoluteId referencerId)
@@ -72,10 +103,10 @@ public async Task RemoveReferencer(AbsoluteId referencerId)
gameDispatcher.EnsureAccess();
using (await loader.LockDatabaseAsynchronously())
{
if (!references.ContainsKey(referencerId))
Dictionary<AssetId, List<ReferenceAccessor>> referencer;
if (!references.TryGetValue(referencerId, out referencer))
throw new InvalidOperationException("The given referencer is not registered.");

var referencer = references[referencerId];
// Properly clear all reference first
foreach (var content in referencer.ToDictionary(x => x.Key, x => x.Value))
{
@@ -91,13 +122,16 @@ public async Task RemoveReferencer(AbsoluteId referencerId)

public async Task PushContentReference(AbsoluteId referencerId, AssetId contentId, IGraphNode contentNode, NodeIndex index)
{
// Temp while I figure out how best to handle this one
if (contentNode.GetType().Name == "AssetMemberNode" && contentNode.Descriptor.Type.Name.StartsWith("UrlReference"))
return;

gameDispatcher.EnsureAccess();
using (await loader.LockDatabaseAsynchronously())
{
if (!references.ContainsKey(referencerId))
if (!references.TryGetValue(referencerId, out var referencer))
throw new InvalidOperationException("The given referencer is not registered.");

var referencer = references[referencerId];
List<ReferenceAccessor> accessors;
if (!referencer.TryGetValue(contentId, out accessors))
{
@@ -134,14 +168,14 @@ public async Task ClearContentReference(AbsoluteId referencerId, AssetId content
gameDispatcher.EnsureAccess();
using (await loader.LockDatabaseAsynchronously())
{
if (!references.ContainsKey(referencerId))
Dictionary<AssetId, List<ReferenceAccessor>> referencer;
if (!references.TryGetValue(referencerId, out referencer))
throw new InvalidOperationException("The given referencer is not registered.");

var referencer = references[referencerId];
if (!referencer.ContainsKey(contentId))
List<ReferenceAccessor> accessors;
if (!referencer.TryGetValue(contentId, out accessors))
throw new InvalidOperationException("The given content is not registered to the given referencer.");

var accessors = referencer[contentId];
var accessor = new ReferenceAccessor(contentNode, index);
var accesorIndex = accessors.IndexOf(accessor);
if (accesorIndex < 0)
38 changes: 35 additions & 3 deletions sources/editor/Stride.GameStudio/ViewModels/DebuggingViewModel.cs
Original file line number Diff line number Diff line change
@@ -277,14 +277,24 @@ private async Task ReloadAssemblies()
var assemblyToAnalyze = assembliesToReload.Where(x => x.LoadedAssembly?.Assembly != null && x.Project != null).ToDictionary(x => x.Project, x => x.LoadedAssembly.Assembly.FullName);
var logResult = new LoggerResult();
BuildLog.AddLogger(logResult);
GameStudioAssemblyReloader.Reload(Session, logResult, async () =>

// We shouldn't use assets whose types were defined in the previous version of this assembly
// We'll rebuild them using the latest type by serializing them before loading the assembly,
// and deserializing them further below once the new assembly is loaded in
var assemblyAssets = Session.AllAssets
.Where(asset => assembliesToReload.Any(assembly => assembly.LoadedAssembly.Assembly == asset.Asset.GetType().Assembly))
.ToDictionary(asset => asset.Url, asset => AssetCloner.DelayedClone(asset.AssetItem.Asset, AssetClonerFlags.None, null));

GameStudioAssemblyReloader.Reload(Session, logResult,
postReloadAction:async () =>
{
foreach (var assemblyToReload in assemblyToAnalyze)
{
await entityComponentsSorter.AnalyzeProject(Session, assemblyToReload.Key, assemblyTrackingCancellation.Token);
}
UpdateCommands();
}, () =>
},
undoAction:() =>
{
foreach (var assemblyToReload in assembliesToReload)
{
@@ -295,7 +305,29 @@ private async Task ReloadAssemblies()
modifiedAssemblies.Add(assemblyToReload.LoadedAssembly, modifiedAssembly);
}
}
}, assembliesToReload.ToDictionary(x => x.LoadedAssembly, x => x.LoadedAssemblyPath));
},
modifiedAssemblies: assembliesToReload.ToDictionary(x => x.LoadedAssembly, x => x.LoadedAssemblyPath));

foreach (var asset in Session.AllAssets)
{
if (assemblyAssets.TryGetValue(asset.Url, out var cloner))
{
var reloadedAsset = (Asset)cloner();
var remappedAssetItem = asset.AssetItem.Clone(newAsset: reloadedAsset);
asset.ReplaceAsset(remappedAssetItem, logResult);
}

foreach (var reference in asset.Dependencies.RecursiveReferencedAssets)
{
if (assemblyAssets.TryGetValue(reference.Url, out var cloner2))
{
var reloadedAsset = (Asset)cloner2();
var remappedAssetItem = reference.AssetItem.Clone(newAsset: reloadedAsset);
reference.ReplaceAsset(remappedAssetItem, logResult);
}
}
}

Session.AllAssets.ForEach(x => x.PropertyGraph?.RefreshBase());
Session.AllAssets.ForEach(x => x.PropertyGraph?.ReconcileWithBase());