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
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,14 @@ public static void Reload([NotNull] SessionViewModel session, ILogger log, Actio
// Serialize types from unloaded assemblies as Yaml, and unset them
var unloadingVisitor = new UnloadingVisitor(log, loadedAssembliesSet);
Dictionary<AssetViewModel, List<ItemToReload>> assetItemsToReload;

// 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
Dictionary<string, List<ParsingEvent>> assetsToReload;
try
{
assetsToReload = PrepareUserDefinedAssetsForReloading(session, modifiedAssemblies, log);
assetItemsToReload = PrepareAssemblyReloading(session, unloadingVisitor, session.UndoRedoService);
}
catch (Exception e)
Expand All @@ -74,6 +80,7 @@ public static void Reload([NotNull] SessionViewModel session, ILogger log, Actio
var reloadingVisitor = new ReloadingVisitor(log, loadedAssembliesSet);
try
{
PostAssemblyReloadingForUserDefinedAssets(session, assetsToReload, log);
PostAssemblyReloading(session.UndoRedoService, session.AssetNodeContainer, reloadingVisitor, log, assetItemsToReload);
}
catch (Exception e)
Expand All @@ -91,6 +98,44 @@ public static void Reload([NotNull] SessionViewModel session, ILogger log, Actio
session.ActiveProperties.RefreshSelectedPropertiesAsync().Forget();
}

private static Dictionary<string, List<ParsingEvent>> PrepareUserDefinedAssetsForReloading([NotNull] SessionViewModel session, [NotNull] Dictionary<PackageLoadedAssembly, string> modifiedAssemblies, ILogger log)
{
var output = new Dictionary<string, List<ParsingEvent>>();
foreach (var asset in session.AllAssets)
{
if (modifiedAssemblies.Any(assembly => assembly.Key.Assembly == asset.Asset.GetType().Assembly) == false)
continue;

var obj = asset.AssetItem.Asset;
var settings = new SerializerContextSettings(log);
var parsingEvents = new List<ParsingEvent>();

AssetYamlSerializer.Default.Serialize(new ParsingEventListEmitter(parsingEvents), obj, typeof(Asset), settings);

output.Add(asset.Url, parsingEvents);
}

return output;
}

private static void PostAssemblyReloadingForUserDefinedAssets(SessionViewModel session, Dictionary<string, List<ParsingEvent>> assetsToReload, ILogger log)
{
var settings = new SerializerContextSettings { Logger = log };
foreach (var group in session.AllAssets
.SelectMany(x => x.Dependencies.RecursiveReferencedAssets.Append(x))
.Where(x => assetsToReload.ContainsKey(x.Url))
.GroupBy(x => x.Url))
{
var events = assetsToReload[group.Key];
foreach (var viewModel in group)
{
var eventReader = new EventReader(new MemoryParser(events));
var asset = (Asset)AssetYamlSerializer.Default.Deserialize(eventReader, null, typeof(Asset), out var properties, settings);
viewModel.UpdateAsset(asset, log);
}
}
}

private static Dictionary<AssetViewModel, List<ItemToReload>> PrepareAssemblyReloading(SessionViewModel session, UnloadingVisitor visitor, IUndoRedoService actionService)
{
var assetItemsToReload = new Dictionary<AssetViewModel, List<ItemToReload>>();
Expand Down Expand Up @@ -395,7 +440,7 @@ protected override bool ProcessObject(object obj, Type expectedType)
// Other case, stop on the actual member (since we'll just visit null)
var expectedPath = unloadedObject.Path.Decompose().Last().GetIndex() != null ? unloadedObject.ParentPath : unloadedObject.Path;

if (CurrentPath.Match(expectedPath))
if (CurrentPath.ToString().Equals(expectedPath.ToString(), StringComparison.Ordinal)) // We have to convert to string here instead of using Match() as the members in the path may refer to outdated types
{
var eventReader = new EventReader(new MemoryParser(unloadedObject.ParsingEvents));
var settings = Log != null ? new SerializerContextSettings { Logger = Log } : null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"/>.
Expand Down Expand Up @@ -444,6 +444,38 @@ private void Rename(string newName)
}
}

/// <summary>
/// Replace the internal asset with the one provided, this function expects the two assets to have the same identity
/// </summary>
/// <exception cref="ArgumentException">The asset provided does not have the same identity as the current one</exception>
public void UpdateAsset(Asset newAsset, ILogger loggerResult)
{
if (newAsset.Id != AssetItem.Asset.Id)
throw new ArgumentException("Assets must have the same Id.");

if (newAsset.MainSource != AssetItem.Asset.MainSource)
throw new ArgumentException("Assets must have the same source.");

var newAssetItem = AssetItem.Clone(newAsset: newAsset);

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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
using Stride.Assets.Materials;
using Stride.Assets.Navigation;
using Stride.Assets.Textures;
using Stride.Core.Reflection;
using Stride.Editor.Build;
using Stride.Editor.EditorGame.Game;
using Stride.Graphics;
Expand Down Expand Up @@ -88,6 +89,7 @@ public EditorContentLoader(IDispatcherService gameDispatcher, ILogger logger, As
currentRenderingMode = settingsProvider.CurrentGameSettings.GetOrCreate<EditorSettings>().RenderingMode;
currentColorSpace = settingsProvider.CurrentGameSettings.GetOrCreate<RenderingSettings>().ColorSpace;
currentNavigationGroupsHash = settingsProvider.CurrentGameSettings.GetOrDefault<NavigationSettings>().ComputeGroupsHash();
AssemblyRegistry.AssemblyUnregistered += AssemblyUnregistered;
}

public LoaderReferenceManager Manager { get; }
Expand Down Expand Up @@ -154,6 +156,11 @@ public void BuildAndReloadAsset(AssetId assetId)
Session.Dispatcher.InvokeAsync(() => BuildAndReloadAssets(assetToRebuild.Yield()));
}

private void AssemblyUnregistered(object sender, AssemblyRegisteredEventArgs e)
{
Manager.ClearUserAssetsIn(e.Assembly).Forget();
}

public T GetRuntimeObject<T>(AssetItem assetItem) where T : class
{
if (assetItem == null) throw new ArgumentNullException(nameof(assetItem));
Expand All @@ -180,6 +187,7 @@ void IDisposable.Dispose()

private void Cleanup()
{
AssemblyRegistry.AssemblyUnregistered -= AssemblyUnregistered;
settingsProvider.GameSettingsChanged -= GameSettingsChanged;
Session.AssetPropertiesChanged -= AssetPropertiesChanged;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 readonly record 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);
}
}

Expand All @@ -55,6 +57,34 @@ public LoaderReferenceManager(IDispatcherService gameDispatcher, IEditorContentL
this.loader = loader;
}

public async Task ClearUserAssetsIn(Assembly assembly)
{
// 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 == assembly && member.MemberDescriptor.Type.GetCustomAttribute(typeof(Core.Serialization.Contents.ReferenceSerializerAttribute)) is not null)
{
assets.Add((id, contentId, accessor.ContentNode, accessor.index));
}
}
}
}

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

public async Task RegisterReferencer(AbsoluteId referencerId)
{
gameDispatcher.EnsureAccess();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,14 +277,17 @@ 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 () =>

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)
{
Expand All @@ -295,7 +298,9 @@ 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));

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

Expand Down