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(); + } }