@ -128,8 +128,7 @@ namespace AZ
return result ;
}
bool SliceConverter : : ConvertSliceFile (
AZ : : SerializeContext * serializeContext , const AZStd : : string & slicePath , bool isDryRun )
bool SliceConverter : : ConvertSliceFile ( AZ : : SerializeContext * serializeContext , const AZStd : : string & slicePath , bool isDryRun )
{
/* To convert a slice file, we read the input file in via ObjectStream, then use the "class ready" callback to convert
* the data in memory to a Prefab .
@ -177,11 +176,23 @@ namespace AZ
}
AZ : : Entity * rootEntity = reinterpret_cast < AZ : : Entity * > ( classPtr ) ;
return ConvertSliceToPrefab ( context , outputPath , isDryRun , rootEntity ) ;
bool convertResult = ConvertSliceToPrefab ( context , outputPath , isDryRun , rootEntity ) ;
// Delete the root entity pointer. Otherwise, it will leak itself along with all of the slice asset references held
// within it.
delete rootEntity ;
return convertResult ;
} ;
// Read in the slice file and call the callback on completion to convert the read-in slice to a prefab.
if ( ! Utilities : : InspectSerializedFile ( inputPath . c_str ( ) , serializeContext , callback ) )
// This will also load dependent slice assets, but no other dependent asset types.
// Since we're not actually initializing any of the entities, we don't need any of the non-slice assets to be loaded.
if ( ! Utilities : : InspectSerializedFile (
inputPath . c_str ( ) , serializeContext , callback ,
[ ] ( const AZ : : Data : : AssetFilterInfo & filterInfo )
{
return ( filterInfo . m_assetType = = azrtti_typeid < AZ : : SliceAsset > ( ) ) ;
} ) )
{
AZ_Warning ( " Convert-Slice " , false , " Failed to load '%s'. File may not contain an object stream. " , inputPath . c_str ( ) ) ;
result = false ;
@ -219,8 +230,12 @@ namespace AZ
return false ;
}
// Get all of the entities from the slice.
// Get all of the entities from the slice. We're taking ownership of them, so we also remove them from the slice component
// without deleting them.
constexpr bool deleteEntities = false ;
constexpr bool removeEmptyInstances = true ;
SliceComponent : : EntityList sliceEntities = sliceComponent - > GetNewEntities ( ) ;
sliceComponent - > RemoveAllEntities ( deleteEntities , removeEmptyInstances ) ;
AZ_Printf ( " Convert-Slice " , " Slice contains %zu entities. \n " , sliceEntities . size ( ) ) ;
// Create the Prefab with the entities from the slice.
@ -256,13 +271,19 @@ namespace AZ
for ( auto & alias : entityAliases )
{
auto id = sourceInstance - > GetEntityId ( alias ) ;
auto result = m_aliasIdMapper . emplace ( TemplateEntityIdPair( templateId , id ) , alias ) ;
auto result = m_aliasIdMapper . emplace ( id, SliceEntityMappingInfo ( templateId , alias ) ) ;
if ( ! result . second )
{
AZ_Printf ( " Convert-Slice " , " Duplicate entity alias -> entity id entries found, conversion may not be successful. \n " ) ;
}
}
// Save off a mapping of the slice's metadata entity ID as well, even though we never converted the entity itself.
// This will help us better detect entity ID mapping errors for nested slice instances.
AZ : : Entity * metadataEntity = sliceComponent - > GetMetadataEntity ( ) ;
constexpr bool isMetadataEntity = true ;
m_aliasIdMapper . emplace ( metadataEntity - > GetId ( ) , SliceEntityMappingInfo ( templateId , " MetadataEntity " , isMetadataEntity ) ) ;
// Update the prefab template with the fixed-up data in our prefab instance.
AzToolsFramework : : Prefab : : PrefabDom prefabDom ;
bool storeResult = AzToolsFramework : : Prefab : : PrefabDomUtils : : StoreInstanceInPrefabDom ( * sourceInstance , prefabDom ) ;
@ -342,10 +363,9 @@ namespace AZ
// For each nested slice, convert it.
for ( auto & slice : sliceList )
{
// Get the nested slice asset
// Get the nested slice asset . These should already be preloaded due to loading the root asset.
auto sliceAsset = slice . GetSliceAsset ( ) ;
sliceAsset . QueueLoad ( ) ;
sliceAsset . BlockUntilLoadComplete ( ) ;
AZ_Assert ( sliceAsset . IsReady ( ) , " slice asset hasn't been loaded yet! " ) ;
// The slice list gives us asset IDs, and we need to get to the source path. So first we get the asset path from the ID,
// then we get the source path from the asset path.
@ -393,6 +413,21 @@ namespace AZ
" Convert-Slice " , " Attaching %zu instances of nested slice '%s'. \n " , instances . size ( ) ,
nestedPrefabPath . Native ( ) . c_str ( ) ) ;
// Before processing any further, save off all the known entity IDs from all the instances and how they map back to
// the base nested prefab that they've come from (i.e. this one). As we proceed up the chain of nesting, this will
// build out a hierarchical list of owning instances for each entity that we can trace upwards to know where to add
// the entity into our nested prefab instance.
// This step needs to occur *before* converting the instances themselves, because while converting instances, they
// might have entity ID references that point to other instances. By having the full instance entity ID map in place
// before conversion, we'll be able to fix them up appropriately.
for ( auto & instance : instances )
{
AZStd : : string instanceAlias = GetInstanceAlias ( instance ) ;
UpdateSliceEntityInstanceMappings ( instance . GetEntityIdToBaseMap ( ) , instanceAlias ) ;
}
// Now that we have all the entity ID mappings, convert all the instances.
for ( auto & instance : instances )
{
bool instanceConvertResult = ConvertSliceInstance ( instance , sliceAsset , nestedTemplate , sourceInstance ) ;
@ -406,6 +441,28 @@ namespace AZ
return true ;
}
AZStd : : string SliceConverter : : GetInstanceAlias ( const AZ : : SliceComponent : : SliceInstance & instance )
{
// When creating the new instance, we would like to have deterministic instance aliases. Prefabs that depend on this one
// will have patches that reference the alias, so if we reconvert this slice a second time, we would like it to produce
// the same results. To get a deterministic and unique alias, we rely on the slice instance. The slice instance contains
// a map of slice entity IDs to unique instance entity IDs. We'll just consistently use the first entry in the map as the
// unique instance ID.
AZStd : : string instanceAlias ;
auto entityIdMap = instance . GetEntityIdMap ( ) ;
if ( ! entityIdMap . empty ( ) )
{
instanceAlias = AZStd : : string : : format ( " Instance_%s " , entityIdMap . begin ( ) - > second . ToString ( ) . c_str ( ) ) ;
}
else
{
AZ_Error ( " Convert-Slice " , false , " Couldn't create deterministic instance alias. " ) ;
instanceAlias = AZStd : : string : : format ( " Instance_%s " , AZ : : Entity : : MakeId ( ) . ToString ( ) . c_str ( ) ) ;
}
return instanceAlias ;
}
bool SliceConverter : : ConvertSliceInstance (
AZ : : SliceComponent : : SliceInstance & instance ,
AZ : : Data : : Asset < AZ : : SliceAsset > & sliceAsset ,
@ -429,6 +486,8 @@ namespace AZ
auto instanceToTemplateInterface = AZ : : Interface < AzToolsFramework : : Prefab : : InstanceToTemplateInterface > : : Get ( ) ;
auto prefabSystemComponentInterface = AZ : : Interface < AzToolsFramework : : Prefab : : PrefabSystemComponentInterface > : : Get ( ) ;
AZStd : : string instanceAlias = GetInstanceAlias ( instance ) ;
// Create a new unmodified prefab Instance for the nested slice instance.
auto nestedInstance = AZStd : : make_unique < AzToolsFramework : : Prefab : : Instance > ( ) ;
AzToolsFramework : : Prefab : : Instance : : EntityList newEntities ;
@ -465,15 +524,62 @@ namespace AZ
auto instantiated =
dataPatch . Apply ( & sourceObjects , dependentSlice - > GetSerializeContext ( ) , filterDesc , sourceDataFlags , targetDataFlags ) ;
// Run through all the instantiated entities and fix up their parent hierarchy:
// Replace all the entities in the instance with the new patched ones. To do this, we'll remove all existing entities
// throughout the entire nested hierarchy, then add the new patched entities back in at the appropriate place in the hierarchy.
// (This is easier than trying to figure out what the patched data changes are - we can let the JSON patch handle it for us)
nestedInstance - > RemoveNestedEntities (
[ ] ( const AZStd : : unique_ptr < AZ : : Entity > & )
{
return true ;
} ) ;
AZStd : : vector < AZStd : : pair < AZ : : Entity * , AzToolsFramework : : Prefab : : Instance * > > addedEntityList ;
for ( auto & entity : instantiated - > m_entities )
{
auto entityEntry = m_aliasIdMapper . find ( entity - > GetId ( ) ) ;
if ( entityEntry ! = m_aliasIdMapper . end ( ) )
{
auto & mappingStruct = entityEntry - > second ;
// Starting with the current nested instance, walk downwards through the nesting hierarchy until we're at the
// correct level for this instanced entity ID, then add it. Because we're adding it with the non-instanced alias,
// it doesn't matter what the slice's instanced entity ID is, and the JSON patch will correctly pick up the changes
// we've made for this instance.
AzToolsFramework : : Prefab : : Instance * addingInstance = nestedInstance . get ( ) ;
for ( auto it = mappingStruct . m_nestedInstanceAliases . rbegin ( ) ; it ! = mappingStruct . m_nestedInstanceAliases . rend ( ) ; it + + )
{
auto foundInstance = addingInstance - > FindNestedInstance ( * it ) ;
if ( foundInstance . has_value ( ) )
{
addingInstance = & ( foundInstance - > get ( ) ) ;
}
else
{
AZ_Assert ( false , " Couldn't find nested instance %s " , it - > c_str ( ) ) ;
}
}
addingInstance - > AddEntity ( * entity , mappingStruct . m_entityAlias ) ;
addedEntityList . emplace_back ( entity , addingInstance ) ;
}
else
{
AZ_Assert ( false , " Failed to find entity alias. " ) ;
nestedInstance - > AddEntity ( * entity ) ;
addedEntityList . emplace_back ( entity , nestedInstance . get ( ) ) ;
}
}
for ( auto & [ entity , addingInstance ] : addedEntityList )
{
// Fix up the parent hierarchy:
// - Invalid parents need to get set to the container.
// - Valid parents into the top-level instance mean that the nested slice instance is also child-nested under an entity.
// Prefabs handle this type of nesting differently - we need to set the parent to the container, and the container's
// parent to that other instance.
auto containerEntity = nestedInstance - > GetContainerEntity ( ) ;
auto containerEntity = adding Instance- > GetContainerEntity ( ) ;
auto containerEntityId = containerEntity - > get ( ) . GetId ( ) ;
for ( auto entity : instantiated - > m_entities )
{
AzToolsFramework : : Components : : TransformComponent * transformComponent =
entity - > FindComponent < AzToolsFramework : : Components : : TransformComponent > ( ) ;
if ( transformComponent )
@ -482,70 +588,76 @@ namespace AZ
auto parentId = transformComponent - > GetParentId ( ) ;
if ( parentId . IsValid ( ) )
{
auto parentAlias = m_aliasIdMapper . find ( TemplateEntityIdPair ( topLevelInstance - > GetTemplateId ( ) , parentId ) ) ;
if ( parentAlias ! = m_aliasIdMapper . end ( ) )
// Look to see if the parent ID exists in the same instance (i.e. an entity in the nested slice is a
// child of an entity in the containing slice). If this case exists, we need to adjust the parents so that
// the child entity connects to the prefab container, and the *container* is the child of the entity in the
// containing slice. (i.e. go from A->B to A->container->B)
auto parentEntry = m_aliasIdMapper . find ( parentId ) ;
if ( parentEntry ! = m_aliasIdMapper . end ( ) )
{
// Set the container's parent to this entity's parent, and set this entity's parent to the container
// (i.e. go from A->B to A->container->B)
auto newParentId = topLevelInstance - > GetEntityId ( parentAlias - > second ) ;
SetParentEntity ( containerEntity - > get ( ) , newParentId , false ) ;
onlySetIfInvalid = false ;
}
auto & parentMappingInfo = parentEntry - > second ;
if ( parentMappingInfo . m_templateId ! = addingInstance - > GetTemplateId ( ) )
{
if ( topLevelInstance - > GetTemplateId ( ) = = parentMappingInfo . m_templateId )
{
parentId = topLevelInstance - > GetEntityId ( parentMappingInfo . m_entityAlias ) ;
}
else
{
AzToolsFramework : : Prefab : : Instance * parentInstance = addingInstance ;
SetParentEntity ( * entity , containerEntityId , onlySetIfInvalid ) ;
}
while ( ( parentInstance - > GetParentInstance ( ) . has_value ( ) ) & &
( parentInstance - > GetTemplateId ( ) ! = parentMappingInfo . m_templateId ) )
{
parentInstance = & ( parentInstance - > GetParentInstance ( ) - > get ( ) ) ;
}
// Replace all the entities in the instance with the new patched ones.
// (This is easier than trying to figure out what the patched data changes are - we can let the JSON patch handle it for us)
nestedInstance - > RemoveNestedEntities (
[ ] ( const AZStd : : unique_ptr < AZ : : Entity > & )
{
return true ;
} ) ;
for ( auto & entity : instantiated - > m_entities )
if ( parentInstance - > GetTemplateId ( ) = = parentMappingInfo . m_templateId )
{
auto entityAlias = m_aliasIdMapper . find ( TemplateEntityIdPair ( nestedInstance - > GetTemplateId ( ) , entity - > GetId ( ) ) ) ;
if ( entityAlias ! = m_aliasIdMapper . end ( ) )
{
nestedInstance - > AddEntity ( * entity , entityAlias - > second ) ;
parentId = parentInstance - > GetEntityId ( parentMappingInfo . m_entityAlias ) ;
}
else
{
AZ_Assert ( false , " Failed to find entity alias. " ) ;
nestedInstance - > AddEntity ( * entity ) ;
AZ_Assert ( false , " Could not find parent instance " ) ;
}
}
// Set the container's parent to this entity's parent, and set this entity's parent to the container
// auto newParentId = topLevelInstance->GetEntityId(parentMappingInfo.m_entityAlias);
SetParentEntity ( containerEntity - > get ( ) , parentId , false ) ;
onlySetIfInvalid = false ;
}
}
// If the parent ID is valid, but NOT in the top-level instance, then it's just a nested hierarchy inside
// the slice and we don't need to adjust anything. "onlySetIfInvalid" will still be true, which means we
// won't change the parent ID below.
}
SetParentEntity ( * entity , containerEntityId , onlySetIfInvalid ) ;
}
}
// Set the container entity of the nested prefab to have the top-level prefab as the parent if it hasn't already gotten
// another entity as its parent.
{
auto containerEntity = nestedInstance - > GetContainerEntity ( ) ;
constexpr bool onlySetIfInvalid = true ;
SetParentEntity ( containerEntity - > get ( ) , topLevelInstance - > GetContainerEntityId ( ) , onlySetIfInvalid ) ;
}
// After doing all of the above, run through entity references in any of the patched entities, and fix up the entity IDs to
// match the new ones in our prefabs.
RemapIdReferences ( m_aliasIdMapper , topLevelInstance , nestedInstance . get ( ) , instantiated , dependentSlice - > GetSerializeContext ( ) ) ;
// Add the nested instance itself to the top-level prefab. To do this, we need to add it to our top-level instance,
// create a patch out of it, and patch the top-level prefab template.
AzToolsFramework : : Prefab : : PrefabDom topLevelInstanceDomBefore ;
instanceToTemplateInterface - > GenerateDomForInstance ( topLevelInstanceDomBefore , * topLevelInstance ) ;
// When creating the new instance, we would like to have deterministic instance aliases. Prefabs that depend on this one
// will have patches that reference the alias, so if we reconvert this slice a second time, we would like it to produce
// the same results. To get a deterministic and unique alias, we rely on the slice instance. The slice instance contains
// a map of slice entity IDs to unique instance entity IDs. We'll just consistently use the first entry in the map as the
// unique instance ID.
AZStd : : string instanceAlias ;
auto entityIdMap = instance . GetEntityIdMap ( ) ;
if ( ! entityIdMap . empty ( ) )
{
instanceAlias = AZStd : : string : : format ( " Instance_%s " , entityIdMap . begin ( ) - > second . ToString ( ) . c_str ( ) ) ;
}
else
{
instanceAlias = AZStd : : string : : format ( " Instance_%s " , AZ : : Entity : : MakeId ( ) . ToString ( ) . c_str ( ) ) ;
}
// Use the deterministic instance alias for this new instance
AzToolsFramework : : Prefab : : Instance & addedInstance = topLevelInstance - > AddInstance ( AZStd : : move ( nestedInstance ) , instanceAlias ) ;
AzToolsFramework : : Prefab : : PrefabDom topLevelInstanceDomAfter ;
@ -670,5 +782,136 @@ namespace AZ
AZ_Error ( " Convert-Slice " , disconnected , " Asset Processor failed to disconnect successfully. " ) ;
}
void SliceConverter : : UpdateSliceEntityInstanceMappings (
const AZ : : SliceComponent : : EntityIdToEntityIdMap & sliceEntityIdMap , const AZStd : : string & currentInstanceAlias )
{
// For each instanced entity, map its ID all the way back to the original prefab template and entity ID that it came from.
// This counts on being run recursively from the leaf nodes upwards, so we first get B->A,
// then C->B which becomes a C->A entry, then D->C which becomes D->A, etc.
for ( auto & [ newId , oldId ] : sliceEntityIdMap )
{
// Try to find the conversion chain from the old ID. if it's there, copy it and use it for the new ID, plus add this
// instance's name to the end of the chain. If it's not there, skip it, since it's probably the slice metadata entity,
// which we didn't convert.
auto parentEntry = m_aliasIdMapper . find ( oldId ) ;
if ( parentEntry ! = m_aliasIdMapper . end ( ) )
{
// Only add this instance's name if we don't already have an entry for the new ID.
if ( m_aliasIdMapper . find ( newId ) = = m_aliasIdMapper . end ( ) )
{
auto newMappingEntry = m_aliasIdMapper . emplace ( newId , parentEntry - > second ) . first ;
newMappingEntry - > second . m_nestedInstanceAliases . emplace_back ( currentInstanceAlias ) ;
}
else
{
// If we already had an entry for the new ID, it might be because the old and new ID are the same. This happens
// when nesting multiple prefabs directly underneath each other without a nesting entity in-between.
// If the IDs are different, it's an unexpected error condition.
AZ_Assert ( oldId = = newId , " The same entity instance ID has unexpectedly appeared twice in the same nested prefab. " ) ;
}
}
else
{
AZ_Warning ( " Convert-Slice " , false , " Couldn't find an entity ID conversion for %s. " , oldId . ToString ( ) . c_str ( ) ) ;
}
}
}
void SliceConverter : : RemapIdReferences (
const AZStd : : unordered_map < AZ : : EntityId , SliceEntityMappingInfo > & idMapper ,
AzToolsFramework : : Prefab : : Instance * topLevelInstance ,
AzToolsFramework : : Prefab : : Instance * nestedInstance ,
SliceComponent : : InstantiatedContainer * instantiatedEntities ,
SerializeContext * context )
{
// Given a set of instantiated entities, run through all of them, look for entity references, and replace the entity IDs with
// new ones that match up with our prefabs.
IdUtils : : Remapper < EntityId > : : ReplaceIdsAndIdRefs (
instantiatedEntities ,
[ idMapper , & topLevelInstance , & nestedInstance ] (
const EntityId & sourceId , bool isEntityId , [[maybe_unused]] const AZStd : : function < EntityId ( ) > & idGenerator ) - > EntityId
{
EntityId newId = sourceId ;
// Only convert valid entity references. Actual entity IDs have already been taken care of elsewhere, so ignore them.
if ( ! isEntityId & & sourceId . IsValid ( ) )
{
auto entityEntry = idMapper . find ( sourceId ) ;
// Since we've already remapped transform hierarchies to include container entities, it's possible that our entity
// reference is pointing to a container, which means it won't be in our slice mapping table. In that case, just
// return it as-is.
if ( entityEntry = = idMapper . end ( ) )
{
return sourceId ;
}
// We've got a slice->prefab mapping entry, so now we need to use it.
auto & mappingStruct = entityEntry - > second ;
if ( mappingStruct . m_nestedInstanceAliases . empty ( ) )
{
// If we don't have a chain of nested instance aliases, then this entity reference is either within the
// current nested instance or it's pointing to an entity in the top-level instance. We'll try them both
// to look for a match.
EntityId prefabId = nestedInstance - > GetEntityId ( mappingStruct . m_entityAlias ) ;
if ( ! prefabId . IsValid ( ) )
{
prefabId = topLevelInstance - > GetEntityId ( mappingStruct . m_entityAlias ) ;
}
if ( prefabId . IsValid ( ) )
{
newId = prefabId ;
}
else
{
AZ_Error ( " Convert-Slice " , false , " Couldn't find source ID %s " , sourceId . ToString ( ) . c_str ( ) ) ;
}
}
else
{
// We *do* have a chain of nested instance aliases. This chain could either be relative to the nested instance
// or the top-level instance. We can tell which one it is by which one can find the first nested instance
// alias.
AzToolsFramework : : Prefab : : Instance * entityInstance = nestedInstance ;
auto it = mappingStruct . m_nestedInstanceAliases . rbegin ( ) ;
if ( ! entityInstance - > FindNestedInstance ( * it ) . has_value ( ) )
{
entityInstance = topLevelInstance ;
}
// Now that we've got a starting point, iterate through the chain of nested instance aliases to find the
// correct instance to get the entity ID for. We have to go from slice IDs -> entity aliases -> entity IDs
// because prefab instance creation can change some of our entity IDs along the way.
for ( ; it ! = mappingStruct . m_nestedInstanceAliases . rend ( ) ; it + + )
{
auto foundInstance = entityInstance - > FindNestedInstance ( * it ) ;
if ( foundInstance . has_value ( ) )
{
entityInstance = & ( foundInstance - > get ( ) ) ;
}
else
{
AZ_Assert ( false , " Couldn't find nested instance %s " , it - > c_str ( ) ) ;
}
}
EntityId prefabId = entityInstance - > GetEntityId ( mappingStruct . m_entityAlias ) ;
if ( prefabId . IsValid ( ) )
{
newId = prefabId ;
}
}
}
return newId ;
} ,
context ) ;
}
} // namespace SerializeContextTools
} // namespace AZ