Fix error when applying prefab changes with missing (deleted) nested prefabs
#3244
This commit is contained in:
@@ -69,8 +69,7 @@ namespace FlaxEditor.CustomEditors.Dedicated
|
||||
Values.SetReferenceValue(prefabInstance);
|
||||
|
||||
// Display prefab UI (when displaying object inside Prefab Window then display only nested prefabs)
|
||||
var prefabId = prefab.ID;
|
||||
Editor.GetPrefabNestedObject(ref prefabId, ref prefabObjectId, out var nestedPrefabId, out var nestedPrefabObjectId);
|
||||
prefab.GetNestedObject(ref prefabObjectId, out var nestedPrefabId, out var nestedPrefabObjectId);
|
||||
var nestedPrefab = FlaxEngine.Content.Load<Prefab>(nestedPrefabId);
|
||||
var panel = layout.CustomContainer<UniformGridPanel>();
|
||||
panel.CustomControl.Height = 20.0f;
|
||||
|
||||
@@ -617,21 +617,6 @@ void ManagedEditor::WipeOutLeftoverSceneObjects()
|
||||
ObjectsRemovalService::Flush();
|
||||
}
|
||||
|
||||
void ManagedEditor::GetPrefabNestedObject(const Guid& prefabId, const Guid& prefabObjectId, Guid& outPrefabId, Guid& outPrefabObjectId)
|
||||
{
|
||||
outPrefabId = Guid::Empty;
|
||||
outPrefabObjectId = Guid::Empty;
|
||||
const auto prefab = Content::Load<Prefab>(prefabId);
|
||||
if (!prefab)
|
||||
return;
|
||||
const ISerializable::DeserializeStream** prefabObjectDataPtr = prefab->ObjectsDataCache.TryGet(prefabObjectId);
|
||||
if (!prefabObjectDataPtr)
|
||||
return;
|
||||
const ISerializable::DeserializeStream& prefabObjectData = **prefabObjectDataPtr;
|
||||
JsonTools::GetGuidIfValid(outPrefabId, prefabObjectData, "PrefabID");
|
||||
JsonTools::GetGuidIfValid(outPrefabObjectId, prefabObjectData, "PrefabObjectID");
|
||||
}
|
||||
|
||||
void ManagedEditor::OnEditorAssemblyLoaded(MAssembly* assembly)
|
||||
{
|
||||
ASSERT(!HasManagedInstance());
|
||||
|
||||
@@ -259,7 +259,6 @@ public:
|
||||
API_FUNCTION(Internal) static Array<VisualScriptLocal> GetVisualScriptLocals();
|
||||
API_FUNCTION(Internal) static bool EvaluateVisualScriptLocal(VisualScript* script, API_PARAM(Ref) VisualScriptLocal& local);
|
||||
API_FUNCTION(Internal) static void WipeOutLeftoverSceneObjects();
|
||||
API_FUNCTION(Internal) static void GetPrefabNestedObject(API_PARAM(Ref) const Guid& prefabId, API_PARAM(Ref) const Guid& prefabObjectId, API_PARAM(Out) Guid& outPrefabId, API_PARAM(Out) Guid& outPrefabObjectId);
|
||||
|
||||
private:
|
||||
void OnEditorAssemblyLoaded(MAssembly* assembly);
|
||||
|
||||
@@ -33,9 +33,14 @@ namespace FlaxEditor.Actions
|
||||
// Check if this object comes from another nested prefab (to break link only from the top-level prefab)
|
||||
Item nested;
|
||||
nested.ID = ID;
|
||||
Editor.GetPrefabNestedObject(ref PrefabID, ref PrefabObjectID, out nested.PrefabID, out nested.PrefabObjectID);
|
||||
if (nested.PrefabID != Guid.Empty && nested.PrefabObjectID != Guid.Empty)
|
||||
var prefab = FlaxEngine.Content.Load<Prefab>(PrefabID);
|
||||
if (prefab != null &&
|
||||
prefab.GetNestedObject(ref PrefabObjectID, out nested.PrefabID, out nested.PrefabObjectID) &&
|
||||
nested.PrefabID != Guid.Empty &&
|
||||
nested.PrefabObjectID != Guid.Empty)
|
||||
{
|
||||
nestedPrefabLinks.Add(nested);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -707,7 +707,7 @@ bool Prefab::ApplyAll(Actor* targetActor)
|
||||
for (int32 i = 0; i < nestedPrefabIds.Count(); i++)
|
||||
{
|
||||
const auto nestedPrefab = Content::LoadAsync<Prefab>(nestedPrefabIds[i]);
|
||||
if (nestedPrefab && nestedPrefab != this && (nestedPrefab->Flags & ObjectFlags::WasMarkedToDelete) == ObjectFlags::None)
|
||||
if (nestedPrefab && nestedPrefab != this && EnumHasNoneFlags(nestedPrefab->Flags, ObjectFlags::WasMarkedToDelete))
|
||||
{
|
||||
allPrefabs.Add(nestedPrefab);
|
||||
}
|
||||
@@ -778,6 +778,29 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr
|
||||
for (int32 i = 0; i < targetObjects->Count(); i++)
|
||||
{
|
||||
SceneObject* obj = targetObjects.Value->At(i);
|
||||
|
||||
// Check the whole chain of prefab references to be valid for this object
|
||||
bool brokenPrefab = false;
|
||||
Guid nestedPrefabId = obj->GetPrefabID(), nestedPrefabObjectId = obj->GetPrefabObjectID();
|
||||
while (!brokenPrefab && nestedPrefabId.IsValid() && nestedPrefabObjectId.IsValid())
|
||||
{
|
||||
auto prefab = Content::Load<Prefab>(nestedPrefabId);
|
||||
if (prefab)
|
||||
{
|
||||
prefab->GetNestedObject(nestedPrefabObjectId, nestedPrefabId, nestedPrefabObjectId);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(Warning, "Missing prefab {0}.", nestedPrefabId);
|
||||
brokenPrefab = true;
|
||||
}
|
||||
}
|
||||
if (brokenPrefab)
|
||||
{
|
||||
LOG(Warning, "Broken prefab reference on object {0}. Breaking linkage to inline object inside prefab.", GetObjectName(obj));
|
||||
obj->BreakPrefabLink();
|
||||
}
|
||||
|
||||
writer.SceneObject(obj);
|
||||
}
|
||||
writer.EndArray();
|
||||
@@ -809,7 +832,7 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr
|
||||
SceneObject* obj = targetObjects.Value->At(i);
|
||||
auto data = it->GetObject();
|
||||
|
||||
// Check if object is from that prefab
|
||||
// Check if object is from this prefab
|
||||
if (obj->GetPrefabID() == prefabId)
|
||||
{
|
||||
if (!obj->GetPrefabObjectID().IsValid())
|
||||
@@ -883,7 +906,7 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr
|
||||
{
|
||||
const SceneObject* obj = targetObjects->At(i);
|
||||
|
||||
// Check if object is from that prefab
|
||||
// Check if object is from this prefab
|
||||
if (obj->GetPrefabID() == prefabId)
|
||||
{
|
||||
// Map prefab instance to existing prefab object
|
||||
|
||||
@@ -94,6 +94,24 @@ SceneObject* Prefab::GetDefaultInstance(const Guid& objectId)
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Prefab::GetNestedObject(const Guid& objectId, Guid& outPrefabId, Guid& outObjectId) const
|
||||
{
|
||||
if (WaitForLoaded())
|
||||
return false;
|
||||
bool result = false;
|
||||
Guid result1 = Guid::Empty, result2 = Guid::Empty;
|
||||
const ISerializable::DeserializeStream** prefabObjectDataPtr = ObjectsDataCache.TryGet(objectId);
|
||||
if (prefabObjectDataPtr)
|
||||
{
|
||||
const ISerializable::DeserializeStream& prefabObjectData = **prefabObjectDataPtr;
|
||||
result = JsonTools::GetGuidIfValid(result1, prefabObjectData, "PrefabID") &&
|
||||
JsonTools::GetGuidIfValid(result2, prefabObjectData, "PrefabObjectID");
|
||||
}
|
||||
outPrefabId = result1;
|
||||
outObjectId = result2;
|
||||
return result;
|
||||
}
|
||||
|
||||
void Prefab::DeleteDefaultInstance()
|
||||
{
|
||||
ScopeLock lock(Locker);
|
||||
|
||||
@@ -70,6 +70,15 @@ public:
|
||||
/// <returns>The object of the prefab loaded from the prefab. Contains the default values. It's not added to gameplay but deserialized with postLoad and init event fired.</returns>
|
||||
API_FUNCTION() SceneObject* GetDefaultInstance(API_PARAM(Ref) const Guid& objectId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reference to the other nested prefab for a specific prefab object.
|
||||
/// </summary>
|
||||
/// <param name="objectId">The ID of the object in this prefab.</param>
|
||||
/// <param name="outPrefabId">Result ID of the prefab asset referenced by the given object.</param>
|
||||
/// <param name="outObjectId">Result ID of the prefab object referenced by the given object.</param>
|
||||
/// <returns>True if got valid reference, otherwise false.</returns>
|
||||
API_FUNCTION() bool GetNestedObject(API_PARAM(Ref) const Guid& objectId, API_PARAM(Out) Guid& outPrefabId, API_PARAM(Out) Guid& outObjectId) const;
|
||||
|
||||
#if USE_EDITOR
|
||||
/// <summary>
|
||||
/// Applies the difference from the prefab object instance, saves the changes and synchronizes them with the active instances of the prefab asset.
|
||||
|
||||
@@ -559,7 +559,7 @@ TEST_CASE("Prefabs")
|
||||
Content::DeleteAsset(prefabNested1);
|
||||
Content::DeleteAsset(prefabBase);
|
||||
}
|
||||
SECTION("Test Applying Prefab ChangeTo Object References")
|
||||
SECTION("Test Applying Prefab Change To Object References")
|
||||
{
|
||||
// https://github.com/FlaxEngine/FlaxEngine/issues/3136
|
||||
|
||||
@@ -614,4 +614,78 @@ TEST_CASE("Prefabs")
|
||||
instanceB->DeleteObject();
|
||||
Content::DeleteAsset(prefab);
|
||||
}
|
||||
SECTION("Test Applying Prefab With Missing Nested Prefab")
|
||||
{
|
||||
// https://github.com/FlaxEngine/FlaxEngine/issues/3244
|
||||
|
||||
// Create Prefab B with just root object
|
||||
AssetReference<Prefab> prefabB = Content::CreateVirtualAsset<Prefab>();
|
||||
REQUIRE(prefabB);
|
||||
Guid id;
|
||||
Guid::Parse("25dbe4b0416be0777a6ce59e8788b10f", id);
|
||||
prefabB->ChangeID(id);
|
||||
auto prefabBInit = prefabB->Init(Prefab::TypeName,
|
||||
"["
|
||||
"{"
|
||||
"\"ID\": \"aac6b9644492fbca1a6ab0a7904a557e\","
|
||||
"\"TypeName\": \"FlaxEngine.ExponentialHeightFog\","
|
||||
"\"Name\": \"Prefab B.Root\""
|
||||
"}"
|
||||
"]");
|
||||
REQUIRE(!prefabBInit);
|
||||
|
||||
// Create Prefab A with nested Prefab B attached to the root
|
||||
AssetReference<Prefab> prefabA = Content::CreateVirtualAsset<Prefab>();
|
||||
REQUIRE(prefabA);
|
||||
Guid::Parse("4cb744714f746e31855f41815612d14b", id);
|
||||
prefabA->ChangeID(id);
|
||||
auto prefabAInit = prefabA->Init(Prefab::TypeName,
|
||||
"["
|
||||
"{"
|
||||
"\"ID\": \"244274a04cc60d56a2f024bfeef5772d\","
|
||||
"\"TypeName\": \"FlaxEngine.SpotLight\","
|
||||
"\"Name\": \"Prefab A.Root\""
|
||||
"},"
|
||||
"{"
|
||||
"\"ID\": \"1e51f1094f430733333f8280e78dfcc3\","
|
||||
"\"PrefabID\": \"25dbe4b0416be0777a6ce59e8788b10f\","
|
||||
"\"PrefabObjectID\": \"aac6b9644492fbca1a6ab0a7904a557e\","
|
||||
"\"ParentID\": \"244274a04cc60d56a2f024bfeef5772d\""
|
||||
"}"
|
||||
"]");
|
||||
REQUIRE(!prefabAInit);
|
||||
|
||||
// Spawn test instances of both prefabs
|
||||
ScriptingObjectReference<Actor> instanceA = PrefabManager::SpawnPrefab(prefabA);
|
||||
ScriptingObjectReference<Actor> instanceB = PrefabManager::SpawnPrefab(prefabB);
|
||||
|
||||
// Delete nested prefab
|
||||
Content::DeleteAsset(prefabB);
|
||||
|
||||
// Apply instance A and verify it's fine even tho prefab B doesn't exist anymore
|
||||
bool applyResult = PrefabManager::ApplyAll(instanceA);
|
||||
REQUIRE(!applyResult);
|
||||
|
||||
// Check state of objects
|
||||
REQUIRE(instanceA);
|
||||
REQUIRE(instanceA->Children.Count() == 1);
|
||||
REQUIRE(instanceA->Children[0] != nullptr);
|
||||
REQUIRE(instanceA->Children[0]->Is<ExponentialHeightFog>());
|
||||
REQUIRE(instanceB);
|
||||
REQUIRE(instanceB->Is<ExponentialHeightFog>());
|
||||
|
||||
// Verify if prefab has new data to properly spawn another prefab
|
||||
ScriptingObjectReference<Actor> instanceC = PrefabManager::SpawnPrefab(prefabA);
|
||||
REQUIRE(instanceC);
|
||||
REQUIRE(instanceC->Children.Count() == 1);
|
||||
REQUIRE(instanceC->Children[0] != nullptr);
|
||||
REQUIRE(instanceC->Children[0]->Is<ExponentialHeightFog>());
|
||||
|
||||
// Cleanup
|
||||
instanceA->DeleteObject();
|
||||
instanceB->DeleteObject();
|
||||
instanceC->DeleteObject();
|
||||
Content::DeleteAsset(prefabA);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user