From 141555377b8198476880c6ec4f18e07e27c2941e Mon Sep 17 00:00:00 2001 From: Menotdan Date: Sun, 7 May 2023 22:58:57 -0400 Subject: [PATCH 01/32] Begin working on an IconButton class, and demoing the IconButton idea using the scale link icon. --- .../Editors/ActorTransformEditor.cs | 32 +++- Source/Engine/UI/GUI/Common/IconButton.cs | 167 ++++++++++++++++++ 2 files changed, 192 insertions(+), 7 deletions(-) create mode 100644 Source/Engine/UI/GUI/Common/IconButton.cs diff --git a/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs b/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs index 2612440a4..a824076f7 100644 --- a/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs +++ b/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs @@ -78,7 +78,8 @@ namespace FlaxEditor.CustomEditors.Editors /// public class ScaleEditor : Float3Editor { - private Image _linkImage; + private Button _linkButton; + private SpriteBrush _linkBrush; /// public override void Initialize(LayoutElementsContainer layout) @@ -87,18 +88,26 @@ namespace FlaxEditor.CustomEditors.Editors LinkValues = Editor.Instance.Windows.PropertiesWin.ScaleLinked; - _linkImage = new Image + // Add button with the link icon. + _linkBrush = new SpriteBrush(Editor.Instance.Icons.Link32); + _linkButton = new Button { Parent = LinkedLabel, Width = 18, Height = 18, - Brush = LinkValues ? new SpriteBrush(Editor.Instance.Icons.Link32) : new SpriteBrush(), + BackgroundBrush = _linkBrush, AnchorPreset = AnchorPresets.TopLeft, - TooltipText = "Scale values are linked together.", }; + + _linkButton.Clicked += ToggleLink; + _linkButton.BorderColor = Color.Transparent; + _linkButton.BorderColorSelected = Color.Transparent; + _linkButton.BorderColorHighlighted = Color.Transparent; + 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) => { @@ -127,7 +136,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() + { + Color backgroundColor = LinkValues ? FlaxEngine.GUI.Style.Current.BackgroundSelected : FlaxEngine.GUI.Style.Current.ForegroundDisabled; + _linkButton.BackgroundColor = backgroundColor; + _linkButton.BackgroundColorHighlighted = backgroundColor.RGBMultiplied(0.9f); + _linkButton.BackgroundColorSelected = backgroundColor.RGBMultiplied(0.8f); + _linkButton.TooltipText = (LinkValues ? "Unlink" : "Link") + " values for uniform scaling."; } } } diff --git a/Source/Engine/UI/GUI/Common/IconButton.cs b/Source/Engine/UI/GUI/Common/IconButton.cs new file mode 100644 index 000000000..196b4b9c4 --- /dev/null +++ b/Source/Engine/UI/GUI/Common/IconButton.cs @@ -0,0 +1,167 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +using System; + +namespace FlaxEngine.GUI +{ + /// + /// Button with an icon. + /// + public class IconButton : Button + { + /// + public override void ClearState() + { + base.ClearState(); + + if (_isPressed) + OnPressEnd(); + } + + /// + public override void DrawSelf() + { + // Cache data + Rectangle clientRect = new Rectangle(Float2.Zero, Size); + bool enabled = EnabledInHierarchy; + Color backgroundColor = BackgroundColor; + Color borderColor = BorderColor; + Color textColor = TextColor; + if (!enabled) + { + backgroundColor *= 0.5f; + borderColor *= 0.5f; + textColor *= 0.6f; + } + else if (_isPressed) + { + backgroundColor = BackgroundColorSelected; + borderColor = BorderColorSelected; + } + else if (IsMouseOver || IsNavFocused) + { + backgroundColor = BackgroundColorHighlighted; + borderColor = BorderColorHighlighted; + } + + // Draw background + if (BackgroundBrush != null) + BackgroundBrush.Draw(clientRect, backgroundColor); + else + Render2D.FillRectangle(clientRect, backgroundColor); + Render2D.DrawRectangle(clientRect, borderColor); + + // Draw text + Render2D.DrawText(_font?.GetFont(), TextMaterial, _text, clientRect, textColor, TextAlignment.Center, TextAlignment.Center); + } + + /// + public override void OnMouseEnter(Float2 location) + { + base.OnMouseEnter(location); + + HoverBegin?.Invoke(); + } + + /// + public override void OnMouseLeave() + { + if (_isPressed) + { + OnPressEnd(); + } + + HoverEnd?.Invoke(); + + base.OnMouseLeave(); + } + + /// + public override bool OnMouseDown(Float2 location, MouseButton button) + { + if (base.OnMouseDown(location, button)) + return true; + + if (button == MouseButton.Left && !_isPressed) + { + OnPressBegin(); + return true; + } + return false; + } + + /// + public override bool OnMouseUp(Float2 location, MouseButton button) + { + if (base.OnMouseUp(location, button)) + return true; + + if (button == MouseButton.Left && _isPressed) + { + OnPressEnd(); + OnClick(); + return true; + } + return false; + } + + /// + public override bool OnTouchDown(Float2 location, int pointerId) + { + if (base.OnTouchDown(location, pointerId)) + return true; + + if (!_isPressed) + { + OnPressBegin(); + return true; + } + return false; + } + + /// + public override bool OnTouchUp(Float2 location, int pointerId) + { + if (base.OnTouchUp(location, pointerId)) + return true; + + if (_isPressed) + { + OnPressEnd(); + OnClick(); + return true; + } + return false; + } + + /// + public override void OnTouchLeave() + { + if (_isPressed) + { + OnPressEnd(); + } + + base.OnTouchLeave(); + } + + /// + public override void OnLostFocus() + { + if (_isPressed) + { + OnPressEnd(); + } + + base.OnLostFocus(); + } + + /// + public override void OnSubmit() + { + OnClick(); + + base.OnSubmit(); + } + } +} From 296ac0b9404dbff349007aca8d6b348abe7930f1 Mon Sep 17 00:00:00 2001 From: Menotdan Date: Sun, 7 May 2023 23:04:11 -0400 Subject: [PATCH 02/32] Revert "Use shorter, relative path for displaying Asset Tooltips." This reverts commit 4c906f4040880b3a77686f27289800fb6c0cb55a. undo changes from master branch which I don't want for this seperate change. --- Source/Editor/Content/Items/AssetItem.cs | 4 +- Source/Editor/Content/Items/ContentFolder.cs | 9 +-- Source/Editor/Content/Items/ContentItem.cs | 14 +--- Source/Editor/Options/InterfaceOptions.cs | 7 -- Source/Editor/Utilities/Utils.cs | 76 +------------------- 5 files changed, 8 insertions(+), 102 deletions(-) diff --git a/Source/Editor/Content/Items/AssetItem.cs b/Source/Editor/Content/Items/AssetItem.cs index 120d0e416..6697ed27c 100644 --- a/Source/Editor/Content/Items/AssetItem.cs +++ b/Source/Editor/Content/Items/AssetItem.cs @@ -80,9 +80,9 @@ namespace FlaxEditor.Content /// The String Builder. protected virtual void OnBuildTooltipText(StringBuilder sb) { - sb.Append("Type: ").Append(Utilities.Utils.TranslateTypeName(TypeName)).AppendLine(); + sb.Append("Type: ").Append(TypeName).AppendLine(); sb.Append("Size: ").Append(Utilities.Utils.FormatBytesCount((int)new FileInfo(Path).Length)).AppendLine(); - sb.Append("Path: ").Append(Utilities.Utils.GetAssetNamePathWithExt(Path)).AppendLine(); + sb.Append("Path: ").Append(Path).AppendLine(); } /// diff --git a/Source/Editor/Content/Items/ContentFolder.cs b/Source/Editor/Content/Items/ContentFolder.cs index 974b813ee..cbbc15f24 100644 --- a/Source/Editor/Content/Items/ContentFolder.cs +++ b/Source/Editor/Content/Items/ContentFolder.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Text; using FlaxEditor.GUI.Drag; using FlaxEngine; using FlaxEngine.GUI; @@ -138,13 +137,7 @@ namespace FlaxEditor.Content /// public override void UpdateTooltipText() { - string fileDescription = "Folder"; - StringBuilder sb = new StringBuilder(); - - sb.Append("Type: ").Append(fileDescription).AppendLine(); - sb.Append("Path: ").Append(Utilities.Utils.GetAssetNamePathWithExt(Path)).AppendLine(); - - TooltipText = sb.ToString(); + TooltipText = Path; } /// diff --git a/Source/Editor/Content/Items/ContentItem.cs b/Source/Editor/Content/Items/ContentItem.cs index b4a4caa9a..284f3f1b3 100644 --- a/Source/Editor/Content/Items/ContentItem.cs +++ b/Source/Editor/Content/Items/ContentItem.cs @@ -2,8 +2,6 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Text; using FlaxEditor.Content.GUI; using FlaxEditor.GUI.Drag; using FlaxEngine; @@ -355,19 +353,11 @@ namespace FlaxEditor.Content } /// - /// Updates the tooltip text. + /// Updates the tooltip text text. /// public virtual void UpdateTooltipText() { - string fileExtension = System.IO.Path.GetExtension(Path); - string fileDescription = Utilities.Utils.TranslateFileExtension(fileExtension); - StringBuilder sb = new StringBuilder(); - - sb.Append("Type: ").Append(fileDescription).AppendLine(); - sb.Append("Size: ").Append(Utilities.Utils.FormatBytesCount((int)new FileInfo(Path).Length)).AppendLine(); - sb.Append("Path: ").Append(Utilities.Utils.GetAssetNamePathWithExt(Path)).AppendLine(); - - TooltipText = sb.ToString(); + TooltipText = "Path: " + Path; } /// diff --git a/Source/Editor/Options/InterfaceOptions.cs b/Source/Editor/Options/InterfaceOptions.cs index d8e30d62b..acfb7f5e8 100644 --- a/Source/Editor/Options/InterfaceOptions.cs +++ b/Source/Editor/Options/InterfaceOptions.cs @@ -134,13 +134,6 @@ namespace FlaxEditor.Options [EditorDisplay("Interface"), EditorOrder(280), Tooltip("Editor content window orientation.")] public FlaxEngine.GUI.Orientation ContentWindowOrientation { get; set; } = FlaxEngine.GUI.Orientation.Horizontal; - /// - /// Gets or sets the option to use type name translations. - /// - [DefaultValue(true)] - [EditorDisplay("Interface"), EditorOrder(290), Tooltip("Attempt to translate asset type names.")] - public bool TranslateTypeNames { get; set; } = true; - /// /// Gets or sets the timestamps prefix mode for output log messages. /// diff --git a/Source/Editor/Utilities/Utils.cs b/Source/Editor/Utilities/Utils.cs index 6ef5a8ea0..ade084431 100644 --- a/Source/Editor/Utilities/Utils.cs +++ b/Source/Editor/Utilities/Utils.cs @@ -20,8 +20,6 @@ using FlaxEditor.SceneGraph; using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.GUI; -using FlaxEditor.Options; -using System.Linq; namespace FlaxEngine { @@ -1019,76 +1017,6 @@ namespace FlaxEditor.Utilities node.Visible = isThisVisible | isAnyChildVisible; } - /// - /// Gets the asset type, translating if possible, and if enabled in InterfaceOptions.TranslateTypes. - /// - /// The type name. - /// The translated type name. - public static string TranslateTypeName(string typeName) - { - // TODO: Surely there is a better way to get this value. - if (!Editor.Instance.Options.Options.Interface.TranslateTypeNames) - { - return typeName; - } - - string[] typeNamespaces = typeName.Split('.'); - string lastNamespace = typeNamespaces.Last(); - - // TODO: Add better handling for unconventional type names. - try - { - // Adds spaces between capital letters. - return string.Concat(lastNamespace.Select(x => Char.IsUpper(x) ? " " + x : x.ToString())).TrimStart(' '); - } catch { - return typeName; - } - } - - /// - /// Gets a description of a file from it's extension. - /// - /// The file's extension - /// The processed description. - public static string TranslateFileExtension(string fileExtension) - { - string fileDescription = ""; - switch (fileExtension) - { - case ".cs": - fileDescription = "C# Source Code"; - break; - case ".cpp": - fileDescription = "C++ Source Code"; - break; - case ".h": - fileDescription = "C++ Header File"; - break; - case ".json": - fileDescription = "JSON File"; - break; - default: - fileDescription = fileExtension; - break; - } - - return fileDescription; - } - - - /// - /// Gets the asset name relative to the project root folder (with asset file extension) - /// - /// The asset path. - /// The processed name path. - public static string GetAssetNamePathWithExt(string path) - { - var projectFolder = Globals.ProjectFolder; - if (path.StartsWith(projectFolder)) - path = path.Substring(projectFolder.Length + 1); - return path; - } - /// /// Gets the asset name relative to the project root folder (without asset file extension) /// @@ -1096,7 +1024,9 @@ namespace FlaxEditor.Utilities /// The processed name path. public static string GetAssetNamePath(string path) { - path = GetAssetNamePathWithExt(path); + var projectFolder = Globals.ProjectFolder; + if (path.StartsWith(projectFolder)) + path = path.Substring(projectFolder.Length + 1); return StringUtils.GetPathWithoutExtension(path); } From 681564189fe720005470cc54d06af60ae149a499 Mon Sep 17 00:00:00 2001 From: Menotdan Date: Mon, 8 May 2023 00:42:38 -0400 Subject: [PATCH 03/32] Finish implementing IconButton and use that to create a more intuitive scale linking interface. --- .../Editors/ActorTransformEditor.cs | 15 +- Source/Engine/UI/GUI/Common/Button.cs | 4 +- Source/Engine/UI/GUI/Common/IconButton.cs | 191 +++++------------- 3 files changed, 55 insertions(+), 155 deletions(-) diff --git a/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs b/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs index a824076f7..fb7a0b29d 100644 --- a/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs +++ b/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs @@ -79,7 +79,6 @@ namespace FlaxEditor.CustomEditors.Editors public class ScaleEditor : Float3Editor { private Button _linkButton; - private SpriteBrush _linkBrush; /// public override void Initialize(LayoutElementsContainer layout) @@ -89,20 +88,16 @@ namespace FlaxEditor.CustomEditors.Editors LinkValues = Editor.Instance.Windows.PropertiesWin.ScaleLinked; // Add button with the link icon. - _linkBrush = new SpriteBrush(Editor.Instance.Icons.Link32); - _linkButton = new Button + //Editor.Instance.Icons.Link32 + _linkButton = new IconButton(Editor.Instance.Icons.Link32) { Parent = LinkedLabel, Width = 18, Height = 18, - BackgroundBrush = _linkBrush, - AnchorPreset = AnchorPresets.TopLeft, + AnchorPreset = AnchorPresets.TopLeft }; _linkButton.Clicked += ToggleLink; - _linkButton.BorderColor = Color.Transparent; - _linkButton.BorderColorSelected = Color.Transparent; - _linkButton.BorderColorHighlighted = Color.Transparent; SetLinkStyle(); var x = LinkedLabel.Text.Value.Length * 7 + 5; @@ -142,9 +137,7 @@ namespace FlaxEditor.CustomEditors.Editors private void SetLinkStyle() { Color backgroundColor = LinkValues ? FlaxEngine.GUI.Style.Current.BackgroundSelected : FlaxEngine.GUI.Style.Current.ForegroundDisabled; - _linkButton.BackgroundColor = backgroundColor; - _linkButton.BackgroundColorHighlighted = backgroundColor.RGBMultiplied(0.9f); - _linkButton.BackgroundColorSelected = backgroundColor.RGBMultiplied(0.8f); + _linkButton.SetColors(backgroundColor); _linkButton.TooltipText = (LinkValues ? "Unlink" : "Link") + " values for uniform scaling."; } } diff --git a/Source/Engine/UI/GUI/Common/Button.cs b/Source/Engine/UI/GUI/Common/Button.cs index 0e0d29615..b6ee13e41 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/Engine/UI/GUI/Common/IconButton.cs b/Source/Engine/UI/GUI/Common/IconButton.cs index 196b4b9c4..e2652cd9f 100644 --- a/Source/Engine/UI/GUI/Common/IconButton.cs +++ b/Source/Engine/UI/GUI/Common/IconButton.cs @@ -9,159 +9,66 @@ namespace FlaxEngine.GUI /// public class IconButton : Button { - /// - public override void ClearState() + /// + /// The sprite rendered on the button. + /// + public SpriteHandle ButtonSprite { get; set; } + + /// + /// Whether or not to hide the border of the button. + /// + public bool HideBorder = true; + + /// + /// Initializes a new instance of the class. + /// + /// The sprite used by the button. + public IconButton(SpriteHandle buttonSprite) + : this(0, 0, buttonSprite) { - base.ClearState(); - - if (_isPressed) - OnPressEnd(); } - /// - public override void DrawSelf() + /// + /// Initializes a new instance of the class. + /// + /// Position X coordinate + /// Position Y coordinate + /// The sprite used by the button. + /// Width + /// Height + /// Whether or not to hide the border. + public IconButton(float x, float y, SpriteHandle buttonSprite, float width = 120, float height = DefaultHeight, bool hideBorder = true) + : base(x, y, width, height) { - // Cache data - Rectangle clientRect = new Rectangle(Float2.Zero, Size); - bool enabled = EnabledInHierarchy; - Color backgroundColor = BackgroundColor; - Color borderColor = BorderColor; - Color textColor = TextColor; - if (!enabled) - { - backgroundColor *= 0.5f; - borderColor *= 0.5f; - textColor *= 0.6f; - } - else if (_isPressed) - { - backgroundColor = BackgroundColorSelected; - borderColor = BorderColorSelected; - } - else if (IsMouseOver || IsNavFocused) - { - backgroundColor = BackgroundColorHighlighted; - borderColor = BorderColorHighlighted; - } - - // Draw background - if (BackgroundBrush != null) - BackgroundBrush.Draw(clientRect, backgroundColor); - else - Render2D.FillRectangle(clientRect, backgroundColor); - Render2D.DrawRectangle(clientRect, borderColor); - - // Draw text - Render2D.DrawText(_font?.GetFont(), TextMaterial, _text, clientRect, textColor, TextAlignment.Center, TextAlignment.Center); + ButtonSprite = buttonSprite; + BackgroundBrush = new SpriteBrush(ButtonSprite); + HideBorder = hideBorder; } - /// - public override void OnMouseEnter(Float2 location) + /// + /// Initializes a new instance of the class. + /// + /// Position + /// Size + /// The sprite used by the button. + public IconButton(Float2 location, Float2 size, SpriteHandle buttonSprite) + : this(location.X, location.Y, buttonSprite, size.X, size.Y) { - base.OnMouseEnter(location); - - HoverBegin?.Invoke(); } - /// - public override void OnMouseLeave() + /// + /// Sets the colors of the button, taking into account the field.> + /// + /// The color to use. + public override void SetColors(Color color) { - if (_isPressed) - { - OnPressEnd(); - } + BackgroundColor = color; + BackgroundColorSelected = color.RGBMultiplied(0.8f); + BackgroundColorHighlighted = color.RGBMultiplied(1.2f); - HoverEnd?.Invoke(); - - base.OnMouseLeave(); - } - - /// - public override bool OnMouseDown(Float2 location, MouseButton button) - { - if (base.OnMouseDown(location, button)) - return true; - - if (button == MouseButton.Left && !_isPressed) - { - OnPressBegin(); - return true; - } - return false; - } - - /// - public override bool OnMouseUp(Float2 location, MouseButton button) - { - if (base.OnMouseUp(location, button)) - return true; - - if (button == MouseButton.Left && _isPressed) - { - OnPressEnd(); - OnClick(); - return true; - } - return false; - } - - /// - public override bool OnTouchDown(Float2 location, int pointerId) - { - if (base.OnTouchDown(location, pointerId)) - return true; - - if (!_isPressed) - { - OnPressBegin(); - return true; - } - return false; - } - - /// - public override bool OnTouchUp(Float2 location, int pointerId) - { - if (base.OnTouchUp(location, pointerId)) - return true; - - if (_isPressed) - { - OnPressEnd(); - OnClick(); - return true; - } - return false; - } - - /// - public override void OnTouchLeave() - { - if (_isPressed) - { - OnPressEnd(); - } - - base.OnTouchLeave(); - } - - /// - public override void OnLostFocus() - { - if (_isPressed) - { - OnPressEnd(); - } - - base.OnLostFocus(); - } - - /// - public override void OnSubmit() - { - OnClick(); - - base.OnSubmit(); + BorderColor = HideBorder ? Color.Transparent : color.RGBMultiplied(0.5f); + BorderColorSelected = BorderColor; + BorderColorHighlighted = BorderColor; } } } From 627b1cee103b58fe42324d72e6fa69323834f508 Mon Sep 17 00:00:00 2001 From: Menotdan <32620310+Menotdan@users.noreply.github.com> Date: Wed, 10 May 2023 17:49:45 -0400 Subject: [PATCH 04/32] Resolve issues that came through when attempting to merge previously. --- Source/Editor/Content/Items/ContentFolder.cs | 1 + Source/Editor/Content/Items/ContentItem.cs | 2 ++ Source/Editor/Utilities/Utils.cs | 1 + 3 files changed, 4 insertions(+) diff --git a/Source/Editor/Content/Items/ContentFolder.cs b/Source/Editor/Content/Items/ContentFolder.cs index a654ea697..86c374fee 100644 --- a/Source/Editor/Content/Items/ContentFolder.cs +++ b/Source/Editor/Content/Items/ContentFolder.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using FlaxEditor.GUI.Drag; using FlaxEngine; using FlaxEngine.GUI; diff --git a/Source/Editor/Content/Items/ContentItem.cs b/Source/Editor/Content/Items/ContentItem.cs index 60adb1caa..2a0390b30 100644 --- a/Source/Editor/Content/Items/ContentItem.cs +++ b/Source/Editor/Content/Items/ContentItem.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Text; using FlaxEditor.Content.GUI; using FlaxEditor.GUI.Drag; using FlaxEngine; diff --git a/Source/Editor/Utilities/Utils.cs b/Source/Editor/Utilities/Utils.cs index 614c4cdc7..acc0cf605 100644 --- a/Source/Editor/Utilities/Utils.cs +++ b/Source/Editor/Utilities/Utils.cs @@ -1030,6 +1030,7 @@ namespace FlaxEditor.Utilities return path; } + /// /// Gets the asset name relative to the project root folder (without asset file extension) /// /// The asset path. From 4482fc0bcfc7242ece25be5679b5b62e127f33a2 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 16 May 2023 12:13:23 +0200 Subject: [PATCH 05/32] Cleanup #1086 Reuse text for UI tooltip Remove `IconButton` and use simple button instead Cleanup code style --- .../Editors/ActorTransformEditor.cs | 17 ++++++++--------- Source/Editor/Utilities/Utils.cs | 4 +--- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs b/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs index fb7a0b29d..076b94b0a 100644 --- a/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs +++ b/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs @@ -87,23 +87,20 @@ namespace FlaxEditor.CustomEditors.Editors LinkValues = Editor.Instance.Windows.PropertiesWin.ScaleLinked; - // Add button with the link icon. - //Editor.Instance.Icons.Link32 - _linkButton = new IconButton(Editor.Instance.Icons.Link32) + // Add button with the link icon + _linkButton = new Button { + BackgroundBrush = new SpriteBrush(Editor.Instance.Icons.Link32), Parent = LinkedLabel, Width = 18, Height = 18, - AnchorPreset = AnchorPresets.TopLeft + AnchorPreset = AnchorPresets.TopLeft, }; - _linkButton.Clicked += ToggleLink; SetLinkStyle(); - var x = LinkedLabel.Text.Value.Length * 7 + 5; _linkButton.LocalX += x; _linkButton.LocalY += 1; - LinkedLabel.SetupContextMenu += (label, menu, editor) => { menu.AddSeparator(); @@ -136,9 +133,11 @@ namespace FlaxEditor.CustomEditors.Editors private void SetLinkStyle() { - Color backgroundColor = LinkValues ? FlaxEngine.GUI.Style.Current.BackgroundSelected : FlaxEngine.GUI.Style.Current.ForegroundDisabled; + var style = FlaxEngine.GUI.Style.Current; + var backgroundColor = LinkValues ? style.Foreground : style.ForegroundDisabled; _linkButton.SetColors(backgroundColor); - _linkButton.TooltipText = (LinkValues ? "Unlink" : "Link") + " values for uniform scaling."; + _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/Utilities/Utils.cs b/Source/Editor/Utilities/Utils.cs index acc0cf605..5f836e069 100644 --- a/Source/Editor/Utilities/Utils.cs +++ b/Source/Editor/Utilities/Utils.cs @@ -1037,9 +1037,7 @@ namespace FlaxEditor.Utilities /// The processed name path. public static string GetAssetNamePath(string path) { - var projectFolder = Globals.ProjectFolder; - if (path.StartsWith(projectFolder)) - path = path.Substring(projectFolder.Length + 1); + path = GetAssetNamePathWithExt(path); return StringUtils.GetPathWithoutExtension(path); } From 3fa9f9e9cfc9ea52e7e493dc59d6aa2139f525db Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 16 May 2023 12:14:52 +0200 Subject: [PATCH 06/32] Follow up 4482fc0bcfc7242ece25be5679b5b62e127f33a2 #1086 --- Source/Engine/UI/GUI/Common/IconButton.cs | 74 ----------------------- 1 file changed, 74 deletions(-) delete mode 100644 Source/Engine/UI/GUI/Common/IconButton.cs diff --git a/Source/Engine/UI/GUI/Common/IconButton.cs b/Source/Engine/UI/GUI/Common/IconButton.cs deleted file mode 100644 index e2652cd9f..000000000 --- a/Source/Engine/UI/GUI/Common/IconButton.cs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. - -using System; - -namespace FlaxEngine.GUI -{ - /// - /// Button with an icon. - /// - public class IconButton : Button - { - /// - /// The sprite rendered on the button. - /// - public SpriteHandle ButtonSprite { get; set; } - - /// - /// Whether or not to hide the border of the button. - /// - public bool HideBorder = true; - - /// - /// Initializes a new instance of the class. - /// - /// The sprite used by the button. - public IconButton(SpriteHandle buttonSprite) - : this(0, 0, buttonSprite) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// Position X coordinate - /// Position Y coordinate - /// The sprite used by the button. - /// Width - /// Height - /// Whether or not to hide the border. - public IconButton(float x, float y, SpriteHandle buttonSprite, float width = 120, float height = DefaultHeight, bool hideBorder = true) - : base(x, y, width, height) - { - ButtonSprite = buttonSprite; - BackgroundBrush = new SpriteBrush(ButtonSprite); - HideBorder = hideBorder; - } - - /// - /// Initializes a new instance of the class. - /// - /// Position - /// Size - /// The sprite used by the button. - public IconButton(Float2 location, Float2 size, SpriteHandle buttonSprite) - : this(location.X, location.Y, buttonSprite, size.X, size.Y) - { - } - - /// - /// Sets the colors of the button, taking into account the field.> - /// - /// The color to use. - public override void SetColors(Color color) - { - BackgroundColor = color; - BackgroundColorSelected = color.RGBMultiplied(0.8f); - BackgroundColorHighlighted = color.RGBMultiplied(1.2f); - - BorderColor = HideBorder ? Color.Transparent : color.RGBMultiplied(0.5f); - BorderColorSelected = BorderColor; - BorderColorHighlighted = BorderColor; - } - } -} From 64f3f1e9bc38125d1d457ab3d63d92a7b510b67d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 16 May 2023 13:08:25 +0200 Subject: [PATCH 07/32] Add network error log for missing network object when accessing ownership #1066 --- Source/Engine/Networking/NetworkReplicator.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Source/Engine/Networking/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp index e150b1ea0..2145753fe 100644 --- a/Source/Engine/Networking/NetworkReplicator.cpp +++ b/Source/Engine/Networking/NetworkReplicator.cpp @@ -878,9 +878,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; @@ -903,9 +910,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; From 953ae3e9bbbfd9d353bd7d2b0b390edbf784b308 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 16 May 2023 14:58:16 +0200 Subject: [PATCH 08/32] Fix hierarchical network ownership propagation to sub-objects #1066 --- .../Engine/Networking/NetworkReplicator.cpp | 86 ++++++++++++------- Source/Engine/Networking/NetworkReplicator.h | 6 +- 2 files changed, 60 insertions(+), 32 deletions(-) diff --git a/Source/Engine/Networking/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp index 2145753fe..3142e54b3 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)); } @@ -864,7 +877,7 @@ void NetworkReplicator::DespawnObject(ScriptingObject* obj) 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()); @@ -896,7 +909,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()); @@ -927,10 +940,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 @@ -958,31 +972,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 @@ -990,7 +1006,7 @@ 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); } } @@ -1196,9 +1212,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; } } } @@ -1637,7 +1655,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]; @@ -1666,6 +1684,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..84db2797b 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); From 70593177c71d730cc89dd622fc2f11e241fd3b2a Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 16 May 2023 14:58:47 +0200 Subject: [PATCH 09/32] Add `NetworkReplicator::HasObject` --- Source/Engine/Networking/NetworkReplicator.cpp | 17 +++++++++++++++++ Source/Engine/Networking/NetworkReplicator.h | 7 +++++++ 2 files changed, 24 insertions(+) diff --git a/Source/Engine/Networking/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp index 3142e54b3..165d20659 100644 --- a/Source/Engine/Networking/NetworkReplicator.cpp +++ b/Source/Engine/Networking/NetworkReplicator.cpp @@ -874,6 +874,23 @@ 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; diff --git a/Source/Engine/Networking/NetworkReplicator.h b/Source/Engine/Networking/NetworkReplicator.h index 84db2797b..0e9a2d0d8 100644 --- a/Source/Engine/Networking/NetworkReplicator.h +++ b/Source/Engine/Networking/NetworkReplicator.h @@ -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. /// From e045f096a9dbe635ad02548155b30e61ea842812 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 16 May 2023 14:59:15 +0200 Subject: [PATCH 10/32] Add network debugging panel to actors and scripts #1066 --- .../Editor/CustomEditors/CustomEditorsUtil.cs | 5 +++- .../CustomEditors/Dedicated/ActorEditor.cs | 2 +- .../Dedicated/ScriptingObjectEditor.cs | 29 +++++++++++++++++++ .../CustomEditors/Dedicated/ScriptsEditor.cs | 1 - 4 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 Source/Editor/CustomEditors/Dedicated/ScriptingObjectEditor.cs 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; From 31fb25a43de10ede8a31cb79a378bde2f0424b6f Mon Sep 17 00:00:00 2001 From: Wiktor Kocielski Date: Wed, 17 May 2023 02:28:45 +0300 Subject: [PATCH 11/32] Fix dispatch order regression --- Source/Engine/Networking/NetworkManager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Networking/NetworkManager.cpp b/Source/Engine/Networking/NetworkManager.cpp index 88a104c3e..373a24c9d 100644 --- a/Source/Engine/Networking/NetworkManager.cpp +++ b/Source/Engine/Networking/NetworkManager.cpp @@ -318,10 +318,10 @@ bool NetworkManager::StartHost() // Auto-connect host LocalClient->State = NetworkConnectionState::Connected; - ClientConnected(LocalClient); - State = NetworkConnectionState::Connected; StateChanged(); + + ClientConnected(LocalClient); return false; } From f0564e0b066ec0673274b905c223b0ff741e151b Mon Sep 17 00:00:00 2001 From: Wiktor Kocielski Date: Wed, 17 May 2023 03:34:03 +0300 Subject: [PATCH 12/32] Fix NetworkReplicator::SetObjectOwnership not considering spawn queue for hierarchical asignment --- Source/Engine/Networking/NetworkReplicator.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Source/Engine/Networking/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp index 165d20659..be7571b80 100644 --- a/Source/Engine/Networking/NetworkReplicator.cpp +++ b/Source/Engine/Networking/NetworkReplicator.cpp @@ -1026,6 +1026,12 @@ void NetworkReplicator::SetObjectOwnership(ScriptingObject* obj, uint32 ownerCli 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); + } } } From b244ffedd2aafd72c7e1af4d88c744d82000871d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 17 May 2023 12:23:01 +0200 Subject: [PATCH 13/32] Fix output log text ranges to handle line ending --- Source/Editor/Windows/OutputLogWindow.cs | 4 ++-- Source/Engine/Render2D/Font.h | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) 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/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); From 0d3bae376157fce471fac7b7fb52b886e482930e Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 17 May 2023 21:37:55 +0200 Subject: [PATCH 14/32] Minor adjustment to #1105 so the local client state is reflected as connecting --- Source/Engine/Networking/NetworkManager.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Networking/NetworkManager.cpp b/Source/Engine/Networking/NetworkManager.cpp index 373a24c9d..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::Connected; + LocalClient->State = NetworkConnectionState::Connecting; State = NetworkConnectionState::Connected; StateChanged(); - + LocalClient->State = NetworkConnectionState::Connected; ClientConnected(LocalClient); + return false; } From d5fcdf6edbb33ee85ebd6db5a38112d8f74ea9e3 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 20 May 2023 11:11:06 +0200 Subject: [PATCH 15/32] Fix `BitArray::Set` to not be `const` --- Source/Engine/Core/Collections/BitArray.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); From 2ae20c5fc440a9d46b158f01987d38b4551774f8 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 21 May 2023 22:36:14 +0200 Subject: [PATCH 16/32] Add formatting to network profiler bytes values --- Source/Editor/Windows/Profiler/Network.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); } /// From 1b8b5853671e14a386c9930bada2a3a9082b4b20 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 21 May 2023 22:38:48 +0200 Subject: [PATCH 17/32] Optimize network replication when no client can receive object --- Source/Engine/Networking/NetworkReplicator.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Networking/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp index be7571b80..8ef668fdb 100644 --- a/Source/Engine/Networking/NetworkReplicator.cpp +++ b/Source/Engine/Networking/NetworkReplicator.cpp @@ -1347,6 +1347,15 @@ void NetworkInternal::NetworkReplicatorUpdate() if (item.Role != NetworkObjectRole::OwnedAuthoritative && (!isClient && item.OwnerClientId != NetworkManager::LocalClientId)) continue; // Send replication messages of only owned objects or from other client objects + // Skip serialization of objects that none will receive + if (!isClient) + { + // TODO: per-object relevancy for connected clients (eg. skip replicating actor to far players) + BuildCachedTargets(item); + if (CachedTargets.Count() == 0) + continue; + } + if (item.AsNetworkObject) item.AsNetworkObject->OnNetworkSerialize(); @@ -1400,8 +1409,6 @@ void NetworkInternal::NetworkReplicatorUpdate() 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); } From 2881ca17a09f98ac1bbb4c3c643d736974f2f40c Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 22 May 2023 18:06:08 +0200 Subject: [PATCH 18/32] Fix bindings code instance object param `obj` to `__obj` to prevent name collisions --- .../Bindings/BindingsGenerator.CSharp.cs | 2 +- .../Bindings/BindingsGenerator.Cpp.cs | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs index 6f7cba455..1495c7e70 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs @@ -450,7 +450,7 @@ namespace Flax.Build.Bindings var separator = false; if (!functionInfo.IsStatic) { - contents.Append("IntPtr obj"); + contents.Append("IntPtr __obj"); separator = true; } diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs index 9acaf1f9c..826d77248 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs @@ -948,7 +948,7 @@ namespace Flax.Build.Bindings var separator = false; if (!functionInfo.IsStatic) { - contents.Append(string.Format("{0}* obj", caller.Name)); + contents.Append(string.Format("{0}* __obj", caller.Name)); separator = true; } @@ -1025,7 +1025,7 @@ namespace Flax.Build.Bindings contents.AppendLine(); contents.AppendLine(" {"); if (!functionInfo.IsStatic) - contents.AppendLine(" if (obj == nullptr) DebugLog::ThrowNullReference();"); + contents.AppendLine(" if (__obj == nullptr) DebugLog::ThrowNullReference();"); string callBegin = " "; if (functionInfo.Glue.UseReferenceForResult) @@ -1052,7 +1052,7 @@ namespace Flax.Build.Bindings else { // Call native member method - call = $"obj->{functionInfo.Name}"; + call = $"__obj->{functionInfo.Name}"; } string callParams = string.Empty; separator = false; @@ -1760,7 +1760,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) { @@ -1838,7 +1838,7 @@ namespace Flax.Build.Bindings CppInternalCalls.Add(new KeyValuePair(eventInfo.Name + "_Bind", eventInfo.Name + "_ManagedBind")); contents.AppendFormat(" static void {0}_ManagedBind(", eventInfo.Name); if (!eventInfo.IsStatic) - contents.AppendFormat("{0}* obj, ", classTypeNameNative); + contents.AppendFormat("{0}* __obj, ", classTypeNameNative); contents.Append("bool bind)").AppendLine(); contents.Append(" {").AppendLine(); contents.Append(" Function();", eventInfo.Name).AppendLine(); else - contents.AppendFormat(" f.Bind<{1}Internal, &{1}Internal::{0}_ManagedWrapper>(({1}Internal*)obj);", eventInfo.Name, classTypeNameInternal).AppendLine(); + contents.AppendFormat(" f.Bind<{1}Internal, &{1}Internal::{0}_ManagedWrapper>(({1}Internal*)__obj);", eventInfo.Name, classTypeNameInternal).AppendLine(); contents.Append(" if (bind)").AppendLine(); contents.AppendFormat(" {0}{1}.Bind(f);", bindPrefix, eventInfo.Name).AppendLine(); contents.Append(" else").AppendLine(); @@ -1890,7 +1890,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(" Function();"); - contents.AppendLine(" wrapper->Object = obj;"); + contents.AppendLine(" wrapper->Object = __obj;"); contents.AppendLine(" return wrapper;"); contents.AppendLine(" }"); From f5adbc08fa522bba62fb59dd3c643ec86cb86b2b Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 23 May 2023 16:08:51 +0200 Subject: [PATCH 19/32] Remove unused `Function::TryCall` --- Source/Engine/Core/Delegate.h | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) 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)...); From 4879b9bd909bb7813b03c1f879287ded74548ba9 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 23 May 2023 16:11:34 +0200 Subject: [PATCH 20/32] Add CPU profiler events to various networking functions --- Source/Engine/Networking/NetworkPeer.cpp | 2 + .../Engine/Networking/NetworkReplicator.cpp | 120 ++++++++++-------- 2 files changed, 68 insertions(+), 54 deletions(-) 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/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp index 8ef668fdb..7ac978bd2 100644 --- a/Source/Engine/Networking/NetworkReplicator.cpp +++ b/Source/Engine/Networking/NetworkReplicator.cpp @@ -1302,28 +1302,31 @@ 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 @@ -1438,49 +1441,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 despawn 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 despawn 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); @@ -1488,6 +1494,7 @@ void NetworkInternal::NetworkReplicatorUpdate() void NetworkInternal::OnNetworkMessageObjectReplicate(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer) { + PROFILE_CPU(); NetworkMessageObjectReplicate msgData; event.Message.ReadStructure(msgData); ScopeLock lock(ObjectsLock); @@ -1519,6 +1526,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); @@ -1531,6 +1539,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)); @@ -1759,6 +1768,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); @@ -1790,6 +1800,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); @@ -1833,6 +1844,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); From 914f7b842e45d4913bec92be001f340db4959b83 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 23 May 2023 18:22:23 +0200 Subject: [PATCH 21/32] Optimize interface method lookup to eliminate` strlen` --- Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs index 826d77248..41887c347 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs @@ -2339,7 +2339,7 @@ namespace Flax.Build.Bindings contents.AppendLine(" auto typeHandle = Object->GetTypeHandle();"); 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;"); From 8ba17f10267d6951eecea47b246c86e02ba41b94 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 23 May 2023 18:22:43 +0200 Subject: [PATCH 22/32] Optimize `MUtils::UnboxScriptingTypeHandle` --- Source/Engine/Scripting/ManagedCLR/MUtils.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Source/Engine/Scripting/ManagedCLR/MUtils.cpp b/Source/Engine/Scripting/ManagedCLR/MUtils.cpp index 3d8e93250..bdc16cbd5 100644 --- a/Source/Engine/Scripting/ManagedCLR/MUtils.cpp +++ b/Source/Engine/Scripting/ManagedCLR/MUtils.cpp @@ -191,10 +191,9 @@ ScriptingTypeHandle MUtils::UnboxScriptingTypeHandle(MonoReflectionType* value) MonoClass* klass = GetClass(value); if (!klass) return ScriptingTypeHandle(); - const MString typeName = MUtils::GetClassFullname(klass); - const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(typeName); + const ScriptingTypeHandle typeHandle = ManagedBinaryModule::FindType(klass); if (!typeHandle) - LOG(Warning, "Unknown scripting type {}", String(typeName)); + LOG(Warning, "Unknown scripting type {}", String(MUtils::GetClassFullname(klass))); return typeHandle; } From eb2fc0f02fa469b9b82a99f24b70407a657fb80d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 24 May 2023 09:46:38 +0200 Subject: [PATCH 23/32] Add **Network Replication Hierarchy** for robust control over replication in multiplayer games --- .../NetworkReplicationHierarchy.cpp | 199 ++++++++++++ .../Networking/NetworkReplicationHierarchy.cs | 25 ++ .../Networking/NetworkReplicationHierarchy.h | 258 +++++++++++++++ .../Engine/Networking/NetworkReplicator.cpp | 300 ++++++++++++------ Source/Engine/Networking/NetworkReplicator.h | 11 + Source/Engine/Networking/Types.h | 1 + 6 files changed, 691 insertions(+), 103 deletions(-) create mode 100644 Source/Engine/Networking/NetworkReplicationHierarchy.cpp create mode 100644 Source/Engine/Networking/NetworkReplicationHierarchy.cs create mode 100644 Source/Engine/Networking/NetworkReplicationHierarchy.h diff --git a/Source/Engine/Networking/NetworkReplicationHierarchy.cpp b/Source/Engine/Networking/NetworkReplicationHierarchy.cpp new file mode 100644 index 000000000..783992fd8 --- /dev/null +++ b/Source/Engine/Networking/NetworkReplicationHierarchy.cpp @@ -0,0 +1,199 @@ +// 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) +{ + ASSERT(obj.Object && 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.ReplicationUpdatesLeft > 0) + { + // Move to the next frame + obj.ReplicationUpdatesLeft--; + } + else + { + NetworkClientsMask targetClients = result->GetClientsMask(); + if (result->_clientsHaveLocation) + { + // 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) + { + // 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..7d6db336d --- /dev/null +++ b/Source/Engine/Networking/NetworkReplicationHierarchy.h @@ -0,0 +1,258 @@ +// 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() ScriptingObject* Object; + // The target amount of the replication updates per second (frequency of the replication). Constrained by NetworkManager::NetworkFPS. + 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. + 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 7ac978bd2..b1088bfa5 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) @@ -788,6 +822,8 @@ void NetworkReplicator::AddObject(ScriptingObject* obj, const ScriptingObject* p } } Objects.Add(MoveTemp(item)); + if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative) + Hierarchy->AddObject(obj); } void NetworkReplicator::RemoveObject(ScriptingObject* obj) @@ -801,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); } @@ -870,6 +908,8 @@ 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); } @@ -1004,6 +1044,8 @@ void NetworkReplicator::SetObjectOwnership(ScriptingObject* obj, uint32 ownerCli { // 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; @@ -1014,6 +1056,8 @@ void NetworkReplicator::SetObjectOwnership(ScriptingObject* obj, uint32 ownerCli { // 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; } } @@ -1107,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); @@ -1121,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; @@ -1140,6 +1187,7 @@ void NetworkInternal::NetworkReplicatorClear() IdsRemappingTable.Clear(); SAFE_DELETE(CachedWriteStream); SAFE_DELETE(CachedReadStream); + SAFE_DELETE(CachedReplicationResult); NewClients.Clear(); CachedTargets.Clear(); DespawnedObjects.Clear(); @@ -1268,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); @@ -1329,114 +1384,141 @@ void NetworkInternal::NetworkReplicatorUpdate() } } - // 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; - } - if (item.Role != NetworkObjectRole::OwnedAuthoritative && (!isClient && item.OwnerClientId != NetworkManager::LocalClientId)) - continue; // Send replication messages of only owned objects or from other client objects - - // Skip serialization of objects that none will receive - if (!isClient) - { - // TODO: per-object relevancy for connected clients (eg. skip replicating actor to far players) - BuildCachedTargets(item); - if (CachedTargets.Count() == 0) + 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.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()); + ASSERT(it.IsNotEnd()); + auto& item = it->Item; - // Send object to clients - { - const uint32 size = stream->GetPosition(); - ASSERT(size <= MAX_uint16) - NetworkMessageObjectReplicate msgData; - msgData.OwnerFrame = NetworkManager::Frame; - msgData.ObjectId = item.ObjectId; - msgData.ParentId = item.ParentId; - if (isClient) + // Skip serialization of objects that none will receive + if (!isClient) { - // Remap local client object ids into server ids - IdsRemappingTable.KeyOf(msgData.ObjectId, &msgData.ObjectId); - IdsRemappingTable.KeyOf(msgData.ParentId, &msgData.ParentId); - } - GetNetworkName(msgData.ObjectTypeName, obj->GetType().Fullname); - msgData.DataSize = size; - const uint32 msgMaxData = peer->Config.MessageSize - sizeof(NetworkMessageObjectReplicate); - const uint32 partMaxData = peer->Config.MessageSize - sizeof(NetworkMessageObjectReplicatePart); - uint32 partsCount = 1; - uint32 dataStart = 0; - uint32 msgDataSize = size; - if (size > msgMaxData) - { - // Send msgMaxData within first message - msgDataSize = msgMaxData; - dataStart += msgMaxData; - - // Send rest of the data in separate parts - partsCount += Math::DivideAndRoundUp(size - dataStart, partMaxData); - } - else - dataStart += size; - ASSERT(partsCount <= MAX_uint8) - msgData.PartsCount = partsCount; - NetworkMessage msg = peer->BeginSendMessage(); - msg.WriteStructure(msgData); - msg.WriteBytes(stream->GetBuffer(), msgDataSize); - if (isClient) - peer->EndSendMessage(NetworkChannelType::Unreliable, msg); - else - { - peer->EndSendMessage(NetworkChannelType::Unreliable, msg, CachedTargets); + // TODO: per-object relevancy for connected clients (eg. skip replicating actor to far players) + BuildCachedTargets(item, e.TargetClients); + if (CachedTargets.Count() == 0) + return; } - // Send all other parts - for (uint32 partIndex = 1; partIndex < partsCount; partIndex++) + if (item.AsNetworkObject) + item.AsNetworkObject->OnNetworkSerialize(); + + // Serialize object + stream->Initialize(); + const bool failed = NetworkReplicator::InvokeSerializer(obj->GetTypeHandle(), obj, stream, true); + if (failed) { - NetworkMessageObjectReplicatePart msgDataPart; - msgDataPart.OwnerFrame = msgData.OwnerFrame; - msgDataPart.ObjectId = msgData.ObjectId; - msgDataPart.DataSize = msgData.DataSize; - msgDataPart.PartsCount = msgData.PartsCount; - msgDataPart.PartStart = dataStart; - msgDataPart.PartSize = Math::Min(size - dataStart, partMaxData); - msg = peer->BeginSendMessage(); - msg.WriteStructure(msgDataPart); - msg.WriteBytes(stream->GetBuffer() + msgDataPart.PartStart, msgDataPart.PartSize); - dataStart += msgDataPart.PartSize; + //NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Cannot serialize object {} of type {} (missing serialization logic)", item.ToString(), obj->GetType().ToString()); + return; + } + + // Send object to clients + { + const uint32 size = stream->GetPosition(); + ASSERT(size <= MAX_uint16) + NetworkMessageObjectReplicate msgData; + msgData.OwnerFrame = NetworkManager::Frame; + msgData.ObjectId = item.ObjectId; + msgData.ParentId = item.ParentId; + if (isClient) + { + // Remap local client object ids into server ids + IdsRemappingTable.KeyOf(msgData.ObjectId, &msgData.ObjectId); + IdsRemappingTable.KeyOf(msgData.ParentId, &msgData.ParentId); + } + GetNetworkName(msgData.ObjectTypeName, obj->GetType().Fullname); + msgData.DataSize = size; + const uint32 msgMaxData = peer->Config.MessageSize - sizeof(NetworkMessageObjectReplicate); + const uint32 partMaxData = peer->Config.MessageSize - sizeof(NetworkMessageObjectReplicatePart); + uint32 partsCount = 1; + uint32 dataStart = 0; + uint32 msgDataSize = size; + if (size > msgMaxData) + { + // Send msgMaxData within first message + msgDataSize = msgMaxData; + dataStart += msgMaxData; + + // Send rest of the data in separate parts + partsCount += Math::DivideAndRoundUp(size - dataStart, partMaxData); + } + else + dataStart += size; + ASSERT(partsCount <= MAX_uint8) + msgData.PartsCount = partsCount; + NetworkMessage msg = peer->BeginSendMessage(); + msg.WriteStructure(msgData); + msg.WriteBytes(stream->GetBuffer(), msgDataSize); if (isClient) peer->EndSendMessage(NetworkChannelType::Unreliable, msg); else peer->EndSendMessage(NetworkChannelType::Unreliable, msg, CachedTargets); - } - ASSERT_LOW_LAYER(dataStart == size); - // TODO: stats for bytes send per object type + // Send all other parts + for (uint32 partIndex = 1; partIndex < partsCount; partIndex++) + { + NetworkMessageObjectReplicatePart msgDataPart; + msgDataPart.OwnerFrame = msgData.OwnerFrame; + msgDataPart.ObjectId = msgData.ObjectId; + msgDataPart.DataSize = msgData.DataSize; + msgDataPart.PartsCount = msgData.PartsCount; + msgDataPart.PartStart = dataStart; + msgDataPart.PartSize = Math::Min(size - dataStart, partMaxData); + msg = peer->BeginSendMessage(); + msg.WriteStructure(msgDataPart); + msg.WriteBytes(stream->GetBuffer() + msgDataPart.PartStart, msgDataPart.PartSize); + dataStart += msgDataPart.PartSize; + if (isClient) + peer->EndSendMessage(NetworkChannelType::Unreliable, msg); + else + peer->EndSendMessage(NetworkChannelType::Unreliable, msg, CachedTargets); + } + ASSERT_LOW_LAYER(dataStart == size); + + // TODO: stats for bytes send per object type + } } } @@ -1564,7 +1646,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) { @@ -1719,6 +1805,8 @@ 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()); @@ -1786,6 +1874,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(); @@ -1822,12 +1912,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()) diff --git a/Source/Engine/Networking/NetworkReplicator.h b/Source/Engine/Networking/NetworkReplicator.h index 0e9a2d0d8..931e13afa 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. /// 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; From 47f9bc2017e11b04dfc969b69253e1e98dae7c0a Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 24 May 2023 09:47:27 +0200 Subject: [PATCH 24/32] Fix regression in Content Item tooltip #1085 --- Source/Editor/Content/Items/ContentItem.cs | 5 +++-- Source/Editor/Content/Items/NewItem.cs | 7 +++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Content/Items/ContentItem.cs b/Source/Editor/Content/Items/ContentItem.cs index 2a0390b30..66825fb42 100644 --- a/Source/Editor/Content/Items/ContentItem.cs +++ b/Source/Editor/Content/Items/ContentItem.cs @@ -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; + } } } From 1a6e4bf14b7e9e6ccd198ff8c09c185990e4de01 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 24 May 2023 09:48:02 +0200 Subject: [PATCH 25/32] Fix missing tracy header in cooked build #948 --- Source/ThirdParty/tracy/tracy.Build.cs | 1 + 1 file changed, 1 insertion(+) 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")); } } From c4f57d19db4442da6955d61dd0c45ad1c0afe377 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 24 May 2023 10:52:59 +0200 Subject: [PATCH 26/32] Fix network replicator incorrect error message filter --- Source/Engine/Networking/NetworkReplicator.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Networking/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp index b1088bfa5..42da4870b 100644 --- a/Source/Engine/Networking/NetworkReplicator.cpp +++ b/Source/Engine/Networking/NetworkReplicator.cpp @@ -1835,7 +1835,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()); } From 2c1f6e561aaf9d350d18082dbba974792f4097ad Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 24 May 2023 11:52:32 +0200 Subject: [PATCH 27/32] Fix replication regression from eb2fc0f02fa469b9b82a99f24b70407a657fb80d --- .../Engine/Networking/NetworkReplicator.cpp | 117 +++++++++--------- 1 file changed, 57 insertions(+), 60 deletions(-) diff --git a/Source/Engine/Networking/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp index 42da4870b..6579d2cc9 100644 --- a/Source/Engine/Networking/NetworkReplicator.cpp +++ b/Source/Engine/Networking/NetworkReplicator.cpp @@ -1436,10 +1436,9 @@ void NetworkInternal::NetworkReplicatorUpdate() // Skip serialization of objects that none will receive if (!isClient) { - // TODO: per-object relevancy for connected clients (eg. skip replicating actor to far players) BuildCachedTargets(item, e.TargetClients); if (CachedTargets.Count() == 0) - return; + continue; } if (item.AsNetworkObject) @@ -1451,74 +1450,72 @@ void NetworkInternal::NetworkReplicatorUpdate() if (failed) { //NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Cannot serialize object {} of type {} (missing serialization logic)", item.ToString(), obj->GetType().ToString()); - return; + continue; } // Send object to clients + const uint32 size = stream->GetPosition(); + ASSERT(size <= MAX_uint16) + NetworkMessageObjectReplicate msgData; + msgData.OwnerFrame = NetworkManager::Frame; + msgData.ObjectId = item.ObjectId; + msgData.ParentId = item.ParentId; + if (isClient) { - const uint32 size = stream->GetPosition(); - ASSERT(size <= MAX_uint16) - NetworkMessageObjectReplicate msgData; - msgData.OwnerFrame = NetworkManager::Frame; - msgData.ObjectId = item.ObjectId; - msgData.ParentId = item.ParentId; - if (isClient) - { - // Remap local client object ids into server ids - IdsRemappingTable.KeyOf(msgData.ObjectId, &msgData.ObjectId); - IdsRemappingTable.KeyOf(msgData.ParentId, &msgData.ParentId); - } - GetNetworkName(msgData.ObjectTypeName, obj->GetType().Fullname); - msgData.DataSize = size; - const uint32 msgMaxData = peer->Config.MessageSize - sizeof(NetworkMessageObjectReplicate); - const uint32 partMaxData = peer->Config.MessageSize - sizeof(NetworkMessageObjectReplicatePart); - uint32 partsCount = 1; - uint32 dataStart = 0; - uint32 msgDataSize = size; - if (size > msgMaxData) - { - // Send msgMaxData within first message - msgDataSize = msgMaxData; - dataStart += msgMaxData; + // Remap local client object ids into server ids + IdsRemappingTable.KeyOf(msgData.ObjectId, &msgData.ObjectId); + IdsRemappingTable.KeyOf(msgData.ParentId, &msgData.ParentId); + } + GetNetworkName(msgData.ObjectTypeName, obj->GetType().Fullname); + msgData.DataSize = size; + const uint32 msgMaxData = peer->Config.MessageSize - sizeof(NetworkMessageObjectReplicate); + const uint32 partMaxData = peer->Config.MessageSize - sizeof(NetworkMessageObjectReplicatePart); + uint32 partsCount = 1; + uint32 dataStart = 0; + uint32 msgDataSize = size; + if (size > msgMaxData) + { + // Send msgMaxData within first message + msgDataSize = msgMaxData; + dataStart += msgMaxData; - // Send rest of the data in separate parts - partsCount += Math::DivideAndRoundUp(size - dataStart, partMaxData); - } - else - dataStart += size; - ASSERT(partsCount <= MAX_uint8) - msgData.PartsCount = partsCount; - NetworkMessage msg = peer->BeginSendMessage(); - msg.WriteStructure(msgData); - msg.WriteBytes(stream->GetBuffer(), msgDataSize); + // Send rest of the data in separate parts + partsCount += Math::DivideAndRoundUp(size - dataStart, partMaxData); + } + else + dataStart += size; + ASSERT(partsCount <= MAX_uint8) + msgData.PartsCount = partsCount; + NetworkMessage msg = peer->BeginSendMessage(); + msg.WriteStructure(msgData); + msg.WriteBytes(stream->GetBuffer(), msgDataSize); + if (isClient) + peer->EndSendMessage(NetworkChannelType::Unreliable, msg); + else + peer->EndSendMessage(NetworkChannelType::Unreliable, msg, CachedTargets); + + // Send all other parts + for (uint32 partIndex = 1; partIndex < partsCount; partIndex++) + { + NetworkMessageObjectReplicatePart msgDataPart; + msgDataPart.OwnerFrame = msgData.OwnerFrame; + msgDataPart.ObjectId = msgData.ObjectId; + msgDataPart.DataSize = msgData.DataSize; + msgDataPart.PartsCount = msgData.PartsCount; + msgDataPart.PartStart = dataStart; + msgDataPart.PartSize = Math::Min(size - dataStart, partMaxData); + msg = peer->BeginSendMessage(); + msg.WriteStructure(msgDataPart); + msg.WriteBytes(stream->GetBuffer() + msgDataPart.PartStart, msgDataPart.PartSize); + dataStart += msgDataPart.PartSize; if (isClient) peer->EndSendMessage(NetworkChannelType::Unreliable, msg); else peer->EndSendMessage(NetworkChannelType::Unreliable, msg, CachedTargets); - - // Send all other parts - for (uint32 partIndex = 1; partIndex < partsCount; partIndex++) - { - NetworkMessageObjectReplicatePart msgDataPart; - msgDataPart.OwnerFrame = msgData.OwnerFrame; - msgDataPart.ObjectId = msgData.ObjectId; - msgDataPart.DataSize = msgData.DataSize; - msgDataPart.PartsCount = msgData.PartsCount; - msgDataPart.PartStart = dataStart; - msgDataPart.PartSize = Math::Min(size - dataStart, partMaxData); - msg = peer->BeginSendMessage(); - msg.WriteStructure(msgDataPart); - msg.WriteBytes(stream->GetBuffer() + msgDataPart.PartStart, msgDataPart.PartSize); - dataStart += msgDataPart.PartSize; - if (isClient) - peer->EndSendMessage(NetworkChannelType::Unreliable, msg); - else - peer->EndSendMessage(NetworkChannelType::Unreliable, msg, CachedTargets); - } - ASSERT_LOW_LAYER(dataStart == size); - - // TODO: stats for bytes send per object type } + ASSERT_LOW_LAYER(dataStart == size); + + // TODO: stats for bytes send per object type } } From 9986d62a28f9e904e72069160459b4132925b58c Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 24 May 2023 12:06:09 +0200 Subject: [PATCH 28/32] Fix crash when scripting object gets deleted while it exists in NetworkReplicationHierarchy --- Source/Engine/Networking/NetworkReplicationHierarchy.cpp | 2 +- Source/Engine/Networking/NetworkReplicationHierarchy.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Networking/NetworkReplicationHierarchy.cpp b/Source/Engine/Networking/NetworkReplicationHierarchy.cpp index 783992fd8..450f22d47 100644 --- a/Source/Engine/Networking/NetworkReplicationHierarchy.cpp +++ b/Source/Engine/Networking/NetworkReplicationHierarchy.cpp @@ -110,7 +110,7 @@ void NetworkReplicationNode::Update(NetworkReplicationHierarchyUpdateResult* res } } } - if (targetClients) + if (targetClients && obj.Object) { // Replicate this frame result->AddObject(obj.Object, targetClients); diff --git a/Source/Engine/Networking/NetworkReplicationHierarchy.h b/Source/Engine/Networking/NetworkReplicationHierarchy.h index 7d6db336d..99770c5a4 100644 --- a/Source/Engine/Networking/NetworkReplicationHierarchy.h +++ b/Source/Engine/Networking/NetworkReplicationHierarchy.h @@ -18,7 +18,7 @@ API_STRUCT(NoDefault, Namespace = "FlaxEngine.Networking") struct FLAXENGINE_API { DECLARE_SCRIPTING_TYPE_MINIMAL(NetworkReplicationObjectInfo); // The object to replicate. - API_FIELD() ScriptingObject* Object; + API_FIELD() ScriptingObjectReference Object; // The target amount of the replication updates per second (frequency of the replication). Constrained by NetworkManager::NetworkFPS. 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. From 982639f21581bbb44ea4de5be6e61647a34cf436 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 24 May 2023 22:55:02 +0200 Subject: [PATCH 29/32] Fix syncing Custom Editor value when using it to edit value-type --- Source/Editor/CustomEditors/CustomEditor.cs | 26 +++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/Source/Editor/CustomEditors/CustomEditor.cs b/Source/Editor/CustomEditors/CustomEditor.cs index 8e252a207..1330616d1 100644 --- a/Source/Editor/CustomEditors/CustomEditor.cs +++ b/Source/Editor/CustomEditors/CustomEditor.cs @@ -147,7 +147,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; @@ -158,7 +158,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(); @@ -248,6 +248,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++) From 7efb3e3f3d04781c417e41ea1cb2326f521646bd Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 24 May 2023 22:56:29 +0200 Subject: [PATCH 30/32] Add `SoftTypeReference` to scripting API for lazy-load type references (via typename) --- .../CustomEditors/Editors/TypeEditor.cs | 34 ++-- Source/Engine/Scripting/SoftTypeReference.cs | 87 ++++++++++ Source/Engine/Scripting/SoftTypeReference.h | 151 ++++++++++++++++++ Source/Engine/Serialization/JsonConverters.cs | 39 ++++- Source/Engine/Serialization/JsonSerializer.cs | 1 + 5 files changed, 298 insertions(+), 14 deletions(-) create mode 100644 Source/Engine/Scripting/SoftTypeReference.cs create mode 100644 Source/Engine/Scripting/SoftTypeReference.h diff --git a/Source/Editor/CustomEditors/Editors/TypeEditor.cs b/Source/Editor/CustomEditors/Editors/TypeEditor.cs index 722235088..9ee2c37a4 100644 --- a/Source/Editor/CustomEditors/Editors/TypeEditor.cs +++ b/Source/Editor/CustomEditors/Editors/TypeEditor.cs @@ -393,11 +393,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]); - } } } @@ -407,9 +404,7 @@ namespace FlaxEditor.CustomEditors.Editors base.Refresh(); if (!HasDifferentValues) - { _element.CustomControl.Value = new ScriptType(Values[0] as Type); - } } } @@ -425,9 +420,7 @@ namespace FlaxEditor.CustomEditors.Editors base.Initialize(layout); if (_element != null) - { _element.CustomControl.ValueChanged += () => SetValue(_element.CustomControl.Value); - } } /// @@ -436,9 +429,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/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 617768a83..30889fce0 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()); From e10aa1ff58a4c520ac6b0a0a5d47053dc0cfdd11 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 24 May 2023 22:58:07 +0200 Subject: [PATCH 31/32] Add support for editing dictionary keys that are structures --- .../CustomEditors/Editors/DictionaryEditor.cs | 39 ++++++++++++------- .../Editor/GUI/ContextMenu/ContextMenuBase.cs | 19 +++++++++ 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/Source/Editor/CustomEditors/Editors/DictionaryEditor.cs b/Source/Editor/CustomEditors/Editors/DictionaryEditor.cs index 1b9a7cca1..18500fc08 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; @@ -45,8 +44,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; } @@ -61,6 +60,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) => { @@ -78,7 +78,6 @@ namespace FlaxEditor.CustomEditors.Editors newKey = JsonSerializer.Deserialize(renamePopup.Text, keyType); else newKey = renamePopup.Text; - _editor.ChangeKey(_key, newKey); _key = newKey; Text = _key.ToString(); @@ -86,6 +85,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) { @@ -108,7 +108,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(); + }; } } @@ -159,7 +173,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; @@ -382,6 +396,7 @@ namespace FlaxEditor.CustomEditors.Editors int newItemsLeft = newSize - oldSize; while (newItemsLeft-- > 0) { + object newKey = null; if (keyType.IsPrimitive) { long uniqueKey = 0; @@ -400,8 +415,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) { @@ -421,8 +435,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)) { @@ -441,13 +454,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/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. /// From 90d633fb2d735bc49bc09e1342d179f125ad94da Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 24 May 2023 22:59:22 +0200 Subject: [PATCH 32/32] Minor improvements to new replication hierarchy --- .../Networking/NetworkReplicationHierarchy.cpp | 18 ++++++++++++------ .../Networking/NetworkReplicationHierarchy.h | 6 ++++-- Source/Engine/Networking/NetworkReplicator.cpp | 3 ++- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/Source/Engine/Networking/NetworkReplicationHierarchy.cpp b/Source/Engine/Networking/NetworkReplicationHierarchy.cpp index 450f22d47..d86acfc7f 100644 --- a/Source/Engine/Networking/NetworkReplicationHierarchy.cpp +++ b/Source/Engine/Networking/NetworkReplicationHierarchy.cpp @@ -49,10 +49,11 @@ bool NetworkReplicationHierarchyUpdateResult::GetClientLocation(int32 clientInde void NetworkReplicationNode::AddObject(NetworkReplicationHierarchyObject obj) { - ASSERT(obj.Object && 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); + 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); } @@ -79,7 +80,12 @@ void NetworkReplicationNode::Update(NetworkReplicationHierarchyUpdateResult* res const float networkFPS = NetworkManager::NetworkFPS / result->ReplicationScale; for (NetworkReplicationHierarchyObject& obj : Objects) { - if (obj.ReplicationUpdatesLeft > 0) + if (obj.ReplicationFPS <= 0.0f) + { + // Always relevant + result->AddObject(obj.Object); + } + else if (obj.ReplicationUpdatesLeft > 0) { // Move to the next frame obj.ReplicationUpdatesLeft--; @@ -87,7 +93,7 @@ void NetworkReplicationNode::Update(NetworkReplicationHierarchyUpdateResult* res else { NetworkClientsMask targetClients = result->GetClientsMask(); - if (result->_clientsHaveLocation) + if (result->_clientsHaveLocation && obj.CullDistance > 0.0f) { // Cull object against viewers locations if (const Actor* actor = obj.GetActor()) diff --git a/Source/Engine/Networking/NetworkReplicationHierarchy.h b/Source/Engine/Networking/NetworkReplicationHierarchy.h index 99770c5a4..55cd6b7b1 100644 --- a/Source/Engine/Networking/NetworkReplicationHierarchy.h +++ b/Source/Engine/Networking/NetworkReplicationHierarchy.h @@ -17,11 +17,12 @@ class Actor; 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. + // 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. + // 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; @@ -61,6 +62,7 @@ inline uint32 GetHash(const NetworkReplicationHierarchyObject& key) 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). diff --git a/Source/Engine/Networking/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp index 6579d2cc9..8d0ba3c0e 100644 --- a/Source/Engine/Networking/NetworkReplicator.cpp +++ b/Source/Engine/Networking/NetworkReplicator.cpp @@ -1430,7 +1430,8 @@ void NetworkInternal::NetworkReplicatorUpdate() { ScriptingObject* obj = e.Object; auto it = Objects.Find(obj->GetID()); - ASSERT(it.IsNotEnd()); + if (it.IsEnd()) + continue; auto& item = it->Item; // Skip serialization of objects that none will receive