diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
index 9cf21b84d..9cbc3754a 100644
--- a/.github/workflows/cd.yml
+++ b/.github/workflows/cd.yml
@@ -1,7 +1,7 @@
name: Continuous Deployment
on:
schedule:
- - cron: '15 6 * * *'
+ - cron: '15 4 * * *'
workflow_dispatch:
env:
diff --git a/Source/Editor/GUI/Docking/DockPanel.cs b/Source/Editor/GUI/Docking/DockPanel.cs
index c8900dcba..ae162c714 100644
--- a/Source/Editor/GUI/Docking/DockPanel.cs
+++ b/Source/Editor/GUI/Docking/DockPanel.cs
@@ -513,20 +513,12 @@ namespace FlaxEditor.GUI.Docking
if (TryCollapseSplitter(_tabsProxy?.Parent as Panel))
return;
}
- else if (!IsMaster)
- {
- throw new InvalidOperationException();
- }
}
else if (_childPanels.Count != 0)
{
if (TryCollapseSplitter(_tabsProxy?.Parent as Panel))
return;
}
- else if (!IsMaster)
- {
- throw new InvalidOperationException();
- }
}
internal bool CollapseEmptyTabsProxy()
diff --git a/Source/Editor/Options/InterfaceOptions.cs b/Source/Editor/Options/InterfaceOptions.cs
index 75acd170d..6d56e9ac7 100644
--- a/Source/Editor/Options/InterfaceOptions.cs
+++ b/Source/Editor/Options/InterfaceOptions.cs
@@ -548,9 +548,9 @@ namespace FlaxEditor.Options
///
/// Gets or sets the curvature of the line connecting to connected visject nodes.
///
- [DefaultValue(1.0f), Range(0.0f, 2.0f)]
+ [DefaultValue(0.25f), Range(0.0f, 2.0f)]
[EditorDisplay("Visject"), EditorOrder(550)]
- public float ConnectionCurvature { get; set; } = 1.0f;
+ public float ConnectionCurvature { get; set; } = 0.25f;
///
/// Gets or sets a value that indicates whether the context menu description panel is shown or not.
diff --git a/Source/Engine/Engine/Engine.cpp b/Source/Engine/Engine/Engine.cpp
index 4b037db37..3d709499b 100644
--- a/Source/Engine/Engine/Engine.cpp
+++ b/Source/Engine/Engine/Engine.cpp
@@ -608,7 +608,7 @@ void EngineImpl::InitLog()
// Log product info
LOG(Info, "Product: {0}, Company: {1}", Globals::ProductName, Globals::CompanyName);
- LOG(Info, "Current culture: {0}", Platform::GetUserLocaleName());
+ LOG(Info, "Current language: {}, culture: {}", Platform::GetUserLanguage(), Platform::GetUserLocaleName());
LOG(Info, "Command line: {0}", CommandLine);
LOG(Info, "Base folder: {0}", Globals::StartupFolder);
LOG(Info, "Binaries folder: {0}", Globals::BinariesFolder);
diff --git a/Source/Engine/Graphics/PostProcessSettings.cpp b/Source/Engine/Graphics/PostProcessSettings.cpp
index c86a0f31a..7c728bcf7 100644
--- a/Source/Engine/Graphics/PostProcessSettings.cpp
+++ b/Source/Engine/Graphics/PostProcessSettings.cpp
@@ -220,7 +220,11 @@ void PostFxMaterialsSettings::BlendWith(PostFxMaterialsSettings& other, float we
while (Materials.Count() != POST_PROCESS_SETTINGS_MAX_MATERIALS && indexSrc < other.Materials.Count())
{
if (!Materials.Contains(materialsSrc[indexSrc].GetID()))
- Materials.Add(materialsSrc[indexSrc]);
+ {
+ auto& ref = materialsSrc[indexSrc];
+ ref.Get(); // Load asset
+ Materials.Add(ref);
+ }
indexSrc++;
}
}
diff --git a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp
index 5136a6cce..cb6c82b5d 100644
--- a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp
+++ b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp
@@ -313,6 +313,12 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI
Actor* oldTargetActor = instance.TargetActor;
if (!oldTargetActor || EnumHasAnyFlags(oldTargetActor->Flags, ObjectFlags::WasMarkedToDelete))
continue;
+
+ // Get scene objects in the prefab instance (use old target actor to maintain actors order matching the prefab data)
+ sceneObjects->Clear();
+ SceneQuery::GetAllSerializableSceneObjects(instance.TargetActor, *sceneObjects.Value);
+
+ // Fixup prefab root when it was changed
Actor* newTargetActor = FindActorWithPrefabObjectId(instance.TargetActor, defaultInstance->GetID());
if (!newTargetActor)
{
@@ -321,15 +327,17 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI
else if (oldTargetActor != newTargetActor)
{
LOG(Info, "Changing root object of prefab instance from {0} to {1}", oldTargetActor->ToString(), newTargetActor->ToString());
- newTargetActor->SetParent(oldTargetActor->GetParent(), true, false);
+ Actor* oldTargetParent = oldTargetActor->GetParent();
+ if (newTargetActor == oldTargetParent || newTargetActor->GetParent() == oldTargetActor)
+ {
+ // Direct reparenting needs additional step to prevent loop in a hierarchy
+ oldTargetActor->SetParent(nullptr, true, false);
+ }
+ newTargetActor->SetParent(oldTargetParent, true, false);
oldTargetActor->SetParent(newTargetActor, true, false);
instance.TargetActor = newTargetActor;
}
- // Get scene objects in the prefab instance
- sceneObjects->Clear();
- SceneQuery::GetAllSerializableSceneObjects(instance.TargetActor, *sceneObjects.Value);
-
int32 existingObjectsCount = sceneObjects->Count();
modifier->IdsMapping.EnsureCapacity((existingObjectsCount + newPrefabObjectIds.Count()));
@@ -350,7 +358,7 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI
continue;
}
- modifier.Value->IdsMapping[obj->GetPrefabObjectID()] = obj->GetSceneObjectId();
+ modifier.Value->IdsMapping[obj->GetPrefabObjectID()] = obj->GetID();
}
}
@@ -820,6 +828,8 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr
PROFILE_CPU_NAMED("Prefab.Apply");
ScopeLock lock(Locker);
const auto prefabId = GetID();
+ const auto oldRootId = GetRootObjectId();
+ const auto newRootId = targetActor->GetPrefabObjectID();
// Gather all scene objects in target instance (reused later)
CollectionPoolCache::ScopeCache targetObjects = ActorsCache::SceneObjectsListCache.Get();
@@ -1121,15 +1131,14 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr
// Find the prefab root object (the root is usually serialized first)
auto root = dynamic_cast(sceneObjects.Value->At(0));
- if (root && root->_parent)
+ int32 targetActorIdx = oldObjectsIds.Find(newRootId);
+ if (newRootId != oldRootId && targetActorIdx > 0 && targetActorIdx < sceneObjects.Value->Count() && dynamic_cast(sceneObjects.Value->At(targetActorIdx)))
{
// When changing prefab root the target actor is a new root so try to find it in the objects
- int32 targetActorIdx = oldObjectsIds.Find(targetActor->GetPrefabObjectID());
- if (targetActorIdx > 0 && targetActorIdx < sceneObjects.Value->Count() && dynamic_cast(sceneObjects.Value->At(targetActorIdx)))
- {
- root = dynamic_cast(sceneObjects.Value->At(targetActorIdx));
- }
-
+ root = (Actor*)sceneObjects.Value->At(targetActorIdx);
+ }
+ else if (root && root->_parent)
+ {
// Try using the first actor without a parent as a new root
for (int32 i = 1; i < sceneObjects->Count(); i++)
{
@@ -1141,13 +1150,13 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr
break;
}
}
-
+ }
+ if (root && root->_parent)
+ {
// Keep root unlinked
- if (root->_parent)
- {
- root->_parent->Children.Remove(root);
- root->_parent = nullptr;
- }
+ root->_parent->Children.Remove(root);
+ root->_parent = nullptr;
+ root->OnParentChanged();
}
if (!root)
{
diff --git a/Source/Engine/Level/Prefabs/PrefabManager.cpp b/Source/Engine/Level/Prefabs/PrefabManager.cpp
index a0b4bb807..62e0d10d7 100644
--- a/Source/Engine/Level/Prefabs/PrefabManager.cpp
+++ b/Source/Engine/Level/Prefabs/PrefabManager.cpp
@@ -584,6 +584,11 @@ bool PrefabManager::ApplyAll(Actor* instance)
// Use the input object as fallback
rootObjectInstance = instance;
}
+ while (rootObjectInstance->GetParent() && rootObjectInstance->GetParent()->GetPrefabID() == rootObjectInstance->GetPrefabID())
+ {
+ // Move up to the root of the prefab instance (eg. in case of root change on instance to apply)
+ rootObjectInstance = rootObjectInstance->GetParent();
+ }
return prefab->ApplyAll(rootObjectInstance);
}
diff --git a/Source/Engine/Localization/Localization.cpp b/Source/Engine/Localization/Localization.cpp
index 231862688..df11ef68f 100644
--- a/Source/Engine/Localization/Localization.cpp
+++ b/Source/Engine/Localization/Localization.cpp
@@ -169,6 +169,50 @@ String LocalizedString::ToStringPlural(int32 n) const
return Localization::GetPluralString(Id, n, Value);
}
+void Serialization::Serialize(ISerializable::SerializeStream& stream, const LocalizedString& v, const void* otherObj)
+{
+ if (v.Id.IsEmpty())
+ {
+ stream.String(v.Value);
+ }
+ else
+ {
+ stream.StartObject();
+ stream.JKEY("Id");
+ stream.String(v.Id);
+ if (v.Value.HasChars())
+ {
+ stream.JKEY("Value");
+ stream.String(v.Value);
+ }
+ stream.EndObject();
+ }
+}
+
+void Serialization::Deserialize(ISerializable::DeserializeStream& stream, LocalizedString& v, ISerializeModifier* modifier)
+{
+ if (stream.IsString())
+ {
+ v.Id = String::Empty;
+ v.Value = stream.GetText();
+ }
+ else if (stream.IsObject())
+ {
+ auto e = SERIALIZE_FIND_MEMBER(stream, "Id");
+ if (e != stream.MemberEnd())
+ v.Id.SetUTF8(e->value.GetString(), e->value.GetStringLength());
+ e = SERIALIZE_FIND_MEMBER(stream, "Value");
+ if (e != stream.MemberEnd())
+ v.Value.SetUTF8(e->value.GetString(), e->value.GetStringLength());
+ else if (v.Id.HasChars())
+ v.Value.Clear();
+ }
+ else
+ {
+ v = LocalizedString();
+ }
+}
+
void LocalizationService::OnLocalizationChanged()
{
PROFILE_CPU();
@@ -288,7 +332,8 @@ bool LocalizationService::Init()
PROFILE_MEM(Localization);
// Use system language as default
- CurrentLanguage = CurrentCulture = CultureInfo(Platform::GetUserLocaleName());
+ CurrentLanguage = CultureInfo(Platform::GetUserLanguage());
+ CurrentCulture = CultureInfo(Platform::GetUserLocaleName());
// Setup localization
Instance.OnLocalizationChanged();
@@ -350,3 +395,16 @@ String Localization::GetPluralString(const String& id, int32 n, const String& fa
const String& format = Instance.Get(id, n - 1, fallback);
return String::Format(format.GetText(), n);
}
+
+Array Localization::GetLocales()
+{
+ Array result;
+ auto& settings = *LocalizationSettings::Get();
+ for (auto& e : settings.LocalizedStringTables)
+ {
+ auto table = e.Get();
+ if (table && !table->WaitForLoaded())
+ result.AddUnique(table->Locale);
+ }
+ return result;
+}
diff --git a/Source/Engine/Localization/Localization.h b/Source/Engine/Localization/Localization.h
index 277052c2f..05cd5115f 100644
--- a/Source/Engine/Localization/Localization.h
+++ b/Source/Engine/Localization/Localization.h
@@ -59,4 +59,10 @@ public:
/// The optional fallback string value to use if localized string is missing.
/// The localized text.
API_FUNCTION() static String GetPluralString(const String& id, int32 n, const String& fallback = String::Empty);
+
+ ///
+ /// Gets the list of unique languages (locale names such as 'pl-PL') defined in project in Localized String Tables set in Localization Settings. Can be used to display all languages available in game.
+ ///
+ /// The list of unique languages (locale names such as 'pl-PL').
+ API_FUNCTION() static Array GetLocales();
};
diff --git a/Source/Engine/Localization/LocalizedString.h b/Source/Engine/Localization/LocalizedString.h
index 15aee439f..7f660827f 100644
--- a/Source/Engine/Localization/LocalizedString.h
+++ b/Source/Engine/Localization/LocalizedString.h
@@ -71,49 +71,8 @@ namespace Serialization
return !otherObj || v != *(LocalizedString*)otherObj;
}
- inline void Serialize(ISerializable::SerializeStream& stream, const LocalizedString& v, const void* otherObj)
- {
- if (v.Id.IsEmpty())
- {
- stream.String(v.Value);
- }
- else
- {
- stream.StartObject();
- stream.JKEY("Id");
- stream.String(v.Id);
- if (v.Value.HasChars())
- {
- stream.JKEY("Value");
- stream.String(v.Value);
- }
- stream.EndObject();
- }
- }
-
- inline void Deserialize(ISerializable::DeserializeStream& stream, LocalizedString& v, ISerializeModifier* modifier)
- {
- if (stream.IsString())
- {
- v.Id = String::Empty;
- v.Value = stream.GetText();
- }
- else if (stream.IsObject())
- {
- auto e = SERIALIZE_FIND_MEMBER(stream, "Id");
- if (e != stream.MemberEnd())
- v.Id.SetUTF8(e->value.GetString(), e->value.GetStringLength());
- e = SERIALIZE_FIND_MEMBER(stream, "Value");
- if (e != stream.MemberEnd())
- v.Value.SetUTF8(e->value.GetString(), e->value.GetStringLength());
- else if (v.Id.HasChars())
- v.Value.Clear();
- }
- else
- {
- v = LocalizedString();
- }
- }
+ FLAXENGINE_API void Serialize(ISerializable::SerializeStream& stream, const LocalizedString& v, const void* otherObj);
+ FLAXENGINE_API void Deserialize(ISerializable::DeserializeStream& stream, LocalizedString& v, ISerializeModifier* modifier);
}
DEFINE_DEFAULT_FORMATTING_VIA_TO_STRING(LocalizedString);
diff --git a/Source/Engine/Platform/Base/PlatformBase.cpp b/Source/Engine/Platform/Base/PlatformBase.cpp
index 7e5b99e60..d8d03f028 100644
--- a/Source/Engine/Platform/Base/PlatformBase.cpp
+++ b/Source/Engine/Platform/Base/PlatformBase.cpp
@@ -609,6 +609,11 @@ ScreenOrientationType PlatformBase::GetScreenOrientationType()
return ScreenOrientationType::Unknown;
}
+String PlatformBase::GetUserLanguage()
+{
+ return Platform::GetUserLocaleName();
+}
+
String PlatformBase::GetUserName()
{
return Users.Count() != 0 ? Users[0]->GetName() : String::Empty;
diff --git a/Source/Engine/Platform/Base/PlatformBase.h b/Source/Engine/Platform/Base/PlatformBase.h
index 904178693..bc2caff44 100644
--- a/Source/Engine/Platform/Base/PlatformBase.h
+++ b/Source/Engine/Platform/Base/PlatformBase.h
@@ -625,7 +625,12 @@ public:
API_PROPERTY() static ScreenOrientationType GetScreenOrientationType();
///
- /// Gets the current locale culture (eg. "pl-PL" or "en-US").
+ /// Gets the current user display language used to localize texts. Returns name of the culture (eg. "pl-PL" or "en-US"), use CultureInfo for display name of the language.
+ ///
+ API_PROPERTY() static String GetUserLanguage();
+
+ ///
+ /// Gets the current user locale culture used to localize numbers, currency and dates. Returns name of the culture (eg. "pl-PL" or "en-US"), use CultureInfo for display name of the language.
///
API_PROPERTY() static String GetUserLocaleName() = delete;
diff --git a/Source/Engine/Platform/Windows/WindowsPlatform.cpp b/Source/Engine/Platform/Windows/WindowsPlatform.cpp
index ac4809b53..61deac794 100644
--- a/Source/Engine/Platform/Windows/WindowsPlatform.cpp
+++ b/Source/Engine/Platform/Windows/WindowsPlatform.cpp
@@ -62,7 +62,7 @@ void FlaxDbgHelpUnlock()
namespace
{
- String UserLocale, ComputerName, WindowsName;
+ String UserLanguage, UserLocale, ComputerName, WindowsName;
HANDLE EngineMutex = nullptr;
Rectangle VirtualScreenBounds(0.0f, 0.0f, 0.0f, 0.0f);
int32 VersionMajor = 0;
@@ -774,12 +774,18 @@ bool WindowsPlatform::Init()
DWORD tmp;
Char buffer[256];
+ ULONG bufferSize = ARRAY_COUNT(buffer), languagesCount = 0;
- // Get user locale string
- if (GetUserDefaultLocaleName(buffer, LOCALE_NAME_MAX_LENGTH))
+ // Get user locale strings
+ if (GetUserDefaultLocaleName(buffer, (int)bufferSize))
{
UserLocale = String(buffer);
}
+ if (GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &languagesCount, buffer, &bufferSize))
+ {
+ // Get the first language
+ UserLanguage = String(buffer);
+ }
// Get computer name string
if (GetComputerNameW(buffer, &tmp))
@@ -951,6 +957,11 @@ int32 WindowsPlatform::GetDpi()
}
#endif
+String WindowsPlatform::GetUserLanguage()
+{
+ return UserLanguage.HasChars() ? UserLanguage : UserLocale;
+}
+
String WindowsPlatform::GetUserLocaleName()
{
return UserLocale;
diff --git a/Source/Engine/Platform/Windows/WindowsPlatform.h b/Source/Engine/Platform/Windows/WindowsPlatform.h
index d45dd237c..b30fc68ad 100644
--- a/Source/Engine/Platform/Windows/WindowsPlatform.h
+++ b/Source/Engine/Platform/Windows/WindowsPlatform.h
@@ -68,6 +68,7 @@ public:
#if !PLATFORM_SDL
static int32 GetDpi();
#endif
+ static String GetUserLanguage();
static String GetUserLocaleName();
static String GetComputerName();
static bool GetHasFocus();
diff --git a/Source/Engine/Tests/TestPrefabs.cpp b/Source/Engine/Tests/TestPrefabs.cpp
index 6389f1cf7..91bce81cc 100644
--- a/Source/Engine/Tests/TestPrefabs.cpp
+++ b/Source/Engine/Tests/TestPrefabs.cpp
@@ -854,4 +854,55 @@ TEST_CASE("Prefabs")
instanceB->DeleteObject();
instanceC->DeleteObject();
}
+ SECTION("Test Changing Prefab Root")
+ {
+ // https://github.com/FlaxEngine/FlaxEngine/issues/4034
+
+ // Create base prefab with 2 objects
+ AssetReference prefab = Content::CreateVirtualAsset();
+ REQUIRE(prefab);
+ SCOPE_EXIT{ Content::DeleteAsset(prefab); };
+ Guid id;
+ Guid::Parse("333334524c696dcfa93cabacd2a4f404", id);
+ prefab->ChangeID(id);
+ auto prefabInit = prefab->Init(Prefab::TypeName,
+ "["
+ "{"
+ "\"ID\": \"1111cfaa4bd1a53435129480e5bbdb3b\","
+ "\"TypeName\": \"FlaxEngine.Decal\","
+ "\"Name\": \"Old Root\""
+ "},"
+ "{"
+ "\"ID\": \"2222814f4d913e58eb35ab8b0b7e2eef\","
+ "\"TypeName\": \"FlaxEngine.Camera\","
+ "\"ParentID\": \"1111cfaa4bd1a53435129480e5bbdb3b\","
+ "\"Name\": \"New Root\""
+ "}"
+ "]");
+ REQUIRE(!prefabInit);
+
+ // Spawn test instances
+ ScriptingObjectReference instance1 = PrefabManager::SpawnPrefab(prefab);
+ ScriptingObjectReference instance2 = PrefabManager::SpawnPrefab(prefab);
+
+ // Change root
+ Actor* child = instance1->Children[0];
+ child->SetParent(nullptr);
+ instance1->SetParent(child);
+ bool applyResult = PrefabManager::ApplyAll(instance1);
+ REQUIRE(!applyResult);
+
+ // Verify scenario
+ REQUIRE(instance2);
+ REQUIRE(instance2->GetName() == TEXT("Old Root"));
+ REQUIRE(instance2->GetChildrenCount() == 0);
+ REQUIRE(instance2->GetParent());
+ REQUIRE(instance2->GetParent()->GetName() == TEXT("New Root"));
+ REQUIRE(instance2->GetParent()->GetChildrenCount() == 1);
+ REQUIRE(instance2->GetParent()->Children[0]->GetName() == TEXT("Old Root"));
+
+ // Cleanup
+ instance1->DeleteObject();
+ instance2->DeleteObject();
+ }
}