diff --git a/Source/Editor/Content/Items/ContentItem.cs b/Source/Editor/Content/Items/ContentItem.cs index 4fe75d2c2..66825fb42 100644 --- a/Source/Editor/Content/Items/ContentItem.cs +++ b/Source/Editor/Content/Items/ContentItem.cs @@ -360,7 +360,7 @@ namespace FlaxEditor.Content } /// - /// Updates the tooltip text. + /// Updates the tooltip text text. /// public virtual void UpdateTooltipText() { @@ -384,7 +384,8 @@ namespace FlaxEditor.Content protected virtual void OnBuildTooltipText(StringBuilder sb) { sb.Append("Type: ").Append(TypeDescription).AppendLine(); - sb.Append("Size: ").Append(Utilities.Utils.FormatBytesCount((int)new FileInfo(Path).Length)).AppendLine(); + if (File.Exists(Path)) + sb.Append("Size: ").Append(Utilities.Utils.FormatBytesCount((int)new FileInfo(Path).Length)).AppendLine(); sb.Append("Path: ").Append(Utilities.Utils.GetAssetNamePathWithExt(Path)).AppendLine(); } @@ -718,7 +719,7 @@ namespace FlaxEditor.Content public override bool OnMouseDoubleClick(Float2 location, MouseButton button) { Focus(); - + // Open (Parent as ContentView).OnItemDoubleClick(this); diff --git a/Source/Editor/Content/Items/NewItem.cs b/Source/Editor/Content/Items/NewItem.cs index 94d95f15b..7040adf44 100644 --- a/Source/Editor/Content/Items/NewItem.cs +++ b/Source/Editor/Content/Items/NewItem.cs @@ -1,5 +1,6 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. +using System.Text; using FlaxEngine; namespace FlaxEditor.Content @@ -47,5 +48,11 @@ namespace FlaxEditor.Content /// protected override bool DrawShadow => true; + + /// + public override void UpdateTooltipText() + { + TooltipText = null; + } } } diff --git a/Source/Editor/CustomEditors/CustomEditor.cs b/Source/Editor/CustomEditors/CustomEditor.cs index 1ece31f4d..03b21e12b 100644 --- a/Source/Editor/CustomEditors/CustomEditor.cs +++ b/Source/Editor/CustomEditors/CustomEditor.cs @@ -148,7 +148,7 @@ namespace FlaxEditor.CustomEditors return; // Special case for root objects to run normal layout build - if (_presenter.Selection == Values) + if (_presenter != null && _presenter.Selection == Values) { _presenter.BuildLayout(); return; @@ -159,7 +159,7 @@ namespace FlaxEditor.CustomEditors var layout = _layout; var control = layout.ContainerControl; var parent = _parent; - var parentScrollV = (_presenter.Panel.Parent as Panel)?.VScrollBar?.Value ?? -1; + var parentScrollV = (_presenter?.Panel.Parent as Panel)?.VScrollBar?.Value ?? -1; control.IsLayoutLocked = true; control.DisposeChildren(); @@ -249,6 +249,28 @@ namespace FlaxEditor.CustomEditors internal virtual void RefreshRootChild() { + // Check if need to update value + if (_hasValueDirty) + { + IsSettingValue = true; + try + { + // Cleanup (won't retry update in case of exception) + object val = _valueToSet; + _hasValueDirty = false; + _valueToSet = null; + + // Assign value + for (int i = 0; i < _values.Count; i++) + _values[i] = val; + } + finally + { + OnUnDirty(); + IsSettingValue = false; + } + } + Refresh(); for (int i = 0; i < _children.Count; i++) diff --git a/Source/Editor/CustomEditors/CustomEditorsUtil.cs b/Source/Editor/CustomEditors/CustomEditorsUtil.cs index 4c664031f..5f76b07e2 100644 --- a/Source/Editor/CustomEditors/CustomEditorsUtil.cs +++ b/Source/Editor/CustomEditors/CustomEditorsUtil.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Runtime.InteropServices.Marshalling; +using FlaxEditor.CustomEditors.Dedicated; using FlaxEditor.CustomEditors.Editors; using FlaxEditor.Scripting; using FlaxEngine; @@ -110,7 +111,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(); @@ -121,6 +122,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 6a2986d2b..f9325fb26 100644 --- a/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs @@ -21,7 +21,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 41dc1dc08..bf82e19da 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/CustomEditors/Editors/DictionaryEditor.cs b/Source/Editor/CustomEditors/Editors/DictionaryEditor.cs index f26bf25a4..73089ff04 100644 --- a/Source/Editor/CustomEditors/Editors/DictionaryEditor.cs +++ b/Source/Editor/CustomEditors/Editors/DictionaryEditor.cs @@ -2,7 +2,6 @@ using System; using System.Collections; -using System.Collections.Generic; using System.Linq; using FlaxEditor.CustomEditors.Elements; using FlaxEditor.CustomEditors.GUI; @@ -46,8 +45,8 @@ namespace FlaxEditor.CustomEditors.Editors private void OnSetupContextMenu(PropertyNameLabel label, ContextMenu menu, CustomEditor linkedEditor) { - menu.AddSeparator(); - + if (menu.Items.Any()) + menu.AddSeparator(); menu.AddButton("Remove", OnRemoveClicked).Enabled = !_editor._readOnly; menu.AddButton("Edit", OnEditClicked).Enabled = _editor._canEditKeys; } @@ -62,6 +61,7 @@ namespace FlaxEditor.CustomEditors.Editors var keyType = _editor.Values.Type.GetGenericArguments()[0]; if (keyType == typeof(string) || keyType.IsPrimitive) { + // Edit as text var popup = RenamePopup.Show(Parent, Rectangle.Margin(Bounds, Margin), Text, false); popup.Validate += (renamePopup, value) => { @@ -79,7 +79,6 @@ namespace FlaxEditor.CustomEditors.Editors newKey = JsonSerializer.Deserialize(renamePopup.Text, keyType); else newKey = renamePopup.Text; - _editor.ChangeKey(_key, newKey); _key = newKey; Text = _key.ToString(); @@ -87,6 +86,7 @@ namespace FlaxEditor.CustomEditors.Editors } else if (keyType.IsEnum) { + // Edit via enum picker var popup = RenamePopup.Show(Parent, Rectangle.Margin(Bounds, Margin), Text, false); var picker = new EnumComboBox(keyType) { @@ -109,7 +109,21 @@ namespace FlaxEditor.CustomEditors.Editors } else { - throw new NotImplementedException("Missing editing for dictionary key type " + keyType); + // Generic editor + var popup = ContextMenuBase.ShowEmptyMenu(Parent, Rectangle.Margin(Bounds, Margin)); + var presenter = new CustomEditorPresenter(null); + presenter.Panel.AnchorPreset = AnchorPresets.StretchAll; + presenter.Panel.IsScrollable = false; + presenter.Panel.Parent = popup; + presenter.Select(_key); + presenter.Modified += () => + { + popup.Hide(); + object newKey = presenter.Selection[0]; + _editor.ChangeKey(_key, newKey); + _key = newKey; + Text = _key?.ToString(); + }; } } @@ -160,7 +174,7 @@ namespace FlaxEditor.CustomEditors.Editors var argTypes = type.GetGenericArguments(); var keyType = argTypes[0]; var valueType = argTypes[1]; - _canEditKeys = keyType == typeof(string) || keyType.IsPrimitive || keyType.IsEnum; + _canEditKeys = keyType == typeof(string) || keyType.IsPrimitive || keyType.IsEnum || keyType.IsValueType; _background = FlaxEngine.GUI.Style.Current.CollectionBackgroundColor; _readOnly = false; _notNullItems = false; @@ -383,6 +397,7 @@ namespace FlaxEditor.CustomEditors.Editors int newItemsLeft = newSize - oldSize; while (newItemsLeft-- > 0) { + object newKey = null; if (keyType.IsPrimitive) { long uniqueKey = 0; @@ -401,8 +416,7 @@ namespace FlaxEditor.CustomEditors.Editors } } } while (!isUnique); - - newValues[Convert.ChangeType(uniqueKey, keyType)] = TypeUtils.GetDefaultValue(new ScriptType(valueType)); + newKey = Convert.ChangeType(uniqueKey, keyType); } else if (keyType.IsEnum) { @@ -422,8 +436,7 @@ namespace FlaxEditor.CustomEditors.Editors } } } while (!isUnique && uniqueKeyIndex < enumValues.Length); - - newValues[enumValues.GetValue(uniqueKeyIndex)] = TypeUtils.GetDefaultValue(new ScriptType(valueType)); + newKey = enumValues.GetValue(uniqueKeyIndex); } else if (keyType == typeof(string)) { @@ -442,13 +455,13 @@ namespace FlaxEditor.CustomEditors.Editors } } } while (!isUnique); - - newValues[uniqueKey] = TypeUtils.GetDefaultValue(new ScriptType(valueType)); + newKey = uniqueKey; } else { - throw new InvalidOperationException(); + newKey = TypeUtils.GetDefaultValue(new ScriptType(keyType)); } + newValues[newKey] = TypeUtils.GetDefaultValue(new ScriptType(valueType)); } SetValue(newValues); diff --git a/Source/Editor/CustomEditors/Editors/TypeEditor.cs b/Source/Editor/CustomEditors/Editors/TypeEditor.cs index eb0c839ed..ab17c4ae1 100644 --- a/Source/Editor/CustomEditors/Editors/TypeEditor.cs +++ b/Source/Editor/CustomEditors/Editors/TypeEditor.cs @@ -394,11 +394,8 @@ namespace FlaxEditor.CustomEditors.Editors if (_element != null) { _element.CustomControl.ValueChanged += () => SetValue(_element.CustomControl.Value.Type); - if (_element.CustomControl.Type == ScriptType.Object) - { _element.CustomControl.Type = Values.Type.Type != typeof(object) || Values[0] == null ? ScriptType.Object : TypeUtils.GetObjectType(Values[0]); - } } } @@ -408,9 +405,7 @@ namespace FlaxEditor.CustomEditors.Editors base.Refresh(); if (!HasDifferentValues) - { _element.CustomControl.Value = new ScriptType(Values[0] as Type); - } } } @@ -426,9 +421,7 @@ namespace FlaxEditor.CustomEditors.Editors base.Initialize(layout); if (_element != null) - { _element.CustomControl.ValueChanged += () => SetValue(_element.CustomControl.Value); - } } /// @@ -437,9 +430,32 @@ namespace FlaxEditor.CustomEditors.Editors base.Refresh(); if (!HasDifferentValues) - { _element.CustomControl.Value = (ScriptType)Values[0]; - } + } + } + + /// + /// Default implementation of the inspector used to edit reference to the . Used to pick classes. + /// + [CustomEditor(typeof(SoftTypeReference)), DefaultEditor] + public class SoftTypeReferenceEditor : TypeEditorBase + { + /// + public override void Initialize(LayoutElementsContainer layout) + { + base.Initialize(layout); + + if (_element != null) + _element.CustomControl.ValueChanged += () => SetValue(new SoftTypeReference(_element.CustomControl.ValueTypeName)); + } + + /// + public override void Refresh() + { + base.Refresh(); + + if (!HasDifferentValues) + _element.CustomControl.ValueTypeName = ((SoftTypeReference)Values[0]).TypeName; } } diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs index 27d47345d..7ecd197eb 100644 --- a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs +++ b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs @@ -110,6 +110,25 @@ namespace FlaxEditor.GUI.ContextMenu _isSubMenu = true; } + /// + /// Shows the empty menu popup o na screen. + /// + /// The target control. + /// The target control area to cover. + /// Created popup. + public static ContextMenuBase ShowEmptyMenu(Control control, Rectangle area) + { + // Calculate the control size in the window space to handle scaled controls + var upperLeft = control.PointToWindow(area.UpperLeft); + var bottomRight = control.PointToWindow(area.BottomRight); + var size = bottomRight - upperLeft; + + var popup = new ContextMenuBase(); + popup.Size = size; + popup.Show(control, area.Location + new Float2(0, (size.Y - popup.Height) * 0.5f)); + return popup; + } + /// /// Show context menu over given control. /// diff --git a/Source/Editor/Windows/OutputLogWindow.cs b/Source/Editor/Windows/OutputLogWindow.cs index 91da504e8..0b2249a33 100644 --- a/Source/Editor/Windows/OutputLogWindow.cs +++ b/Source/Editor/Windows/OutputLogWindow.cs @@ -542,7 +542,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) @@ -551,7 +551,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/Editor/Windows/Profiler/Network.cs b/Source/Editor/Windows/Profiler/Network.cs index 662ccf445..f0b93a03c 100644 --- a/Source/Editor/Windows/Profiler/Network.cs +++ b/Source/Editor/Windows/Profiler/Network.cs @@ -52,7 +52,7 @@ namespace FlaxEditor.Windows.Profiler private static string FormatSampleBytes(float v) { - return (uint)v + " bytes"; + return Utilities.Utils.FormatBytesCount((ulong)v); } /// diff --git a/Source/Engine/Core/Collections/BitArray.h b/Source/Engine/Core/Collections/BitArray.h index 6058dc8a5..01238d434 100644 --- a/Source/Engine/Core/Collections/BitArray.h +++ b/Source/Engine/Core/Collections/BitArray.h @@ -214,7 +214,7 @@ public: /// /// The index of the item. /// The value to set. - void Set(int32 index, bool value) const + void Set(int32 index, bool value) { ASSERT(index >= 0 && index < _count); const ItemType offset = index / sizeof(ItemType); diff --git a/Source/Engine/Core/Delegate.h b/Source/Engine/Core/Delegate.h index 21a4374ea..4fcf60390 100644 --- a/Source/Engine/Core/Delegate.h +++ b/Source/Engine/Core/Delegate.h @@ -203,23 +203,12 @@ public: return _function != nullptr; } - /// - /// Calls the binded function if any has been assigned. - /// - /// A list of parameters for the function invocation. - /// Function result - void TryCall(Params ... params) const - { - if (_function) - _function(_callee, Forward(params)...); - } - /// /// Calls the binded function (it must be assigned). /// /// A list of parameters for the function invocation. /// Function result - ReturnType operator()(Params ... params) const + FORCE_INLINE ReturnType operator()(Params ... params) const { ASSERT(_function); return _function(_callee, Forward(params)...); 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/NetworkPeer.cpp b/Source/Engine/Networking/NetworkPeer.cpp index 342cba674..d86824156 100644 --- a/Source/Engine/Networking/NetworkPeer.cpp +++ b/Source/Engine/Networking/NetworkPeer.cpp @@ -6,6 +6,7 @@ #include "Engine/Core/Log.h" #include "Engine/Core/Math/Math.h" #include "Engine/Platform/CPUInfo.h" +#include "Engine/Profiler/ProfilerCPU.h" namespace { @@ -131,6 +132,7 @@ void NetworkPeer::Disconnect(const NetworkConnection& connection) bool NetworkPeer::PopEvent(NetworkEvent& eventRef) { + PROFILE_CPU(); return NetworkDriver->PopEvent(&eventRef); } diff --git a/Source/Engine/Networking/NetworkReplicationHierarchy.cpp b/Source/Engine/Networking/NetworkReplicationHierarchy.cpp new file mode 100644 index 000000000..d86acfc7f --- /dev/null +++ b/Source/Engine/Networking/NetworkReplicationHierarchy.cpp @@ -0,0 +1,205 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +#include "NetworkReplicationHierarchy.h" +#include "NetworkManager.h" +#include "Engine/Level/Actor.h" +#include "Engine/Level/SceneObject.h" + +uint16 NetworkReplicationNodeObjectCounter = 0; +NetworkClientsMask NetworkClientsMask::All = { MAX_uint64, MAX_uint64 }; + +Actor* NetworkReplicationHierarchyObject::GetActor() const +{ + auto* actor = ScriptingObject::Cast(Object); + if (!actor) + { + if (const auto* sceneObject = ScriptingObject::Cast(Object)) + actor = sceneObject->GetParent(); + } + return actor; +} + +void NetworkReplicationHierarchyUpdateResult::Init() +{ + _clientsHaveLocation = false; + _clients.Resize(NetworkManager::Clients.Count()); + _clientsMask = NetworkClientsMask(); + for (int32 i = 0; i < _clients.Count(); i++) + _clientsMask.SetBit(i); + _entries.Clear(); + ReplicationScale = 1.0f; +} + +void NetworkReplicationHierarchyUpdateResult::SetClientLocation(int32 clientIndex, const Vector3& location) +{ + CHECK(clientIndex >= 0 && clientIndex < _clients.Count()); + _clientsHaveLocation = true; + Client& client = _clients[clientIndex]; + client.HasLocation = true; + client.Location = location; +} + +bool NetworkReplicationHierarchyUpdateResult::GetClientLocation(int32 clientIndex, Vector3& location) const +{ + CHECK_RETURN(clientIndex >= 0 && clientIndex < _clients.Count(), false); + const Client& client = _clients[clientIndex]; + location = client.Location; + return client.HasLocation; +} + +void NetworkReplicationNode::AddObject(NetworkReplicationHierarchyObject obj) +{ + if (obj.ReplicationFPS > 0.0f) + { + // Randomize initial replication update to spread rep rates more evenly for large scenes that register all objects within the same frame + obj.ReplicationUpdatesLeft = NetworkReplicationNodeObjectCounter++ % Math::Clamp(Math::RoundToInt(NetworkManager::NetworkFPS / obj.ReplicationFPS), 1, 60); + } + + Objects.Add(obj); +} + +bool NetworkReplicationNode::RemoveObject(ScriptingObject* obj) +{ + return !Objects.Remove(obj); +} + +bool NetworkReplicationNode::DirtyObject(ScriptingObject* obj) +{ + const int32 index = Objects.Find(obj); + if (index != -1) + { + NetworkReplicationHierarchyObject& e = Objects[index]; + e.ReplicationUpdatesLeft = 0; + } + return index != -1; +} + +void NetworkReplicationNode::Update(NetworkReplicationHierarchyUpdateResult* result) +{ + CHECK(result); + const float networkFPS = NetworkManager::NetworkFPS / result->ReplicationScale; + for (NetworkReplicationHierarchyObject& obj : Objects) + { + if (obj.ReplicationFPS <= 0.0f) + { + // Always relevant + result->AddObject(obj.Object); + } + else if (obj.ReplicationUpdatesLeft > 0) + { + // Move to the next frame + obj.ReplicationUpdatesLeft--; + } + else + { + NetworkClientsMask targetClients = result->GetClientsMask(); + if (result->_clientsHaveLocation && obj.CullDistance > 0.0f) + { + // Cull object against viewers locations + if (const Actor* actor = obj.GetActor()) + { + const Vector3 objPosition = actor->GetPosition(); + const Real cullDistanceSq = Math::Square(obj.CullDistance); + for (int32 clientIndex = 0; clientIndex < result->_clients.Count(); clientIndex++) + { + const auto& client = result->_clients[clientIndex]; + if (client.HasLocation) + { + const Real distanceSq = Vector3::DistanceSquared(objPosition, client.Location); + // TODO: scale down replication FPS when object is far away from all clients (eg. by 10-50%) + if (distanceSq >= cullDistanceSq) + { + // Object is too far from this viewer so don't send data to him + targetClients.UnsetBit(clientIndex); + } + } + } + } + } + if (targetClients && obj.Object) + { + // Replicate this frame + result->AddObject(obj.Object, targetClients); + } + + // Calculate frames until next replication + obj.ReplicationUpdatesLeft = (uint16)Math::Clamp(Math::RoundToInt(networkFPS / obj.ReplicationFPS) - 1, 0, MAX_uint16); + } + } +} + +NetworkReplicationGridNode::~NetworkReplicationGridNode() +{ + for (const auto& e : _children) + Delete(e.Value.Node); +} + +void NetworkReplicationGridNode::AddObject(NetworkReplicationHierarchyObject obj) +{ + // Chunk actors locations into a grid coordinates + Int3 coord = Int3::Zero; + if (const Actor* actor = obj.GetActor()) + { + coord = actor->GetPosition() / CellSize; + } + + Cell* cell = _children.TryGet(coord); + if (!cell) + { + // Allocate new cell + cell = &_children[coord]; + cell->Node = New(); + cell->MinCullDistance = obj.CullDistance; + } + cell->Node->AddObject(obj); + + // Cache minimum culling distance for a whole cell to skip it at once + cell->MinCullDistance = Math::Min(cell->MinCullDistance, obj.CullDistance); +} + +bool NetworkReplicationGridNode::RemoveObject(ScriptingObject* obj) +{ + for (const auto& e : _children) + { + if (e.Value.Node->RemoveObject(obj)) + { + // TODO: remove empty cells? + // TODO: update MinCullDistance for cell? + return true; + } + } + return false; +} + +void NetworkReplicationGridNode::Update(NetworkReplicationHierarchyUpdateResult* result) +{ + CHECK(result); + if (result->_clientsHaveLocation) + { + // Update only cells within a range + const Real cellRadiusSq = Math::Square(CellSize * 1.414f); + for (const auto& e : _children) + { + const Vector3 cellPosition = (e.Key * CellSize) + (CellSize * 0.5f); + Real distanceSq = MAX_Real; + for (auto& client : result->_clients) + { + if (client.HasLocation) + distanceSq = Math::Min(distanceSq, Vector3::DistanceSquared(cellPosition, client.Location)); + } + const Real minCullDistanceSq = Math::Square(e.Value.MinCullDistance); + if (distanceSq < minCullDistanceSq + cellRadiusSq) + { + e.Value.Node->Update(result); + } + } + } + else + { + // Brute-force over all cells + for (const auto& e : _children) + { + e.Value.Node->Update(result); + } + } +} diff --git a/Source/Engine/Networking/NetworkReplicationHierarchy.cs b/Source/Engine/Networking/NetworkReplicationHierarchy.cs new file mode 100644 index 000000000..f03419275 --- /dev/null +++ b/Source/Engine/Networking/NetworkReplicationHierarchy.cs @@ -0,0 +1,25 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +namespace FlaxEngine.Networking +{ + partial struct NetworkReplicationHierarchyObject + { + /// + /// Gets the actors context (object itself or parent actor). + /// + public Actor Actor + { + get + { + var actor = Object as Actor; + if (actor == null) + { + var sceneObject = Object as SceneObject; + if (sceneObject != null) + actor = sceneObject.Parent; + } + return actor; + } + } + } +} diff --git a/Source/Engine/Networking/NetworkReplicationHierarchy.h b/Source/Engine/Networking/NetworkReplicationHierarchy.h new file mode 100644 index 000000000..55cd6b7b1 --- /dev/null +++ b/Source/Engine/Networking/NetworkReplicationHierarchy.h @@ -0,0 +1,260 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +#pragma once + +#include "Types.h" +#include "Engine/Core/Math/Vector3.h" +#include "Engine/Core/Collections/Array.h" +#include "Engine/Core/Collections/Dictionary.h" +#include "Engine/Scripting/ScriptingObject.h" +#include "Engine/Scripting/ScriptingObjectReference.h" + +class Actor; + +/// +/// Network replication hierarchy object data. +/// +API_STRUCT(NoDefault, Namespace = "FlaxEngine.Networking") struct FLAXENGINE_API NetworkReplicationHierarchyObject +{ + DECLARE_SCRIPTING_TYPE_MINIMAL(NetworkReplicationObjectInfo); + + // The object to replicate. + API_FIELD() ScriptingObjectReference Object; + // The target amount of the replication updates per second (frequency of the replication). Constrained by NetworkManager::NetworkFPS. Use 0 for 'always relevant' object. + API_FIELD() float ReplicationFPS = 60; + // The minimum distance from the player to the object at which it can process replication. For example, players further away won't receive object data. Use 0 if unused. + API_FIELD() float CullDistance = 15000; + // Runtime value for update frames left for the next replication of this object. Matches NetworkManager::NetworkFPS calculated from ReplicationFPS. + API_FIELD(Attributes="HideInEditor") uint16 ReplicationUpdatesLeft = 0; + + FORCE_INLINE NetworkReplicationHierarchyObject(const ScriptingObjectReference& obj) + : Object(obj.Get()) + { + } + + FORCE_INLINE NetworkReplicationHierarchyObject(ScriptingObject* obj = nullptr) + : Object(obj) + { + } + + // Gets the actors context (object itself or parent actor). + Actor* GetActor() const; + + bool operator==(const NetworkReplicationHierarchyObject& other) const + { + return Object == other.Object; + } + + bool operator==(const ScriptingObject* other) const + { + return Object == other; + } +}; + +inline uint32 GetHash(const NetworkReplicationHierarchyObject& key) +{ + return GetHash(key.Object); +} + +/// +/// Bit mask for NetworkClient list (eg. to selectively send object replication). +/// +API_STRUCT(NoDefault, Namespace = "FlaxEngine.Networking") struct FLAXENGINE_API NetworkClientsMask +{ + DECLARE_SCRIPTING_TYPE_MINIMAL(NetworkClientsMask); + + // The first 64 bits (each for one client). + API_FIELD() uint64 Word0 = 0; + // The second 64 bits (each for one client). + API_FIELD() uint64 Word1 = 0; + + // All bits set for all clients. + API_FIELD() static NetworkClientsMask All; + + FORCE_INLINE bool HasBit(int32 bitIndex) const + { + const int32 wordIndex = bitIndex / 64; + const uint64 wordMask = 1ull << (uint64)(bitIndex - wordIndex * 64); + const uint64 word = *(&Word0 + wordIndex); + return (word & wordMask) == wordMask; + } + + FORCE_INLINE void SetBit(int32 bitIndex) + { + const int32 wordIndex = bitIndex / 64; + const uint64 wordMask = 1ull << (uint64)(bitIndex - wordIndex * 64); + uint64& word = *(&Word0 + wordIndex); + word |= wordMask; + } + + FORCE_INLINE void UnsetBit(int32 bitIndex) + { + const int32 wordIndex = bitIndex / 64; + const uint64 wordMask = 1ull << (uint64)(bitIndex - wordIndex * 64); + uint64& word = *(&Word0 + wordIndex); + word &= ~wordMask; + } + + FORCE_INLINE operator bool() const + { + return Word0 + Word1 != 0; + } + + bool operator==(const NetworkClientsMask& other) const + { + return Word0 == other.Word0 && Word1 == other.Word1; + } +}; + +/// +/// Network replication hierarchy output data to send. +/// +API_CLASS(Namespace = "FlaxEngine.Networking") class FLAXENGINE_API NetworkReplicationHierarchyUpdateResult : public ScriptingObject +{ + DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(NetworkReplicationHierarchyUpdateResult, ScriptingObject); + friend class NetworkInternal; + friend class NetworkReplicationNode; + friend class NetworkReplicationGridNode; + +private: + struct Client + { + bool HasLocation; + Vector3 Location; + }; + + struct Entry + { + ScriptingObject* Object; + NetworkClientsMask TargetClients; + }; + + bool _clientsHaveLocation; + NetworkClientsMask _clientsMask; + Array _clients; + Array _entries; + + void Init(); + +public: + // Scales the ReplicationFPS property of objects in hierarchy. Can be used to slow down or speed up replication rate. + API_FIELD() float ReplicationScale = 1.0f; + + // Adds object to the update results. + API_FUNCTION() void AddObject(ScriptingObject* obj) + { + Entry& e = _entries.AddOne(); + e.Object = obj; + e.TargetClients = NetworkClientsMask::All; + } + + // Adds object to the update results. Defines specific clients to receive the update (server-only, unused on client). Mask matches NetworkManager::Clients. + API_FUNCTION() void AddObject(ScriptingObject* obj, NetworkClientsMask targetClients) + { + Entry& e = _entries.AddOne(); + e.Object = obj; + e.TargetClients = targetClients; + } + + // Gets amount of the clients to use. Matches NetworkManager::Clients. + API_PROPERTY() int32 GetClientsCount() const + { + return _clients.Count(); + } + + // Gets mask with all client bits set. Matches NetworkManager::Clients. + API_PROPERTY() NetworkClientsMask GetClientsMask() const + { + return _clientsMask; + } + + // Sets the viewer location for a certain client. Client index must match NetworkManager::Clients. + API_FUNCTION() void SetClientLocation(int32 clientIndex, const Vector3& location); + + // Gets the viewer location for a certain client. Client index must match NetworkManager::Clients. Returns true if got a location set, otherwise false. + API_FUNCTION() bool GetClientLocation(int32 clientIndex, API_PARAM(out) Vector3& location) const; +}; + +/// +/// Base class for the network objects replication hierarchy nodes. Contains a list of objects. +/// +API_CLASS(Abstract, Namespace = "FlaxEngine.Networking") class FLAXENGINE_API NetworkReplicationNode : public ScriptingObject +{ + DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(NetworkReplicationNode, ScriptingObject); + + /// + /// List with objects stored in this node. + /// + API_FIELD() Array Objects; + + /// + /// Adds an object into the hierarchy. + /// + /// The object to add. + API_FUNCTION() virtual void AddObject(NetworkReplicationHierarchyObject obj); + + /// + /// Removes object from the hierarchy. + /// + /// The object to remove. + /// True on successful removal, otherwise false. + API_FUNCTION() virtual bool RemoveObject(ScriptingObject* obj); + + /// + /// Force replicates the object during the next update. Resets any internal tracking state to force the synchronization. + /// + /// The object to update. + /// True on successful update, otherwise false. + API_FUNCTION() virtual bool DirtyObject(ScriptingObject* obj); + + /// + /// Iterates over all objects and adds them to the replication work. + /// + /// The update results container. + API_FUNCTION() virtual void Update(NetworkReplicationHierarchyUpdateResult* result); +}; + +inline uint32 GetHash(const Int3& key) +{ + uint32 hash = GetHash(key.X); + CombineHash(hash, GetHash(key.Y)); + CombineHash(hash, GetHash(key.Z)); + return hash; +} + +/// +/// Network replication hierarchy node with 3D grid spatialization. Organizes static objects into chunks to improve performance in large worlds. +/// +API_CLASS(Namespace = "FlaxEngine.Networking") class FLAXENGINE_API NetworkReplicationGridNode : public NetworkReplicationNode +{ + DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(NetworkReplicationGridNode, NetworkReplicationNode); + ~NetworkReplicationGridNode(); + +private: + struct Cell + { + NetworkReplicationNode* Node; + float MinCullDistance; + }; + + Dictionary _children; + +public: + /// + /// Size of the grid cell (in world units). Used to chunk the space for separate nodes. + /// + API_FIELD() float CellSize = 10000.0f; + + void AddObject(NetworkReplicationHierarchyObject obj) override; + bool RemoveObject(ScriptingObject* obj) override; + void Update(NetworkReplicationHierarchyUpdateResult* result) override; +}; + +/// +/// Defines the network objects replication hierarchy (tree structure) that controls chunking and configuration of the game objects replication. +/// Contains only 'owned' objects. It's used by the networking system only on a main thread. +/// +API_CLASS(Namespace = "FlaxEngine.Networking") class FLAXENGINE_API NetworkReplicationHierarchy : public NetworkReplicationNode +{ + DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(NetworkReplicationHierarchy, NetworkReplicationNode); +}; diff --git a/Source/Engine/Networking/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp index a61d7577f..6813eeb54 100644 --- a/Source/Engine/Networking/NetworkReplicator.cpp +++ b/Source/Engine/Networking/NetworkReplicator.cpp @@ -12,6 +12,7 @@ #include "NetworkRpc.h" #include "INetworkSerializable.h" #include "INetworkObject.h" +#include "NetworkReplicationHierarchy.h" #include "Engine/Core/Collections/HashSet.h" #include "Engine/Core/Collections/Dictionary.h" #include "Engine/Core/Collections/ChunkedArray.h" @@ -199,6 +200,8 @@ namespace Dictionary IdsRemappingTable; NetworkStream* CachedWriteStream = nullptr; NetworkStream* CachedReadStream = nullptr; + NetworkReplicationHierarchyUpdateResult* CachedReplicationResult = nullptr; + NetworkReplicationHierarchy* Hierarchy = nullptr; Array NewClients; Array CachedTargets; Dictionary SerializersTable; @@ -307,14 +310,15 @@ void BuildCachedTargets(const Array& clients, const NetworkClien } } -void BuildCachedTargets(const Array& clients, const DataContainer& clientIds, const uint32 excludedClientId = NetworkManager::ServerClientId) +void BuildCachedTargets(const Array& clients, const DataContainer& clientIds, const uint32 excludedClientId = NetworkManager::ServerClientId, const NetworkClientsMask clientsMask = NetworkClientsMask::All) { CachedTargets.Clear(); if (clientIds.IsValid()) { - for (const NetworkClient* client : clients) + for (int32 clientIndex = 0; clientIndex < clients.Count(); clientIndex++) { - if (client->State == NetworkConnectionState::Connected && client->ClientId != excludedClientId) + const NetworkClient* client = clients.Get()[clientIndex]; + if (client->State == NetworkConnectionState::Connected && client->ClientId != excludedClientId && clientsMask.HasBit(clientIndex)) { for (int32 i = 0; i < clientIds.Length(); i++) { @@ -329,9 +333,10 @@ void BuildCachedTargets(const Array& clients, const DataContaine } else { - for (const NetworkClient* client : clients) + for (int32 clientIndex = 0; clientIndex < clients.Count(); clientIndex++) { - if (client->State == NetworkConnectionState::Connected && client->ClientId != excludedClientId) + const NetworkClient* client = clients.Get()[clientIndex]; + if (client->State == NetworkConnectionState::Connected && client->ClientId != excludedClientId && clientsMask.HasBit(clientIndex)) CachedTargets.Add(client->Connection); } } @@ -377,10 +382,10 @@ void BuildCachedTargets(const Array& clients, const DataContaine } } -FORCE_INLINE void BuildCachedTargets(const NetworkReplicatedObject& item) +FORCE_INLINE void BuildCachedTargets(const NetworkReplicatedObject& item, const NetworkClientsMask clientsMask = NetworkClientsMask::All) { // By default send object to all connected clients excluding the owner but with optional TargetClientIds list - BuildCachedTargets(NetworkManager::Clients, item.TargetClientIds, item.OwnerClientId); + BuildCachedTargets(NetworkManager::Clients, item.TargetClientIds, item.OwnerClientId, clientsMask); } FORCE_INLINE void GetNetworkName(char buffer[128], const StringAnsiView& name) @@ -561,9 +566,10 @@ void FindObjectsForSpawn(SpawnGroup& group, ChunkedArray& spawnI } } -void DirtyObjectImpl(NetworkReplicatedObject& item, ScriptingObject* obj) +FORCE_INLINE void DirtyObjectImpl(NetworkReplicatedObject& item, ScriptingObject* obj) { - // TODO: implement objects state replication frequency and dirtying + if (Hierarchy) + Hierarchy->DirtyObject(obj); } template @@ -703,6 +709,34 @@ StringAnsiView NetworkReplicator::GetCSharpCachedName(const StringAnsiView& name #endif +NetworkReplicationHierarchy* NetworkReplicator::GetHierarchy() +{ + return Hierarchy; +} + +void NetworkReplicator::SetHierarchy(NetworkReplicationHierarchy* value) +{ + ScopeLock lock(ObjectsLock); + if (Hierarchy == value) + return; + NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Set hierarchy to '{}'", value ? value->ToString() : String::Empty); + if (Hierarchy) + { + // Clear old hierarchy + Delete(Hierarchy); + } + Hierarchy = value; + if (value) + { + // Add all owned objects to the hierarchy + for (auto& e : Objects) + { + if (e.Item.Object && e.Item.Role == NetworkObjectRole::OwnedAuthoritative) + value->AddObject(e.Item.Object); + } + } +} + void NetworkReplicator::AddSerializer(const ScriptingTypeHandle& typeHandle, SerializeFunc serialize, SerializeFunc deserialize, void* serializeTag, void* deserializeTag) { if (!typeHandle) @@ -745,7 +779,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,7 +808,22 @@ 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)); + if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative) + Hierarchy->AddObject(obj); } void NetworkReplicator::RemoveObject(ScriptingObject* obj) @@ -788,6 +837,8 @@ void NetworkReplicator::RemoveObject(ScriptingObject* obj) // Remove object from the list NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Remove object {}, owned by {}", obj->GetID().ToString(), it->Item.ParentId.ToString()); + if (Hierarchy && it->Item.Role == NetworkObjectRole::OwnedAuthoritative) + Hierarchy->RemoveObject(obj); Objects.Remove(it); } @@ -857,14 +908,33 @@ void NetworkReplicator::DespawnObject(ScriptingObject* obj) DespawnedObjects.Add(item.ObjectId); if (item.AsNetworkObject) item.AsNetworkObject->OnNetworkDespawn(); + if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative) + Hierarchy->RemoveObject(obj); Objects.Remove(it); 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 +948,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 +966,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 +980,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 +997,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 +1029,37 @@ 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); + if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative) + Hierarchy->RemoveObject(obj); + 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); + if (Hierarchy && it->Item.Role == NetworkObjectRole::OwnedAuthoritative) + Hierarchy->RemoveObject(obj); + item.Role = localRole; + } } // Go down hierarchy @@ -976,9 +1067,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); + } } } @@ -1054,6 +1151,8 @@ void NetworkInternal::NetworkReplicatorClientDisconnected(NetworkClient* client) // Delete object locally NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Despawn object {}", item.ObjectId); + if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative) + Hierarchy->RemoveObject(obj); if (item.AsNetworkObject) item.AsNetworkObject->OnNetworkDespawn(); DeleteNetworkObject(obj); @@ -1068,6 +1167,7 @@ void NetworkInternal::NetworkReplicatorClear() // Cleanup NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Shutdown"); + NetworkReplicator::SetHierarchy(nullptr); for (auto it = Objects.Begin(); it.IsNotEnd(); ++it) { auto& item = it->Item; @@ -1087,6 +1187,7 @@ void NetworkInternal::NetworkReplicatorClear() IdsRemappingTable.Clear(); SAFE_DELETE(CachedWriteStream); SAFE_DELETE(CachedReadStream); + SAFE_DELETE(CachedReplicationResult); NewClients.Clear(); CachedTargets.Clear(); DespawnedObjects.Clear(); @@ -1182,9 +1283,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; } } } @@ -1213,7 +1316,14 @@ void NetworkInternal::NetworkReplicatorUpdate() if (e.HasOwnership) { - item.Role = e.Role; + if (item.Role != e.Role) + { + if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative) + Hierarchy->RemoveObject(obj); + item.Role = e.Role; + if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative) + Hierarchy->AddObject(obj); + } item.OwnerClientId = e.OwnerClientId; if (e.HierarchicalOwnership) NetworkReplicator::SetObjectOwnership(obj, e.OwnerClientId, e.Role, true); @@ -1247,65 +1357,104 @@ void NetworkInternal::NetworkReplicatorUpdate() } // Apply parts replication - for (int32 i = ReplicationParts.Count() - 1; i >= 0; i--) { - auto& e = ReplicationParts[i]; - if (e.PartsLeft > 0) + PROFILE_CPU_NAMED("ReplicationParts"); + for (int32 i = ReplicationParts.Count() - 1; i >= 0; i--) { - // TODO: remove replication items after some TTL to prevent memory leaks - continue; - } - ScriptingObject* obj = e.Object.Get(); - if (obj) - { - auto it = Objects.Find(obj->GetID()); - if (it != Objects.End()) + auto& e = ReplicationParts[i]; + if (e.PartsLeft > 0) { - auto& item = it->Item; - - // Replicate from all collected parts data - InvokeObjectReplication(item, e.OwnerFrame, e.Data.Get(), e.Data.Count(), e.OwnerClientId); + // TODO: remove replication items after some TTL to prevent memory leaks + continue; } - } + ScriptingObject* obj = e.Object.Get(); + if (obj) + { + auto it = Objects.Find(obj->GetID()); + if (it != Objects.End()) + { + auto& item = it->Item; - ReplicationParts.RemoveAt(i); + // Replicate from all collected parts data + InvokeObjectReplication(item, e.OwnerFrame, e.Data.Get(), e.Data.Count(), e.OwnerClientId); + } + } + + ReplicationParts.RemoveAt(i); + } } - // Brute force synchronize all networked objects with clients - if (CachedWriteStream == nullptr) - CachedWriteStream = New(); - NetworkStream* stream = CachedWriteStream; - stream->SenderId = NetworkManager::LocalClientId; - // TODO: introduce NetworkReplicationHierarchy to optimize objects replication in large worlds (eg. batched culling networked scene objects that are too far from certain client to be relevant) - // TODO: per-object sync interval (in frames) - could be scaled by hierarchy (eg. game could slow down sync rate for objects far from player) - for (auto it = Objects.Begin(); it.IsNotEnd(); ++it) + // Replicate all owned networked objects with other clients or server + if (!CachedReplicationResult) + CachedReplicationResult = New(); + CachedReplicationResult->Init(); + if (!isClient && NetworkManager::Clients.IsEmpty()) { - auto& item = it->Item; - ScriptingObject* obj = item.Object.Get(); - if (!obj) + // No need to update replication when nobody's around + } + else if (Hierarchy) + { + // Tick using hierarchy + PROFILE_CPU_NAMED("ReplicationHierarchyUpdate"); + Hierarchy->Update(CachedReplicationResult); + } + else + { + // Tick all owned objects + PROFILE_CPU_NAMED("ReplicationUpdate"); + for (auto it = Objects.Begin(); it.IsNotEnd(); ++it) { - // Object got deleted - NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Remove object {}, owned by {}", item.ToString(), item.ParentId.ToString()); - Objects.Remove(it); - continue; + auto& item = it->Item; + ScriptingObject* obj = item.Object.Get(); + if (!obj) + { + // Object got deleted + NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Remove object {}, owned by {}", item.ToString(), item.ParentId.ToString()); + Objects.Remove(it); + continue; + } + if (item.Role != NetworkObjectRole::OwnedAuthoritative) + continue; // Send replication messages of only owned objects or from other client objects + CachedReplicationResult->AddObject(obj); } - if (item.Role != NetworkObjectRole::OwnedAuthoritative && (!isClient && item.OwnerClientId != NetworkManager::LocalClientId)) - continue; // Send replication messages of only owned objects or from other client objects - - if (item.AsNetworkObject) - item.AsNetworkObject->OnNetworkSerialize(); - - // Serialize object - stream->Initialize(); - const bool failed = NetworkReplicator::InvokeSerializer(obj->GetTypeHandle(), obj, stream, true); - if (failed) + } + if (CachedReplicationResult->_entries.HasItems()) + { + PROFILE_CPU_NAMED("Replication"); + if (CachedWriteStream == nullptr) + CachedWriteStream = New(); + NetworkStream* stream = CachedWriteStream; + stream->SenderId = NetworkManager::LocalClientId; + // TODO: use Job System when replicated objects count is large + for (auto& e : CachedReplicationResult->_entries) { - //NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Cannot serialize object {} of type {} (missing serialization logic)", item.ToString(), obj->GetType().ToString()); - continue; - } + ScriptingObject* obj = e.Object; + auto it = Objects.Find(obj->GetID()); + if (it.IsEnd()) + continue; + auto& item = it->Item; - // Send object to clients - { + // Skip serialization of objects that none will receive + if (!isClient) + { + BuildCachedTargets(item, e.TargetClients); + if (CachedTargets.Count() == 0) + continue; + } + + if (item.AsNetworkObject) + item.AsNetworkObject->OnNetworkSerialize(); + + // Serialize object + stream->Initialize(); + const bool failed = NetworkReplicator::InvokeSerializer(obj->GetTypeHandle(), obj, stream, true); + if (failed) + { + //NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Cannot serialize object {} of type {} (missing serialization logic)", item.ToString(), obj->GetType().ToString()); + continue; + } + + // Send object to clients const uint32 size = stream->GetPosition(); ASSERT(size <= MAX_uint16) NetworkMessageObjectReplicate msgData; @@ -1344,11 +1493,7 @@ void NetworkInternal::NetworkReplicatorUpdate() if (isClient) peer->EndSendMessage(NetworkChannelType::Unreliable, msg); else - { - // TODO: per-object relevancy for connected clients (eg. skip replicating actor to far players) - BuildCachedTargets(item); peer->EndSendMessage(NetworkChannelType::Unreliable, msg, CachedTargets); - } // Send all other parts for (uint32 partIndex = 1; partIndex < partsCount; partIndex++) @@ -1376,49 +1521,52 @@ void NetworkInternal::NetworkReplicatorUpdate() } // Invoke RPCs - for (auto& e : RpcQueue) { - ScriptingObject* obj = e.Object.Get(); - if (!obj) - continue; - auto it = Objects.Find(obj->GetID()); - if (it == Objects.End()) - continue; - auto& item = it->Item; + PROFILE_CPU_NAMED("Rpc"); + for (auto& e : RpcQueue) + { + ScriptingObject* obj = e.Object.Get(); + if (!obj) + continue; + auto it = Objects.Find(obj->GetID()); + if (it == Objects.End()) + continue; + auto& item = it->Item; - // Send RPC message - //NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Rpc {}::{} object ID={}", e.Name.First.ToString(), String(e.Name.Second), item.ToString()); - NetworkMessageObjectRpc msgData; - msgData.ObjectId = item.ObjectId; - if (isClient) - { - // Remap local client object ids into server ids - IdsRemappingTable.KeyOf(msgData.ObjectId, &msgData.ObjectId); - } - GetNetworkName(msgData.RpcTypeName, e.Name.First.GetType().Fullname); - GetNetworkName(msgData.RpcName, e.Name.Second); - msgData.ArgsSize = (uint16)e.ArgsData.Length(); - NetworkMessage msg = peer->BeginSendMessage(); - msg.WriteStructure(msgData); - msg.WriteBytes(e.ArgsData.Get(), e.ArgsData.Length()); - NetworkChannelType channel = (NetworkChannelType)e.Info.Channel; - if (e.Info.Server && isClient) - { - // Client -> Server + // Send RPC message + //NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Rpc {}::{} object ID={}", e.Name.First.ToString(), String(e.Name.Second), item.ToString()); + NetworkMessageObjectRpc msgData; + msgData.ObjectId = item.ObjectId; + if (isClient) + { + // Remap local client object ids into server ids + IdsRemappingTable.KeyOf(msgData.ObjectId, &msgData.ObjectId); + } + GetNetworkName(msgData.RpcTypeName, e.Name.First.GetType().Fullname); + GetNetworkName(msgData.RpcName, e.Name.Second); + msgData.ArgsSize = (uint16)e.ArgsData.Length(); + NetworkMessage msg = peer->BeginSendMessage(); + msg.WriteStructure(msgData); + msg.WriteBytes(e.ArgsData.Get(), e.ArgsData.Length()); + NetworkChannelType channel = (NetworkChannelType)e.Info.Channel; + if (e.Info.Server && isClient) + { + // Client -> Server #if USE_NETWORK_REPLICATOR_LOG - if (e.Targets.Length() != 0) - NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Server RPC '{}::{}' called with non-empty list of targets is not supported (only server will receive it)", e.Name.First.ToString(), e.Name.Second.ToString()); + if (e.Targets.Length() != 0) + NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Server RPC '{}::{}' called with non-empty list of targets is not supported (only server will receive it)", e.Name.First.ToString(), e.Name.Second.ToString()); #endif - peer->EndSendMessage(channel, msg); - } - else if (e.Info.Client && (isServer || isHost)) - { - // Server -> Client(s) - BuildCachedTargets(NetworkManager::Clients, item.TargetClientIds, e.Targets, NetworkManager::LocalClientId); - peer->EndSendMessage(channel, msg, CachedTargets); + peer->EndSendMessage(channel, msg); + } + else if (e.Info.Client && (isServer || isHost)) + { + // Server -> Client(s) + BuildCachedTargets(NetworkManager::Clients, item.TargetClientIds, e.Targets, NetworkManager::LocalClientId); + peer->EndSendMessage(channel, msg, CachedTargets); + } } + RpcQueue.Clear(); } - RpcQueue.Clear(); // Clear networked objects mapping table Scripting::ObjectsLookupIdMapping.Set(nullptr); @@ -1426,6 +1574,7 @@ void NetworkInternal::NetworkReplicatorUpdate() void NetworkInternal::OnNetworkMessageObjectReplicate(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer) { + PROFILE_CPU(); NetworkMessageObjectReplicate msgData; event.Message.ReadStructure(msgData); ScopeLock lock(ObjectsLock); @@ -1457,6 +1606,7 @@ void NetworkInternal::OnNetworkMessageObjectReplicate(NetworkEvent& event, Netwo void NetworkInternal::OnNetworkMessageObjectReplicatePart(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer) { + PROFILE_CPU(); NetworkMessageObjectReplicatePart msgData; event.Message.ReadStructure(msgData); ScopeLock lock(ObjectsLock); @@ -1469,6 +1619,7 @@ void NetworkInternal::OnNetworkMessageObjectReplicatePart(NetworkEvent& event, N void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer) { + PROFILE_CPU(); NetworkMessageObjectSpawn msgData; event.Message.ReadStructure(msgData); auto* msgDataItems = (NetworkMessageObjectSpawnItem*)event.Message.SkipBytes(msgData.ItemsCount * sizeof(NetworkMessageObjectSpawnItem)); @@ -1493,7 +1644,11 @@ void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkCl // Server always knows the best so update ownership of the existing object item.OwnerClientId = msgData.OwnerClientId; if (item.Role == NetworkObjectRole::OwnedAuthoritative) + { + if (Hierarchy) + Hierarchy->AddObject(item.Object); item.Role = NetworkObjectRole::Replicated; + } } else if (item.OwnerClientId != msgData.OwnerClientId) { @@ -1623,7 +1778,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]; @@ -1648,10 +1803,22 @@ void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkCl item.Spawned = true; NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Add new object {}:{}, parent {}:{}", item.ToString(), obj->GetType().ToString(), item.ParentId.ToString(), parent ? parent->Object->GetType().ToString() : String::Empty); Objects.Add(MoveTemp(item)); + if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative) + Hierarchy->AddObject(obj); // 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); @@ -1666,7 +1833,7 @@ void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkCl #if USE_NETWORK_REPLICATOR_LOG // Ignore case when parent object in a message was a scene (eg. that is already unloaded on a client) AssetInfo assetInfo; - if (!Content::GetAssetInfo(msgDataItem.ParentId, assetInfo) || assetInfo.TypeName == TEXT("FlaxEngine.SceneAsset")) + if (!Content::GetAssetInfo(msgDataItem.ParentId, assetInfo) || assetInfo.TypeName != TEXT("FlaxEngine.SceneAsset")) { NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find object {} as parent to spawned object", msgDataItem.ParentId.ToString()); } @@ -1687,6 +1854,7 @@ void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkCl void NetworkInternal::OnNetworkMessageObjectDespawn(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer) { + PROFILE_CPU(); NetworkMessageObjectDespawn msgData; event.Message.ReadStructure(msgData); ScopeLock lock(ObjectsLock); @@ -1704,6 +1872,8 @@ void NetworkInternal::OnNetworkMessageObjectDespawn(NetworkEvent& event, Network // Remove object NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Despawn object {}", msgData.ObjectId); + if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative) + Hierarchy->RemoveObject(obj); DespawnedObjects.Add(msgData.ObjectId); if (item.AsNetworkObject) item.AsNetworkObject->OnNetworkDespawn(); @@ -1718,6 +1888,7 @@ void NetworkInternal::OnNetworkMessageObjectDespawn(NetworkEvent& event, Network void NetworkInternal::OnNetworkMessageObjectRole(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer) { + PROFILE_CPU(); NetworkMessageObjectRole msgData; event.Message.ReadStructure(msgData); ScopeLock lock(ObjectsLock); @@ -1739,12 +1910,16 @@ void NetworkInternal::OnNetworkMessageObjectRole(NetworkEvent& event, NetworkCli if (item.OwnerClientId == NetworkManager::LocalClientId) { // Upgrade ownership automatically + if (Hierarchy && item.Role != NetworkObjectRole::OwnedAuthoritative) + Hierarchy->AddObject(obj); item.Role = NetworkObjectRole::OwnedAuthoritative; item.LastOwnerFrame = 0; } else if (item.Role == NetworkObjectRole::OwnedAuthoritative) { // Downgrade ownership automatically + if (Hierarchy) + Hierarchy->RemoveObject(obj); item.Role = NetworkObjectRole::Replicated; } if (!NetworkManager::IsClient()) @@ -1761,6 +1936,7 @@ void NetworkInternal::OnNetworkMessageObjectRole(NetworkEvent& event, NetworkCli void NetworkInternal::OnNetworkMessageObjectRpc(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer) { + PROFILE_CPU(); NetworkMessageObjectRpc msgData; event.Message.ReadStructure(msgData); ScopeLock lock(ObjectsLock); diff --git a/Source/Engine/Networking/NetworkReplicator.h b/Source/Engine/Networking/NetworkReplicator.h index 2ab97a51b..156e7eeea 100644 --- a/Source/Engine/Networking/NetworkReplicator.h +++ b/Source/Engine/Networking/NetworkReplicator.h @@ -42,6 +42,17 @@ public: API_FIELD() static bool EnableLog; #endif + /// + /// Gets the network replication hierarchy. + /// + API_PROPERTY() static NetworkReplicationHierarchy* GetHierarchy(); + + /// + /// Sets the network replication hierarchy. + /// + API_PROPERTY() static void SetHierarchy(NetworkReplicationHierarchy* value); + +public: /// /// Adds the network replication serializer for a given type. /// @@ -68,7 +79,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 +91,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 +110,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/Networking/Types.h b/Source/Engine/Networking/Types.h index 11675023a..0d754e727 100644 --- a/Source/Engine/Networking/Types.h +++ b/Source/Engine/Networking/Types.h @@ -11,6 +11,7 @@ class INetworkSerializable; class NetworkPeer; class NetworkClient; class NetworkStream; +class NetworkReplicationHierarchy; struct NetworkEvent; struct NetworkConnection; 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/Scripting/SoftTypeReference.cs b/Source/Engine/Scripting/SoftTypeReference.cs new file mode 100644 index 000000000..f12f2bdb7 --- /dev/null +++ b/Source/Engine/Scripting/SoftTypeReference.cs @@ -0,0 +1,87 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +using System; + +namespace FlaxEngine +{ + /// + /// The soft reference to the scripting type contained in the scripting assembly. + /// + public struct SoftTypeReference : IComparable, IComparable + { + private string _typeName; + + /// + /// Gets or sets the type full name (eg. FlaxEngine.Actor). + /// + public string TypeName + { + get => _typeName; + set => _typeName = value; + } + + /// + /// Gets or sets the type (resolves soft reference). + /// + public Type Type + { + get => _typeName != null ? Type.GetType(_typeName) : null; + set => _typeName = value?.FullName; + } + + /// + /// Initializes a new instance of the . + /// + /// The type name. + public SoftTypeReference(string typeName) + { + _typeName = typeName; + } + + /// + /// Gets the soft type reference from full name. + /// + /// The type name. + /// The soft type reference. + public static implicit operator SoftTypeReference(string s) + { + return new SoftTypeReference { _typeName = s }; + } + + /// + /// Gets the soft type reference from runtime type. + /// + /// The type. + /// The soft type reference. + public static implicit operator SoftTypeReference(Type s) + { + return new SoftTypeReference { _typeName = s?.FullName }; + } + + /// + public override string ToString() + { + return _typeName; + } + + /// + public override int GetHashCode() + { + return _typeName?.GetHashCode() ?? 0; + } + + /// + public int CompareTo(object obj) + { + if (obj is SoftTypeReference other) + return CompareTo(other); + return 0; + } + + /// + public int CompareTo(SoftTypeReference other) + { + return string.Compare(_typeName, other._typeName, StringComparison.Ordinal); + } + } +} diff --git a/Source/Engine/Scripting/SoftTypeReference.h b/Source/Engine/Scripting/SoftTypeReference.h new file mode 100644 index 000000000..30508f639 --- /dev/null +++ b/Source/Engine/Scripting/SoftTypeReference.h @@ -0,0 +1,151 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +#pragma once + +#include "Scripting.h" +#include "ScriptingObject.h" +#include "Engine/Core/Log.h" +#include "Engine/Core/Types/String.h" +#include "Engine/Serialization/SerializationFwd.h" + +/// +/// The soft reference to the scripting type contained in the scripting assembly. +/// +template +API_STRUCT(InBuild) struct SoftTypeReference +{ +protected: + StringAnsi _typeName; + +public: + SoftTypeReference() = default; + + SoftTypeReference(const SoftTypeReference& s) + : _typeName(s._typeName) + { + } + + SoftTypeReference(SoftTypeReference&& s) noexcept + : _typeName(MoveTemp(s._typeName)) + { + } + + SoftTypeReference(const StringView& s) + : _typeName(s) + { + } + + SoftTypeReference(const StringAnsiView& s) + : _typeName(s) + { + } + + SoftTypeReference(const char* s) + : _typeName(s) + { + } + +public: + FORCE_INLINE SoftTypeReference& operator=(SoftTypeReference&& s) noexcept + { + _typeName = MoveTemp(s._typeName); + return *this; + } + + FORCE_INLINE SoftTypeReference& operator=(StringAnsi&& s) noexcept + { + _typeName = MoveTemp(s); + return *this; + } + + FORCE_INLINE SoftTypeReference& operator=(const SoftTypeReference& s) + { + _typeName = s._typeName; + return *this; + } + + FORCE_INLINE SoftTypeReference& operator=(const StringAnsiView& s) + { + _typeName = s; + return *this; + } + + FORCE_INLINE bool operator==(const SoftTypeReference& other) const + { + return _typeName == other._typeName; + } + + FORCE_INLINE bool operator!=(const SoftTypeReference& other) const + { + return _typeName != other._typeName; + } + + FORCE_INLINE bool operator==(const StringAnsiView& other) const + { + return _typeName == other; + } + + FORCE_INLINE bool operator!=(const StringAnsiView& other) const + { + return _typeName != other; + } + + FORCE_INLINE operator bool() const + { + return _typeName.HasChars(); + } + +public: + // Gets the type full name (eg. FlaxEngine.Actor). + StringAnsiView GetTypeName() const + { + return StringAnsiView(_typeName); + } + + // Gets the type (resolves soft reference). + ScriptingTypeHandle GetType() const + { + return Scripting::FindScriptingType(_typeName); + } + + // Creates a new objects of that type (or of type T if failed to solve typename). + T* NewObject() const + { + const ScriptingTypeHandle type = Scripting::FindScriptingType(_typeName); + auto obj = ScriptingObject::NewObject(type); + if (!obj) + { + if (_typeName.HasChars()) + LOG(Error, "Unknown or invalid type {0}", String(_typeName)); + obj = ScriptingObject::NewObject(); + } + return obj; + } +}; + +template +uint32 GetHash(const SoftTypeReference& key) +{ + return GetHash(key.GetTypeName()); +} + +// @formatter:off +namespace Serialization +{ + template + bool ShouldSerialize(const SoftTypeReference& v, const void* otherObj) + { + return !otherObj || v != *(SoftTypeReference*)otherObj; + } + template + void Serialize(ISerializable::SerializeStream& stream, const SoftTypeReference& v, const void* otherObj) + { + stream.String(v.GetTypeName()); + } + template + void Deserialize(ISerializable::DeserializeStream& stream, SoftTypeReference& v, ISerializeModifier* modifier) + { + v = stream.GetTextAnsi(); + } +} +// @formatter:on diff --git a/Source/Engine/Serialization/JsonConverters.cs b/Source/Engine/Serialization/JsonConverters.cs index 07388a09b..40f9cb5a2 100644 --- a/Source/Engine/Serialization/JsonConverters.cs +++ b/Source/Engine/Serialization/JsonConverters.cs @@ -7,7 +7,7 @@ using Newtonsoft.Json; namespace FlaxEngine.Json { /// - /// Serialize references to the FlaxEngine.Object as Guid. + /// Serialize references to the as Guid. /// /// internal class FlaxObjectConverter : JsonConverter @@ -46,7 +46,7 @@ namespace FlaxEngine.Json } /// - /// Serialize SceneReference as Guid in internal format. + /// Serialize as Guid in internal format. /// /// internal class SceneReferenceConverter : JsonConverter @@ -79,7 +79,7 @@ namespace FlaxEngine.Json } /// - /// Serialize SoftObjectReference as Guid in internal format. + /// Serialize as Guid in internal format. /// /// internal class SoftObjectReferenceConverter : JsonConverter @@ -111,7 +111,36 @@ namespace FlaxEngine.Json } /// - /// Serialize SoftObjectReference as Guid in internal format. + /// Serialize as typename string in internal format. + /// + /// + internal class SoftTypeReferenceConverter : JsonConverter + { + /// + public override void WriteJson(JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer) + { + writer.WriteValue(((SoftTypeReference)value).TypeName); + } + + /// + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer) + { + var result = new SoftTypeReference(); + if (reader.TokenType == JsonToken.String) + result.TypeName = (string)reader.Value; + + return result; + } + + /// + public override bool CanConvert(Type objectType) + { + return objectType == typeof(SoftTypeReference); + } + } + + /// + /// Serialize as Guid in internal format. /// /// internal class MarginConverter : JsonConverter @@ -237,7 +266,7 @@ namespace FlaxEngine.Json } /// - /// Serialize LocalizedString as inlined text is not using localization (Id member is empty). + /// Serialize as inlined text is not using localization (Id member is empty). /// /// internal class LocalizedStringConverter : JsonConverter diff --git a/Source/Engine/Serialization/JsonSerializer.cs b/Source/Engine/Serialization/JsonSerializer.cs index b9706b724..619862b1f 100644 --- a/Source/Engine/Serialization/JsonSerializer.cs +++ b/Source/Engine/Serialization/JsonSerializer.cs @@ -123,6 +123,7 @@ namespace FlaxEngine.Json settings.Converters.Add(ObjectConverter); settings.Converters.Add(new SceneReferenceConverter()); settings.Converters.Add(new SoftObjectReferenceConverter()); + settings.Converters.Add(new SoftTypeReferenceConverter()); settings.Converters.Add(new MarginConverter()); settings.Converters.Add(new VersionConverter()); settings.Converters.Add(new LocalizedStringConverter()); diff --git a/Source/Engine/UI/GUI/Common/Button.cs b/Source/Engine/UI/GUI/Common/Button.cs index e3bee0017..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; @@ -195,7 +195,7 @@ namespace FlaxEngine.GUI /// Sets the button colors palette based on a given main color. /// /// The main color. - public void SetColors(Color color) + public virtual void SetColors(Color color) { BackgroundColor = color; BorderColor = color.RGBMultiplied(0.5f); diff --git a/Source/ThirdParty/tracy/tracy.Build.cs b/Source/ThirdParty/tracy/tracy.Build.cs index d6119a5ca..ca5f485b2 100644 --- a/Source/ThirdParty/tracy/tracy.Build.cs +++ b/Source/ThirdParty/tracy/tracy.Build.cs @@ -57,6 +57,7 @@ public class tracy : ThirdPartyModule files.Add(Path.Combine(FolderPath, "tracy", "Tracy.hpp")); files.Add(Path.Combine(FolderPath, "common", "TracySystem.hpp")); + files.Add(Path.Combine(FolderPath, "common", "TracyQueue.hpp")); files.Add(Path.Combine(FolderPath, "client", "TracyCallstack.h")); } } diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs index 8ccb6105e..25d30a37a 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs @@ -541,7 +541,7 @@ namespace Flax.Build.Bindings var separator = false; if (!functionInfo.IsStatic) { - contents.Append("IntPtr obj"); + contents.Append("IntPtr __obj"); separator = true; } @@ -1511,7 +1511,8 @@ namespace Flax.Build.Bindings if (fieldInfo.Type.IsObjectRef) { - toManagedContent.Append($"managed.{fieldInfo.Name} != IntPtr.Zero ? Unsafe.As<{fieldInfo.Type.GenericArgs[0].Type}>(ManagedHandle.FromIntPtr(managed.{fieldInfo.Name}).Target) : null"); + var managedType = GenerateCSharpNativeToManaged(buildData, fieldInfo.Type.GenericArgs[0], structureInfo); + toManagedContent.Append($"managed.{fieldInfo.Name} != IntPtr.Zero ? Unsafe.As<{managedType}>(ManagedHandle.FromIntPtr(managed.{fieldInfo.Name}).Target) : null"); toNativeContent.Append($"managed.{fieldInfo.Name} != null ? ManagedHandle.ToIntPtr(managed.{fieldInfo.Name}, GCHandleType.Weak) : IntPtr.Zero"); freeContents.AppendLine($"if (unmanaged.{fieldInfo.Name} != IntPtr.Zero) {{ ManagedHandle.FromIntPtr(unmanaged.{fieldInfo.Name}).Free(); }}"); diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs index 665063613..2c2c9b341 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs @@ -1017,7 +1017,7 @@ namespace Flax.Build.Bindings var signatureStart = contents.Length; if (!functionInfo.IsStatic) { - contents.Append(caller.Name).Append("* obj"); + contents.Append(caller.Name).Append("* __obj"); separator = true; } @@ -1127,7 +1127,7 @@ namespace Flax.Build.Bindings contents.Append(indent).AppendLine($"MSVC_FUNC_EXPORT(\"{libraryEntryPoint}\")"); // Export generated function binding under the C# name #endif if (!functionInfo.IsStatic) - contents.Append(indent).AppendLine("if (obj == nullptr) DebugLog::ThrowNullReference();"); + contents.Append(indent).AppendLine("if (__obj == nullptr) DebugLog::ThrowNullReference();"); string callBegin = indent; if (functionInfo.Glue.UseReferenceForResult) @@ -1165,7 +1165,7 @@ namespace Flax.Build.Bindings else { // Call native member method - call = $"obj->{functionInfo.Name}"; + call = $"__obj->{functionInfo.Name}"; } string callParams = string.Empty; separator = false; @@ -1925,7 +1925,7 @@ namespace Flax.Build.Bindings continue; var paramsCount = eventInfo.Type.GenericArgs?.Count ?? 0; CppIncludeFiles.Add("Engine/Profiler/ProfilerCPU.h"); - var bindPrefix = eventInfo.IsStatic ? classTypeNameNative + "::" : "obj->"; + var bindPrefix = eventInfo.IsStatic ? classTypeNameNative + "::" : "__obj->"; if (useCSharp) { @@ -2006,7 +2006,7 @@ namespace Flax.Build.Bindings if (buildData.Toolchain?.Compiler == TargetCompiler.Clang) useSeparateImpl = true; // DLLEXPORT doesn't properly export function thus separate implementation from declaration if (!eventInfo.IsStatic) - contents.AppendFormat("{0}* obj, ", classTypeNameNative); + contents.AppendFormat("{0}* __obj, ", classTypeNameNative); contents.Append("bool bind)"); var contentsPrev = contents; var indent = " "; @@ -2034,7 +2034,7 @@ namespace Flax.Build.Bindings if (eventInfo.IsStatic) contents.Append(indent).AppendFormat(" f.Bind<{0}_ManagedWrapper>();", eventInfo.Name).AppendLine(); else - contents.Append(indent).AppendFormat(" f.Bind<{1}, &{1}::{0}_ManagedWrapper>(({1}*)obj);", eventInfo.Name, internalTypeName).AppendLine(); + contents.Append(indent).AppendFormat(" f.Bind<{1}, &{1}::{0}_ManagedWrapper>(({1}*)__obj);", eventInfo.Name, internalTypeName).AppendLine(); contents.Append(indent).Append(" if (bind)").AppendLine(); contents.Append(indent).AppendFormat(" {0}{1}.Bind(f);", bindPrefix, eventInfo.Name).AppendLine(); contents.Append(indent).Append(" else").AppendLine(); @@ -2074,7 +2074,7 @@ namespace Flax.Build.Bindings // Scripting event wrapper binding method (binds/unbinds generic wrapper to C++ delegate) contents.AppendFormat(" static void {0}_Bind(", eventInfo.Name); - contents.AppendFormat("{0}* obj, void* instance, bool bind)", classTypeNameNative).AppendLine(); + contents.AppendFormat("{0}* __obj, void* instance, bool bind)", classTypeNameNative).AppendLine(); contents.Append(" {").AppendLine(); contents.Append(" FunctionGetTypeHandle();"); contents.AppendLine(" while (typeHandle)"); contents.AppendLine(" {"); - contents.AppendLine($" auto method = typeHandle.Module->FindMethod(typeHandle, \"{functionInfo.Name}\", {functionInfo.Parameters.Count});"); + contents.AppendLine($" auto method = typeHandle.Module->FindMethod(typeHandle, StringAnsiView(\"{functionInfo.Name}\", {functionInfo.Name.Length}), {functionInfo.Parameters.Count});"); contents.AppendLine(" if (method)"); contents.AppendLine(" {"); contents.AppendLine(" Variant __result;"); @@ -2565,10 +2565,10 @@ namespace Flax.Build.Bindings contents.AppendLine(" }").AppendLine(); // Interface implementation wrapper accessor for scripting types - contents.AppendLine(" static void* GetInterfaceWrapper(ScriptingObject* obj)"); + contents.AppendLine(" static void* GetInterfaceWrapper(ScriptingObject* __obj)"); contents.AppendLine(" {"); contents.AppendLine($" auto wrapper = New<{interfaceTypeNameInternal}Wrapper>();"); - contents.AppendLine(" wrapper->Object = obj;"); + contents.AppendLine(" wrapper->Object = __obj;"); contents.AppendLine(" return wrapper;"); contents.AppendLine(" }");