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
on:
schedule:
- cron: '15 6 * * *'
- cron: '15 4 * * *'
workflow_dispatch:
env:
-8
View File
@@ -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()
+2 -2
View File
@@ -548,9 +548,9 @@ namespace FlaxEditor.Options
/// <summary>
/// Gets or sets the curvature of the line connecting to connected visject nodes.
/// </summary>
[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;
/// <summary>
/// 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(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);
@@ -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++;
}
}
+25 -16
View File
@@ -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<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)
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
int32 targetActorIdx = oldObjectsIds.Find(targetActor->GetPrefabObjectID());
if (targetActorIdx > 0 && targetActorIdx < sceneObjects.Value->Count() && dynamic_cast<Actor*>(sceneObjects.Value->At(targetActorIdx)))
{
root = dynamic_cast<Actor*>(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;
}
}
// Keep root unlinked
if (root->_parent)
}
if (root && root->_parent)
{
// Keep root unlinked
root->_parent->Children.Remove(root);
root->_parent = nullptr;
}
root->OnParentChanged();
}
if (!root)
{
@@ -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);
}
+59 -1
View File
@@ -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<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>
/// <returns>The localized text.</returns>
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;
}
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);
@@ -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;
+6 -1
View File
@@ -625,7 +625,12 @@ public:
API_PROPERTY() static ScreenOrientationType GetScreenOrientationType();
/// <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>
API_PROPERTY() static String GetUserLocaleName() = delete;
@@ -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;
@@ -68,6 +68,7 @@ public:
#if !PLATFORM_SDL
static int32 GetDpi();
#endif
static String GetUserLanguage();
static String GetUserLocaleName();
static String GetComputerName();
static bool GetHasFocus();
+51
View File
@@ -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> 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();
}
}