Merge remote-tracking branch 'origin/master' into 1.12

# Conflicts:
#	Source/Engine/Platform/Windows/WindowsPlatform.h
This commit is contained in:
2026-04-08 13:24:29 +02:00
15 changed files with 186 additions and 80 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
name: Continuous Deployment name: Continuous Deployment
on: on:
schedule: schedule:
- cron: '15 6 * * *' - cron: '15 4 * * *'
workflow_dispatch: workflow_dispatch:
env: env:
-8
View File
@@ -513,20 +513,12 @@ namespace FlaxEditor.GUI.Docking
if (TryCollapseSplitter(_tabsProxy?.Parent as Panel)) if (TryCollapseSplitter(_tabsProxy?.Parent as Panel))
return; return;
} }
else if (!IsMaster)
{
throw new InvalidOperationException();
}
} }
else if (_childPanels.Count != 0) else if (_childPanels.Count != 0)
{ {
if (TryCollapseSplitter(_tabsProxy?.Parent as Panel)) if (TryCollapseSplitter(_tabsProxy?.Parent as Panel))
return; return;
} }
else if (!IsMaster)
{
throw new InvalidOperationException();
}
} }
internal bool CollapseEmptyTabsProxy() internal bool CollapseEmptyTabsProxy()
+2 -2
View File
@@ -548,9 +548,9 @@ namespace FlaxEditor.Options
/// <summary> /// <summary>
/// Gets or sets the curvature of the line connecting to connected visject nodes. /// Gets or sets the curvature of the line connecting to connected visject nodes.
/// </summary> /// </summary>
[DefaultValue(1.0f), Range(0.0f, 2.0f)] [DefaultValue(0.25f), Range(0.0f, 2.0f)]
[EditorDisplay("Visject"), EditorOrder(550)] [EditorDisplay("Visject"), EditorOrder(550)]
public float ConnectionCurvature { get; set; } = 1.0f; public float ConnectionCurvature { get; set; } = 0.25f;
/// <summary> /// <summary>
/// Gets or sets a value that indicates whether the context menu description panel is shown or not. /// Gets or sets a value that indicates whether the context menu description panel is shown or not.
+1 -1
View File
@@ -608,7 +608,7 @@ void EngineImpl::InitLog()
// Log product info // Log product info
LOG(Info, "Product: {0}, Company: {1}", Globals::ProductName, Globals::CompanyName); 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, "Command line: {0}", CommandLine);
LOG(Info, "Base folder: {0}", Globals::StartupFolder); LOG(Info, "Base folder: {0}", Globals::StartupFolder);
LOG(Info, "Binaries folder: {0}", Globals::BinariesFolder); LOG(Info, "Binaries folder: {0}", Globals::BinariesFolder);
@@ -220,7 +220,11 @@ void PostFxMaterialsSettings::BlendWith(PostFxMaterialsSettings& other, float we
while (Materials.Count() != POST_PROCESS_SETTINGS_MAX_MATERIALS && indexSrc < other.Materials.Count()) while (Materials.Count() != POST_PROCESS_SETTINGS_MAX_MATERIALS && indexSrc < other.Materials.Count())
{ {
if (!Materials.Contains(materialsSrc[indexSrc].GetID())) if (!Materials.Contains(materialsSrc[indexSrc].GetID()))
Materials.Add(materialsSrc[indexSrc]); {
auto& ref = materialsSrc[indexSrc];
ref.Get(); // Load asset
Materials.Add(ref);
}
indexSrc++; indexSrc++;
} }
} }
+25 -16
View File
@@ -313,6 +313,12 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI
Actor* oldTargetActor = instance.TargetActor; Actor* oldTargetActor = instance.TargetActor;
if (!oldTargetActor || EnumHasAnyFlags(oldTargetActor->Flags, ObjectFlags::WasMarkedToDelete)) if (!oldTargetActor || EnumHasAnyFlags(oldTargetActor->Flags, ObjectFlags::WasMarkedToDelete))
continue; 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()); Actor* newTargetActor = FindActorWithPrefabObjectId(instance.TargetActor, defaultInstance->GetID());
if (!newTargetActor) if (!newTargetActor)
{ {
@@ -321,15 +327,17 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI
else if (oldTargetActor != newTargetActor) else if (oldTargetActor != newTargetActor)
{ {
LOG(Info, "Changing root object of prefab instance from {0} to {1}", oldTargetActor->ToString(), newTargetActor->ToString()); 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); oldTargetActor->SetParent(newTargetActor, true, false);
instance.TargetActor = newTargetActor; instance.TargetActor = newTargetActor;
} }
// Get scene objects in the prefab instance
sceneObjects->Clear();
SceneQuery::GetAllSerializableSceneObjects(instance.TargetActor, *sceneObjects.Value);
int32 existingObjectsCount = sceneObjects->Count(); int32 existingObjectsCount = sceneObjects->Count();
modifier->IdsMapping.EnsureCapacity((existingObjectsCount + newPrefabObjectIds.Count())); modifier->IdsMapping.EnsureCapacity((existingObjectsCount + newPrefabObjectIds.Count()));
@@ -350,7 +358,7 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI
continue; 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"); PROFILE_CPU_NAMED("Prefab.Apply");
ScopeLock lock(Locker); ScopeLock lock(Locker);
const auto prefabId = GetID(); const auto prefabId = GetID();
const auto oldRootId = GetRootObjectId();
const auto newRootId = targetActor->GetPrefabObjectID();
// Gather all scene objects in target instance (reused later) // Gather all scene objects in target instance (reused later)
CollectionPoolCache<ActorsCache::SceneObjectsListType>::ScopeCache targetObjects = ActorsCache::SceneObjectsListCache.Get(); CollectionPoolCache<ActorsCache::SceneObjectsListType>::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) // Find the prefab root object (the root is usually serialized first)
auto root = dynamic_cast<Actor*>(sceneObjects.Value->At(0)); auto root = dynamic_cast<Actor*>(sceneObjects.Value->At(0));
if (root && root->_parent) int32 targetActorIdx = oldObjectsIds.Find(newRootId);
if (newRootId != oldRootId && targetActorIdx > 0 && targetActorIdx < sceneObjects.Value->Count() && dynamic_cast<Actor*>(sceneObjects.Value->At(targetActorIdx)))
{ {
// When changing prefab root the target actor is a new root so try to find it in the objects // 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()); root = (Actor*)sceneObjects.Value->At(targetActorIdx);
if (targetActorIdx > 0 && targetActorIdx < sceneObjects.Value->Count() && dynamic_cast<Actor*>(sceneObjects.Value->At(targetActorIdx)))
{
root = dynamic_cast<Actor*>(sceneObjects.Value->At(targetActorIdx));
} }
else if (root && root->_parent)
{
// Try using the first actor without a parent as a new root // Try using the first actor without a parent as a new root
for (int32 i = 1; i < sceneObjects->Count(); i++) for (int32 i = 1; i < sceneObjects->Count(); i++)
{ {
@@ -1141,13 +1150,13 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr
break; break;
} }
} }
}
// Keep root unlinked if (root && root->_parent)
if (root->_parent)
{ {
// Keep root unlinked
root->_parent->Children.Remove(root); root->_parent->Children.Remove(root);
root->_parent = nullptr; root->_parent = nullptr;
} root->OnParentChanged();
} }
if (!root) if (!root)
{ {
@@ -584,6 +584,11 @@ bool PrefabManager::ApplyAll(Actor* instance)
// Use the input object as fallback // Use the input object as fallback
rootObjectInstance = instance; 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); return prefab->ApplyAll(rootObjectInstance);
} }
+59 -1
View File
@@ -169,6 +169,50 @@ String LocalizedString::ToStringPlural(int32 n) const
return Localization::GetPluralString(Id, n, Value); 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() void LocalizationService::OnLocalizationChanged()
{ {
PROFILE_CPU(); PROFILE_CPU();
@@ -288,7 +332,8 @@ bool LocalizationService::Init()
PROFILE_MEM(Localization); PROFILE_MEM(Localization);
// Use system language as default // Use system language as default
CurrentLanguage = CurrentCulture = CultureInfo(Platform::GetUserLocaleName()); CurrentLanguage = CultureInfo(Platform::GetUserLanguage());
CurrentCulture = CultureInfo(Platform::GetUserLocaleName());
// Setup localization // Setup localization
Instance.OnLocalizationChanged(); 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); const String& format = Instance.Get(id, n - 1, fallback);
return String::Format(format.GetText(), n); return String::Format(format.GetText(), n);
} }
Array<String> Localization::GetLocales()
{
Array<String> result;
auto& settings = *LocalizationSettings::Get();
for (auto& e : settings.LocalizedStringTables)
{
auto table = e.Get();
if (table && !table->WaitForLoaded())
result.AddUnique(table->Locale);
}
return result;
}
@@ -59,4 +59,10 @@ public:
/// <param name="fallback">The optional fallback string value to use if localized string is missing.</param> /// <param name="fallback">The optional fallback string value to use if localized string is missing.</param>
/// <returns>The localized text.</returns> /// <returns>The localized text.</returns>
API_FUNCTION() static String GetPluralString(const String& id, int32 n, const String& fallback = String::Empty); API_FUNCTION() static String GetPluralString(const String& id, int32 n, const String& fallback = String::Empty);
/// <summary>
/// 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.
/// </summary>
/// <returns>The list of unique languages (locale names such as 'pl-PL').</returns>
API_FUNCTION() static Array<String, HeapAllocation> GetLocales();
}; };
+2 -43
View File
@@ -71,49 +71,8 @@ namespace Serialization
return !otherObj || v != *(LocalizedString*)otherObj; return !otherObj || v != *(LocalizedString*)otherObj;
} }
inline void Serialize(ISerializable::SerializeStream& stream, const LocalizedString& v, const void* otherObj) FLAXENGINE_API void Serialize(ISerializable::SerializeStream& stream, const LocalizedString& v, const void* otherObj);
{ FLAXENGINE_API void Deserialize(ISerializable::DeserializeStream& stream, LocalizedString& v, ISerializeModifier* modifier);
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();
}
}
} }
DEFINE_DEFAULT_FORMATTING_VIA_TO_STRING(LocalizedString); DEFINE_DEFAULT_FORMATTING_VIA_TO_STRING(LocalizedString);
@@ -609,6 +609,11 @@ ScreenOrientationType PlatformBase::GetScreenOrientationType()
return ScreenOrientationType::Unknown; return ScreenOrientationType::Unknown;
} }
String PlatformBase::GetUserLanguage()
{
return Platform::GetUserLocaleName();
}
String PlatformBase::GetUserName() String PlatformBase::GetUserName()
{ {
return Users.Count() != 0 ? Users[0]->GetName() : String::Empty; return Users.Count() != 0 ? Users[0]->GetName() : String::Empty;
+6 -1
View File
@@ -625,7 +625,12 @@ public:
API_PROPERTY() static ScreenOrientationType GetScreenOrientationType(); API_PROPERTY() static ScreenOrientationType GetScreenOrientationType();
/// <summary> /// <summary>
/// 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.
/// </summary>
API_PROPERTY() static String GetUserLanguage();
/// <summary>
/// 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.
/// </summary> /// </summary>
API_PROPERTY() static String GetUserLocaleName() = delete; API_PROPERTY() static String GetUserLocaleName() = delete;
@@ -62,7 +62,7 @@ void FlaxDbgHelpUnlock()
namespace namespace
{ {
String UserLocale, ComputerName, WindowsName; String UserLanguage, UserLocale, ComputerName, WindowsName;
HANDLE EngineMutex = nullptr; HANDLE EngineMutex = nullptr;
Rectangle VirtualScreenBounds(0.0f, 0.0f, 0.0f, 0.0f); Rectangle VirtualScreenBounds(0.0f, 0.0f, 0.0f, 0.0f);
int32 VersionMajor = 0; int32 VersionMajor = 0;
@@ -774,12 +774,18 @@ bool WindowsPlatform::Init()
DWORD tmp; DWORD tmp;
Char buffer[256]; Char buffer[256];
ULONG bufferSize = ARRAY_COUNT(buffer), languagesCount = 0;
// Get user locale string // Get user locale strings
if (GetUserDefaultLocaleName(buffer, LOCALE_NAME_MAX_LENGTH)) if (GetUserDefaultLocaleName(buffer, (int)bufferSize))
{ {
UserLocale = String(buffer); UserLocale = String(buffer);
} }
if (GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &languagesCount, buffer, &bufferSize))
{
// Get the first language
UserLanguage = String(buffer);
}
// Get computer name string // Get computer name string
if (GetComputerNameW(buffer, &tmp)) if (GetComputerNameW(buffer, &tmp))
@@ -951,6 +957,11 @@ int32 WindowsPlatform::GetDpi()
} }
#endif #endif
String WindowsPlatform::GetUserLanguage()
{
return UserLanguage.HasChars() ? UserLanguage : UserLocale;
}
String WindowsPlatform::GetUserLocaleName() String WindowsPlatform::GetUserLocaleName()
{ {
return UserLocale; return UserLocale;
@@ -68,6 +68,7 @@ public:
#if !PLATFORM_SDL #if !PLATFORM_SDL
static int32 GetDpi(); static int32 GetDpi();
#endif #endif
static String GetUserLanguage();
static String GetUserLocaleName(); static String GetUserLocaleName();
static String GetComputerName(); static String GetComputerName();
static bool GetHasFocus(); static bool GetHasFocus();
+51
View File
@@ -854,4 +854,55 @@ TEST_CASE("Prefabs")
instanceB->DeleteObject(); instanceB->DeleteObject();
instanceC->DeleteObject(); instanceC->DeleteObject();
} }
SECTION("Test Changing Prefab Root")
{
// https://github.com/FlaxEngine/FlaxEngine/issues/4034
// Create base prefab with 2 objects
AssetReference<Prefab> prefab = Content::CreateVirtualAsset<Prefab>();
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<Actor> instance1 = PrefabManager::SpawnPrefab(prefab);
ScriptingObjectReference<Actor> 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();
}
} }