diff --git a/Source/Editor/CustomEditors/CustomEditorsUtil.cs b/Source/Editor/CustomEditors/CustomEditorsUtil.cs index 070ad1fb3..7f653376b 100644 --- a/Source/Editor/CustomEditors/CustomEditorsUtil.cs +++ b/Source/Editor/CustomEditors/CustomEditorsUtil.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; +using FlaxEditor.CustomEditors.Dedicated; using FlaxEditor.CustomEditors.Editors; using FlaxEditor.Scripting; using FlaxEngine; @@ -107,7 +108,7 @@ namespace FlaxEditor.CustomEditors // Select default editor (based on type) if (targetType.IsEnum) return new EnumEditor(); - if (targetType.IsGenericType) + if (targetType.IsGenericType) { if (targetTypeType.GetGenericTypeDefinition() == typeof(Dictionary<,>)) return new DictionaryEditor(); @@ -118,6 +119,8 @@ namespace FlaxEditor.CustomEditors if (customEditorType != null) return (CustomEditor)Activator.CreateInstance(customEditorType); } + if (typeof(FlaxEngine.Object).IsAssignableFrom(targetTypeType)) + return new ScriptingObjectEditor(); // The most generic editor return new GenericEditor(); diff --git a/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs b/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs index 75e3fff23..a8773a8d7 100644 --- a/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs @@ -20,7 +20,7 @@ namespace FlaxEditor.CustomEditors.Dedicated /// /// [CustomEditor(typeof(Actor)), DefaultEditor] - public class ActorEditor : GenericEditor + public class ActorEditor : ScriptingObjectEditor { private Guid _linkedPrefabId; diff --git a/Source/Editor/CustomEditors/Dedicated/ScriptingObjectEditor.cs b/Source/Editor/CustomEditors/Dedicated/ScriptingObjectEditor.cs new file mode 100644 index 000000000..477deb708 --- /dev/null +++ b/Source/Editor/CustomEditors/Dedicated/ScriptingObjectEditor.cs @@ -0,0 +1,29 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +using FlaxEditor.CustomEditors.Editors; +using FlaxEngine.Networking; + +namespace FlaxEditor.CustomEditors.Dedicated +{ + /// + /// Custom editor for . + /// + public class ScriptingObjectEditor : GenericEditor + { + /// + public override void Initialize(LayoutElementsContainer layout) + { + // Network objects debugging + var obj = Values[0] as FlaxEngine.Object; + if (Editor.IsPlayMode && NetworkManager.IsConnected && NetworkReplicator.HasObject(obj)) + { + var group = layout.Group("Network"); + group.Panel.Open(); + group.Label("Role", Utilities.Utils.GetPropertyNameUI(NetworkReplicator.GetObjectRole(obj).ToString())); + group.Label("Owner Client Id", NetworkReplicator.GetObjectOwnerClientId(obj).ToString()); + } + + base.Initialize(layout); + } + } +} diff --git a/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs b/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs index 73a43f1bf..613f5d3b4 100644 --- a/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs @@ -3,7 +3,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; using FlaxEditor.Actions; using FlaxEditor.Content; using FlaxEditor.GUI; diff --git a/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs b/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs index 2612440a4..076b94b0a 100644 --- a/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs +++ b/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs @@ -78,7 +78,7 @@ namespace FlaxEditor.CustomEditors.Editors /// public class ScaleEditor : Float3Editor { - private Image _linkImage; + private Button _linkButton; /// public override void Initialize(LayoutElementsContainer layout) @@ -87,19 +87,20 @@ namespace FlaxEditor.CustomEditors.Editors LinkValues = Editor.Instance.Windows.PropertiesWin.ScaleLinked; - _linkImage = new Image + // Add button with the link icon + _linkButton = new Button { + BackgroundBrush = new SpriteBrush(Editor.Instance.Icons.Link32), Parent = LinkedLabel, Width = 18, Height = 18, - Brush = LinkValues ? new SpriteBrush(Editor.Instance.Icons.Link32) : new SpriteBrush(), AnchorPreset = AnchorPresets.TopLeft, - TooltipText = "Scale values are linked together.", }; + _linkButton.Clicked += ToggleLink; + SetLinkStyle(); var x = LinkedLabel.Text.Value.Length * 7 + 5; - _linkImage.LocalX += x; - _linkImage.LocalY += 1; - + _linkButton.LocalX += x; + _linkButton.LocalY += 1; LinkedLabel.SetupContextMenu += (label, menu, editor) => { menu.AddSeparator(); @@ -127,7 +128,16 @@ namespace FlaxEditor.CustomEditors.Editors { LinkValues = !LinkValues; Editor.Instance.Windows.PropertiesWin.ScaleLinked = LinkValues; - _linkImage.Brush = LinkValues ? new SpriteBrush(Editor.Instance.Icons.Link32) : new SpriteBrush(); + SetLinkStyle(); + } + + private void SetLinkStyle() + { + var style = FlaxEngine.GUI.Style.Current; + var backgroundColor = LinkValues ? style.Foreground : style.ForegroundDisabled; + _linkButton.SetColors(backgroundColor); + _linkButton.BorderColor = _linkButton.BorderColorSelected = _linkButton.BorderColorHighlighted = Color.Transparent; + _linkButton.TooltipText = LinkValues ? "Unlinks scale components from uniform scaling" : "Links scale components for uniform scaling"; } } } diff --git a/Source/Editor/Windows/OutputLogWindow.cs b/Source/Editor/Windows/OutputLogWindow.cs index 544253fa5..f2f3b4aff 100644 --- a/Source/Editor/Windows/OutputLogWindow.cs +++ b/Source/Editor/Windows/OutputLogWindow.cs @@ -541,7 +541,7 @@ namespace FlaxEditor.Windows { ref var line = ref lines[j]; textBlock.Range.StartIndex = startIndex + line.FirstCharIndex; - textBlock.Range.EndIndex = startIndex + line.LastCharIndex; + textBlock.Range.EndIndex = startIndex + line.LastCharIndex + 1; textBlock.Bounds = new Rectangle(new Float2(0.0f, prevBlockBottom), line.Size); if (textBlock.Range.Length > 0) @@ -550,7 +550,7 @@ namespace FlaxEditor.Windows var regexStart = line.FirstCharIndex; if (j == 0) regexStart += prefixLength; - var regexLength = line.LastCharIndex - regexStart; + var regexLength = line.LastCharIndex + 1 - regexStart; if (regexLength > 0) { var match = _compileRegex.Match(entryText, regexStart, regexLength); diff --git a/Source/Engine/Networking/NetworkManager.cpp b/Source/Engine/Networking/NetworkManager.cpp index 88a104c3e..88c59ad9a 100644 --- a/Source/Engine/Networking/NetworkManager.cpp +++ b/Source/Engine/Networking/NetworkManager.cpp @@ -317,11 +317,12 @@ bool NetworkManager::StartHost() LocalClient = New(LocalClientId, NetworkConnection{ 0 }); // Auto-connect host + LocalClient->State = NetworkConnectionState::Connecting; + State = NetworkConnectionState::Connected; + StateChanged(); LocalClient->State = NetworkConnectionState::Connected; ClientConnected(LocalClient); - State = NetworkConnectionState::Connected; - StateChanged(); return false; } diff --git a/Source/Engine/Networking/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp index e150b1ea0..be7571b80 100644 --- a/Source/Engine/Networking/NetworkReplicator.cpp +++ b/Source/Engine/Networking/NetworkReplicator.cpp @@ -745,7 +745,7 @@ bool NetworkReplicator::InvokeSerializer(const ScriptingTypeHandle& typeHandle, return false; } -void NetworkReplicator::AddObject(ScriptingObject* obj, ScriptingObject* parent) +void NetworkReplicator::AddObject(ScriptingObject* obj, const ScriptingObject* parent) { if (!obj || NetworkManager::IsOffline()) return; @@ -774,6 +774,19 @@ void NetworkReplicator::AddObject(ScriptingObject* obj, ScriptingObject* parent) item.OwnerClientId = NetworkManager::ServerClientId; // Server owns objects by default item.Role = NetworkManager::IsClient() ? NetworkObjectRole::Replicated : NetworkObjectRole::OwnedAuthoritative; NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Add new object {}:{}, parent {}:{}", item.ToString(), obj->GetType().ToString(), item.ParentId.ToString(), parent ? parent->GetType().ToString() : String::Empty); + for (const SpawnItem& spawnItem : SpawnQueue) + { + if (spawnItem.HasOwnership && spawnItem.HierarchicalOwnership) + { + if (IsParentOf(obj, spawnItem.Object)) + { + // Inherit ownership + item.Role = spawnItem.Role; + item.OwnerClientId = spawnItem.OwnerClientId; + break; + } + } + } Objects.Add(MoveTemp(item)); } @@ -861,10 +874,27 @@ void NetworkReplicator::DespawnObject(ScriptingObject* obj) DeleteNetworkObject(obj); } +bool NetworkReplicator::HasObject(const ScriptingObject* obj) +{ + if (obj) + { + ScopeLock lock(ObjectsLock); + const auto it = Objects.Find(obj->GetID()); + if (it != Objects.End()) + return true; + for (const SpawnItem& item : SpawnQueue) + { + if (item.Object == obj) + return true; + } + } + return false; +} + uint32 NetworkReplicator::GetObjectOwnerClientId(const ScriptingObject* obj) { uint32 id = NetworkManager::ServerClientId; - if (obj) + if (obj && NetworkManager::IsConnected()) { ScopeLock lock(ObjectsLock); const auto it = Objects.Find(obj->GetID()); @@ -878,9 +908,16 @@ uint32 NetworkReplicator::GetObjectOwnerClientId(const ScriptingObject* obj) { if (item.HasOwnership) id = item.OwnerClientId; +#if USE_NETWORK_REPLICATOR_LOG + return id; +#else break; +#endif } } +#if USE_NETWORK_REPLICATOR_LOG + NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to get ownership of unregistered network object {} ({})", obj->GetID(), obj->GetType().ToString()); +#endif } } return id; @@ -889,7 +926,7 @@ uint32 NetworkReplicator::GetObjectOwnerClientId(const ScriptingObject* obj) NetworkObjectRole NetworkReplicator::GetObjectRole(const ScriptingObject* obj) { NetworkObjectRole role = NetworkObjectRole::None; - if (obj) + if (obj && NetworkManager::IsConnected()) { ScopeLock lock(ObjectsLock); const auto it = Objects.Find(obj->GetID()); @@ -903,9 +940,16 @@ NetworkObjectRole NetworkReplicator::GetObjectRole(const ScriptingObject* obj) { if (item.HasOwnership) role = item.Role; +#if USE_NETWORK_REPLICATOR_LOG + return role; +#else break; +#endif } } +#if USE_NETWORK_REPLICATOR_LOG + NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to get ownership of unregistered network object {} ({})", obj->GetID(), obj->GetType().ToString()); +#endif } } return role; @@ -913,10 +957,11 @@ NetworkObjectRole NetworkReplicator::GetObjectRole(const ScriptingObject* obj) void NetworkReplicator::SetObjectOwnership(ScriptingObject* obj, uint32 ownerClientId, NetworkObjectRole localRole, bool hierarchical) { - if (!obj) + if (!obj || NetworkManager::IsOffline()) return; + const Guid objectId = obj->GetID(); ScopeLock lock(ObjectsLock); - const auto it = Objects.Find(obj->GetID()); + const auto it = Objects.Find(objectId); if (it == Objects.End()) { // Special case if we're just spawning this object @@ -944,31 +989,33 @@ void NetworkReplicator::SetObjectOwnership(ScriptingObject* obj, uint32 ownerCli break; } } - return; - } - auto& item = it->Item; - if (item.Object != obj) - return; - - // Check if this client is object owner - if (item.OwnerClientId == NetworkManager::LocalClientId) - { - // Check if object owner will change - if (item.OwnerClientId != ownerClientId) - { - // Change role locally - CHECK(localRole != NetworkObjectRole::OwnedAuthoritative); - item.OwnerClientId = ownerClientId; - item.LastOwnerFrame = 1; - item.Role = localRole; - SendObjectRoleMessage(item); - } } else { - // Allow to change local role of the object (except ownership) - CHECK(localRole != NetworkObjectRole::OwnedAuthoritative); - item.Role = localRole; + auto& item = it->Item; + if (item.Object != obj) + return; + + // Check if this client is object owner + if (item.OwnerClientId == NetworkManager::LocalClientId) + { + // Check if object owner will change + if (item.OwnerClientId != ownerClientId) + { + // Change role locally + CHECK(localRole != NetworkObjectRole::OwnedAuthoritative); + item.OwnerClientId = ownerClientId; + item.LastOwnerFrame = 1; + item.Role = localRole; + SendObjectRoleMessage(item); + } + } + else + { + // Allow to change local role of the object (except ownership) + CHECK(localRole != NetworkObjectRole::OwnedAuthoritative); + item.Role = localRole; + } } // Go down hierarchy @@ -976,9 +1023,15 @@ void NetworkReplicator::SetObjectOwnership(ScriptingObject* obj, uint32 ownerCli { for (auto& e : Objects) { - if (e.Item.ParentId == item.ObjectId) + if (e.Item.ParentId == objectId) SetObjectOwnership(e.Item.Object.Get(), ownerClientId, localRole, hierarchical); } + + for (const SpawnItem& spawnItem : SpawnQueue) + { + if (IsParentOf(spawnItem.Object, obj)) + SetObjectOwnership(spawnItem.Object, ownerClientId, localRole, hierarchical); + } } } @@ -1182,9 +1235,11 @@ void NetworkInternal::NetworkReplicatorUpdate() { if (!q.HasOwnership && IsParentOf(q.Object, e.Object)) { + // Inherit ownership q.HasOwnership = true; q.Role = e.Role; q.OwnerClientId = e.OwnerClientId; + break; } } } @@ -1623,7 +1678,7 @@ void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkCl } } - // Setup all newly spawned objects + // Add all newly spawned objects for (int32 i = 0; i < msgData.ItemsCount; i++) { auto& msgDataItem = msgDataItems[i]; @@ -1652,6 +1707,16 @@ void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkCl // Boost future lookups by using indirection NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Remap object ID={} into object {}:{}", msgDataItem.ObjectId, item.ToString(), obj->GetType().ToString()); IdsRemappingTable.Add(msgDataItem.ObjectId, item.ObjectId); + } + + // Spawn all newly spawned objects (ensure to have valid ownership hierarchy set before spawning object) + for (int32 i = 0; i < msgData.ItemsCount; i++) + { + auto& msgDataItem = msgDataItems[i]; + ScriptingObject* obj = objects[i]; + auto it = Objects.Find(obj->GetID()); + auto& item = it->Item; + const NetworkReplicatedObject* parent = ResolveObject(msgDataItem.ParentId); // Automatic parenting for scene objects auto sceneObject = ScriptingObject::Cast(obj); diff --git a/Source/Engine/Networking/NetworkReplicator.h b/Source/Engine/Networking/NetworkReplicator.h index 389350e5f..0e9a2d0d8 100644 --- a/Source/Engine/Networking/NetworkReplicator.h +++ b/Source/Engine/Networking/NetworkReplicator.h @@ -68,7 +68,7 @@ public: /// Does nothing if network is offline. /// The object to replicate. /// The parent of the object (eg. player that spawned it). - API_FUNCTION() static void AddObject(ScriptingObject* obj, ScriptingObject* parent = nullptr); + API_FUNCTION() static void AddObject(ScriptingObject* obj, const ScriptingObject* parent = nullptr); /// /// Removes the object from the network replication system. @@ -80,14 +80,14 @@ public: /// /// Spawns the object to the other clients. Can be spawned by the owner who locally created it (eg. from prefab). /// - /// Does nothing if network is offline. + /// Does nothing if network is offline. Doesn't spawn actor in a level - but in network replication system. /// The object to spawn on other clients. API_FUNCTION() static void SpawnObject(ScriptingObject* obj); /// /// Spawns the object to the other clients. Can be spawned by the owner who locally created it (eg. from prefab). /// - /// Does nothing if network is offline. + /// Does nothing if network is offline. Doesn't spawn actor in a level - but in network replication system. /// The object to spawn on other clients. /// List with network client IDs that should receive network spawn event. Empty to spawn on all clients. API_FUNCTION() static void SpawnObject(ScriptingObject* obj, const DataContainer& clientIds); @@ -99,6 +99,13 @@ public: /// The object to despawn on other clients. API_FUNCTION() static void DespawnObject(ScriptingObject* obj); + /// + /// Checks if the network object is spawned or added to the network replication system. + /// + /// The network object. + /// True if object exists in networking, otherwise false. + API_FUNCTION() static bool HasObject(const ScriptingObject* obj); + /// /// Gets the Client Id of the network object owner. /// diff --git a/Source/Engine/Render2D/Font.h b/Source/Engine/Render2D/Font.h index d3806f0b4..3f42fd396 100644 --- a/Source/Engine/Render2D/Font.h +++ b/Source/Engine/Render2D/Font.h @@ -17,17 +17,17 @@ class FontAsset; /// /// The text range. /// -API_STRUCT() struct TextRange +API_STRUCT(NoDefault) struct TextRange { DECLARE_SCRIPTING_TYPE_MINIMAL(TextRange); /// - /// The start index. + /// The start index (inclusive). /// API_FIELD() int32 StartIndex; /// - /// The end index. + /// The end index (exclusive). /// API_FIELD() int32 EndIndex; @@ -70,7 +70,7 @@ DECLARE_SCRIPTING_TYPE_MINIMAL(TextRange); /// /// Gets the substring from the source text. /// - /// The text. + /// The text. /// The substring of the original text of the defined range. StringView Substring(const StringView& text) const { @@ -87,7 +87,7 @@ struct TIsPODType /// /// The font line info generated during text processing. /// -API_STRUCT() struct FontLineCache +API_STRUCT(NoDefault) struct FontLineCache { DECLARE_SCRIPTING_TYPE_MINIMAL(FontLineCache); @@ -151,7 +151,7 @@ struct TIsPODType /// /// The cached font character entry (read for rendering and further processing). /// -API_STRUCT() struct FontCharacterEntry +API_STRUCT(NoDefault) struct FontCharacterEntry { DECLARE_SCRIPTING_TYPE_MINIMAL(FontCharacterEntry); diff --git a/Source/Engine/UI/GUI/Common/Button.cs b/Source/Engine/UI/GUI/Common/Button.cs index da6fc1529..8ab6366eb 100644 --- a/Source/Engine/UI/GUI/Common/Button.cs +++ b/Source/Engine/UI/GUI/Common/Button.cs @@ -10,7 +10,7 @@ namespace FlaxEngine.GUI public class Button : ContainerControl { /// - /// The default height fro the buttons. + /// The default height for the buttons. /// public const float DefaultHeight = 24.0f;