Skip to content

Instantly share code, notes, and snippets.

@Eideren
Last active March 13, 2023 02:20
Show Gist options
  • Save Eideren/0aa59644eb7c5b029d04b7fa5a285e62 to your computer and use it in GitHub Desktop.
Save Eideren/0aa59644eb7c5b029d04b7fa5a285e62 to your computer and use it in GitHub Desktop.
A small class to export/save a scene or a group of entities within a running stride game
namespace Project
{
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using Stride.Core;
using Stride.Core.Annotations;
using Stride.Core.Mathematics;
using Stride.Core.Yaml;
using Stride.Core.Yaml.Serialization;
using Stride.Engine;
/// <summary>
/// This class requires NuGet 'Stride.Core.Packages', 'Stride.Core.BuildEngine.Common',
/// 'Stride.Core.Assets', 'Stride.Core.Design' and 'Stride.Core.Yaml'.
/// Add them as 'PackageReference' to your project.
/// </summary>
public class StrideExporter
{
/// <summary>
/// Serializes <paramref name="rootEntities"/> into stride's scene (sdscene) format which can be read by the editor.
/// </summary>
/// <param name="stream">The output stream, a file stream pointing to a *.sdscene file for example</param>
/// <param name="rootEntities">The entities at the root of the scene</param>
public static void AsYamlScene( Stream stream, List<Entity> rootEntities )
{
var sceneProxy = new SceneAssetProxy();
var root = sceneProxy.Hierarchy.RootParts;
var all = sceneProxy.Hierarchy.Parts;
root.AddRange( rootEntities );
foreach( Entity e in root )
all.Add( new Wrapper { Entity = e } );
// Collect all entities
for( int i = 0; i < all.Count; i++ )
{
var e = all[ i ].Entity;
foreach( var child in e.Transform.Children )
all.Add( new Wrapper { Entity = child.Entity } );
}
// Ensure ids are unique
var ogIdMapping = new Dictionary<IIdentifiable, Guid>();
try
{
var uniqueIdTest = new HashSet<Guid>();
foreach( var wrapper in all )
{
var e = wrapper.Entity;
ogIdMapping.Add( e, e.Id );
while( uniqueIdTest.Contains( e.Id ) )
e.Id = Guid.NewGuid();
uniqueIdTest.Add( e.Id );
foreach( var comp in wrapper.Entity )
{
ogIdMapping.Add( comp, comp.Id );
while( uniqueIdTest.Contains( comp.Id ) )
comp.Id = Guid.NewGuid();
uniqueIdTest.Add( comp.Id );
}
}
using( MemoryStream memStream = new MemoryStream() )
{
AssetYamlSerializer.Default.Serialize( memStream, sceneProxy );
memStream.Flush();
memStream.Seek( 0, SeekOrigin.Begin );
var yaml = new YamlStream();
using( var reader = new StreamReader( memStream ) )
yaml.Load( reader );
var rootNode = yaml.Documents[ 0 ].RootNode;
rootNode.Tag = "!SceneAsset";
// Serialization does not create refs, we're taking care of that here
MapRefs( ref rootNode, new Stack<(YamlNode, YamlNode)>(), new HashSet<Guid>() );
using( var writer = new StreamWriter( stream ) )
yaml.Save( writer );
}
}
finally
{
// Reset Ids to previous, potentially not unique, value
foreach( var kvp in ogIdMapping )
kvp.Key.Id = kvp.Value;
}
}
static bool MapRefs( ref YamlNode n, Stack<(YamlNode, YamlNode)> stack, HashSet<Guid> refs )
{
if( n is YamlMappingNode m )
{
for( int i = 0; i < m.Children.Count; i++ )
{
var kvp = m.Children[ i ];
// This node has an Id, it most likely is an IIdentifiable,
// see if we should transform this node from an instance into a ref instead
// this part might create bugs if one of your components has an 'Id' parameter with a valid Guid,
// not sure how to work around that without introducing lots of garbage
if( kvp.Key is YamlScalarNode ysn
&& ysn.Value == "Id"
&& kvp.Value is YamlScalarNode ysn2
&& Guid.TryParse( ysn2.Value, out var guid ) )
{
if( refs.Contains( guid ) || IsTransformCompChild( stack ) || IsRoot( stack ) )
{
n = new YamlScalarNode( $"ref!! {guid}" );
return true;
}
else
{
refs.Add( guid );
}
}
var k = kvp.Key;
var v = kvp.Value;
stack.Push( ( k, v ) );
if( MapRefs( ref k, stack, refs ) )
m.Children[ i ] = new KeyValuePair<YamlNode, YamlNode>( k, v );
if( MapRefs( ref v, stack, refs ) )
m.Children[ i ] = new KeyValuePair<YamlNode, YamlNode>( k, v );
stack.Pop();
}
}
else if( n is YamlSequenceNode ysq )
{
for( int i = 0; i < ysq.Children.Count; i++ )
{
stack.Push( ( new YamlScalarNode { Value = "-" }, null ) );
var k = ysq.Children[ i ];
if( MapRefs( ref k, stack, refs ) )
ysq.Children[ i ] = k;
stack.Pop();
}
}
return false;
}
/// <summary>
/// Is stack on a ...Hierarchy.RootParts.-.?
/// </summary>
static bool IsRoot( Stack<(YamlNode left, YamlNode right)> stack )
{
using var e = stack.GetEnumerator();
if( e.MoveNext() && e.Current.left is YamlScalarNode ysn && ysn.Value == "-"
&& e.MoveNext() && e.Current.left is YamlScalarNode ysn2 && ysn2.Value == "RootParts"
&& e.MoveNext() && e.Current.left is YamlScalarNode ysn3 && ysn3.Value == "Hierarchy" )
return true;
return false;
}
/// <summary>
/// Is stack on a ...!TransformComponent.Children.{id}.?
/// </summary>
static bool IsTransformCompChild( Stack<(YamlNode left, YamlNode right)> stack )
{
using var e = stack.GetEnumerator();
if( e.MoveNext() // an id
&& e.MoveNext() && e.Current.left is YamlScalarNode ysn && ysn.Value == "Children"
&& e.MoveNext() && e.Current.right is YamlMappingNode ymn && ymn.Tag == "!TransformComponent" )
return true;
return false;
}
/// <summary>
/// We're not using SceneAsset directly as that namespace is a pain in the ass to use,
/// so we're replicating the way that class is laid out through this very similar class
/// </summary>
[ DataContract( "SceneAssetProxy" ) ]
public class SceneAssetProxy
{
[ DataMember( 0, DataMemberMode.Assign ) ]
public Guid Id = Guid.NewGuid();
[ DataMember( 1, DataMemberMode.Assign ), DataStyle( DataStyle.Compact ), DefaultValue( null ), NonIdentifiableCollectionItems ]
public Dictionary<string, string> SerializedVersion = new Dictionary<string, string> { { "Stride", "3.1.0.1" } };
[ DataMember( 2 ), NonIdentifiableCollectionItems, MemberCollection( NotNullItems = true ) ]
public List<object> Tags = new List<object>();
[ DataMember( 3 ), NonIdentifiableCollectionItems, NotNull ]
public List<Guid> ChildrenIds = new List<Guid>();
[ DataMember( 4 ) ]
public Vector3 Offset;
[ DataMember( 5 ), NotNull ]
public HierarchyProxy Hierarchy = new HierarchyProxy();
[ DataContract( "HierarchyProxy" ) ]
public class HierarchyProxy
{
[ DataMember( 0 ), NonIdentifiableCollectionItems, NotNull ]
public List<Entity> RootParts = new List<Entity>();
[ DataMember( 1 ), NonIdentifiableCollectionItems, ItemNotNull, NotNull ]
public List<Wrapper> Parts = new List<Wrapper>();
}
}
/// <summary>
/// SceneAsset.Hierarchy.Parts does not use Entity directly,
/// we're replicating that behavior through this class
/// </summary>
[ DataContract( "EntityWrapper" ) ]
public class Wrapper
{
public Entity Entity;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment