Fix error when applying prefab changes with missing (deleted) nested prefabs

#3244
This commit is contained in:
Wojtek Figat
2025-02-26 17:46:22 +01:00
parent 33dd47c169
commit 2d956ebb36
8 changed files with 136 additions and 24 deletions

View File

@@ -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;

View File

@@ -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());

View File

@@ -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);

View File

@@ -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);
}
}
}
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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.

View File

@@ -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);
}
}