diff --git a/Content/Editor/Camera/O_Camera.flax b/Content/Editor/Camera/O_Camera.flax index 5e0940624..47d37472d 100644 --- a/Content/Editor/Camera/O_Camera.flax +++ b/Content/Editor/Camera/O_Camera.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2ec3410338bc342f7de1c4af6ae0f6310c739140e83de45632f3a3bc7c47f12 -size 88720 +oid sha256:0f9bbd661420e4f930a995acc46c9530fcacb8836db4d0bbfa5df184e4d1a5cc +size 88495 diff --git a/Source/Editor/Content/Items/ContentItem.cs b/Source/Editor/Content/Items/ContentItem.cs index fb930d192..284f3f1b3 100644 --- a/Source/Editor/Content/Items/ContentItem.cs +++ b/Source/Editor/Content/Items/ContentItem.cs @@ -222,7 +222,7 @@ namespace FlaxEditor.Content /// /// Gets a value indicating whether this item can be dragged and dropped. /// - public virtual bool CanDrag => true; + public virtual bool CanDrag => Root != null; /// /// Gets a value indicating whether this exists on drive. diff --git a/Source/Editor/Content/Proxy/CollisionDataProxy.cs b/Source/Editor/Content/Proxy/CollisionDataProxy.cs index e865834fd..55e8c6327 100644 --- a/Source/Editor/Content/Proxy/CollisionDataProxy.cs +++ b/Source/Editor/Content/Proxy/CollisionDataProxy.cs @@ -80,7 +80,8 @@ namespace FlaxEditor.Content /// /// The associated model. /// The action to call once the collision data gets created (or reused from existing). - public void CreateCollisionDataFromModel(Model model, Action created = null) + /// True if start initial item renaming by user, or tru to skip it. + public void CreateCollisionDataFromModel(Model model, Action created = null, bool withRenaming = true) { // Check if there already is collision data for that model to reuse var modelItem = (AssetItem)Editor.Instance.ContentDatabase.Find(model.ID); @@ -140,7 +141,7 @@ namespace FlaxEditor.Content }); }; var initialName = (modelItem?.ShortName ?? Path.GetFileNameWithoutExtension(model.Path)) + " Collision"; - Editor.Instance.Windows.ContentWin.NewItem(this, null, create, initialName); + Editor.Instance.Windows.ContentWin.NewItem(this, null, create, initialName, withRenaming); } } } diff --git a/Source/Editor/Content/Proxy/ModelProxy.cs b/Source/Editor/Content/Proxy/ModelProxy.cs index ec6b3cd1b..845cbc80b 100644 --- a/Source/Editor/Content/Proxy/ModelProxy.cs +++ b/Source/Editor/Content/Proxy/ModelProxy.cs @@ -47,9 +47,23 @@ namespace FlaxEditor.Content menu.AddButton("Create collision data", () => { - var model = FlaxEngine.Content.LoadAsync(((ModelItem)item).ID); var collisionDataProxy = (CollisionDataProxy)Editor.Instance.ContentDatabase.GetProxy(); - collisionDataProxy.CreateCollisionDataFromModel(model); + var selection = Editor.Instance.Windows.ContentWin.View.Selection; + if (selection.Count > 1) + { + // Batch action + var items = selection.ToArray(); // Clone to prevent issue when iterating over and content window changes the selection + foreach (var contentItem in items) + { + if (contentItem is ModelItem modelItem) + collisionDataProxy.CreateCollisionDataFromModel(FlaxEngine.Content.LoadAsync(modelItem.ID), null, false); + } + } + else + { + var model = FlaxEngine.Content.LoadAsync(((ModelItem)item).ID); + collisionDataProxy.CreateCollisionDataFromModel(model); + } }); } diff --git a/Source/Editor/GUI/Tree/Tree.cs b/Source/Editor/GUI/Tree/Tree.cs index ff99f6506..3a9780ed9 100644 --- a/Source/Editor/GUI/Tree/Tree.cs +++ b/Source/Editor/GUI/Tree/Tree.cs @@ -40,6 +40,11 @@ namespace FlaxEditor.GUI.Tree private Margin _margin; private bool _autoSize = true; + /// + /// The TreeNode that is being dragged over. This could have a value when not dragging. + /// + public TreeNode DraggedOverNode = null; + /// /// Action fired when tree nodes selection gets changed. /// diff --git a/Source/Editor/GUI/Tree/TreeNode.cs b/Source/Editor/GUI/Tree/TreeNode.cs index f2c8e2d6a..8691cd14d 100644 --- a/Source/Editor/GUI/Tree/TreeNode.cs +++ b/Source/Editor/GUI/Tree/TreeNode.cs @@ -659,7 +659,7 @@ namespace FlaxEditor.GUI.Tree Render2D.DrawText(TextFont.GetFont(), _text, textRect, _cachedTextColor, TextAlignment.Near, TextAlignment.Center); // Draw drag and drop effect - if (IsDragOver) + if (IsDragOver && _tree.DraggedOverNode == this) { Color dragOverColor = style.BackgroundSelected * 0.6f; Rectangle rect; @@ -669,10 +669,10 @@ namespace FlaxEditor.GUI.Tree rect = textRect; break; case DragItemPositioning.Above: - rect = new Rectangle(textRect.X, textRect.Y - DefaultDragInsertPositionMargin - DefaultNodeOffsetY, textRect.Width, DefaultDragInsertPositionMargin * 2.0f); + rect = new Rectangle(textRect.X, textRect.Top - DefaultDragInsertPositionMargin - DefaultNodeOffsetY - _margin.Top, textRect.Width, DefaultDragInsertPositionMargin * 2.0f); break; case DragItemPositioning.Below: - rect = new Rectangle(textRect.X, textRect.Bottom - DefaultDragInsertPositionMargin, textRect.Width, DefaultDragInsertPositionMargin * 2.0f); + rect = new Rectangle(textRect.X, textRect.Bottom + _margin.Bottom - DefaultDragInsertPositionMargin, textRect.Width, DefaultDragInsertPositionMargin * 2.0f); break; default: rect = Rectangle.Empty; @@ -922,6 +922,8 @@ namespace FlaxEditor.GUI.Tree if (result == DragDropEffect.None) { UpdateDrawPositioning(ref location); + if (ParentTree != null) + ParentTree.DraggedOverNode = this; // Check if mouse is over header _isDragOverHeader = TestHeaderHit(ref location); @@ -999,6 +1001,8 @@ namespace FlaxEditor.GUI.Tree // Clear cache _isDragOverHeader = false; _dragOverMode = DragItemPositioning.None; + if (ParentTree != null) + ParentTree.DraggedOverNode = null; return result; } diff --git a/Source/Editor/Modules/UIModule.cs b/Source/Editor/Modules/UIModule.cs index 9f8167731..858ef2b1a 100644 --- a/Source/Editor/Modules/UIModule.cs +++ b/Source/Editor/Modules/UIModule.cs @@ -526,7 +526,7 @@ namespace FlaxEditor.Modules _menuGamePlay = cm.AddButton("Play", inputOptions.Play.ToString(), Editor.Simulation.RequestStartPlay); _menuGamePause = cm.AddButton("Pause", inputOptions.Pause.ToString(), Editor.Simulation.RequestPausePlay); cm.AddSeparator(); - cm.AddButton("Cook&Run", Editor.Windows.GameCookerWin.BuildAndRun).LinkTooltip("Runs Game Cooker to build the game for this platform and runs the game after."); + cm.AddButton("Cook & Run", Editor.Windows.GameCookerWin.BuildAndRun).LinkTooltip("Runs Game Cooker to build the game for this platform and runs the game after."); cm.AddButton("Run cooked game", Editor.Windows.GameCookerWin.RunCooked).LinkTooltip("Runs the game build from the last cooking output. Use Cook&Play or Game Cooker first."); // Tools diff --git a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs index 606462ef8..768052386 100644 --- a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs +++ b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs @@ -707,6 +707,8 @@ namespace FlaxEditor.SceneGraph.GUI { DragData data; var tree = ParentTree; + if (tree.Selection.Count == 1) + Select(); // Check if this node is selected if (tree.Selection.Contains(this)) @@ -716,6 +718,11 @@ namespace FlaxEditor.SceneGraph.GUI for (var i = 0; i < tree.Selection.Count; i++) { var e = tree.Selection[i]; + + // Skip if parent is already selected to keep correct parenting + if (tree.Selection.Contains(e.Parent)) + continue; + if (e is ActorTreeNode node && node.ActorNode.CanDrag) actors.Add(node.ActorNode); } diff --git a/Source/Editor/Windows/ContentWindow.cs b/Source/Editor/Windows/ContentWindow.cs index 9535e8d3e..669d3165f 100644 --- a/Source/Editor/Windows/ContentWindow.cs +++ b/Source/Editor/Windows/ContentWindow.cs @@ -320,6 +320,7 @@ namespace FlaxEditor.Windows /// Shows popup dialog with UI to rename content item. /// /// The item to rename. + /// The created renaming popup. public void Rename(ContentItem item) { // Show element in the view @@ -337,24 +338,7 @@ namespace FlaxEditor.Windows popup.Tag = item; popup.Validate += OnRenameValidate; popup.Renamed += renamePopup => Rename((ContentItem)renamePopup.Tag, renamePopup.Text); - popup.Closed += renamePopup => - { - // Restore scrolling in content view - if (_contentViewPanel.VScrollBar != null) - _contentViewPanel.VScrollBar.ThumbEnabled = true; - if (_contentViewPanel.HScrollBar != null) - _contentViewPanel.HScrollBar.ThumbEnabled = true; - ScrollingOnContentView(true); - - // Check if was creating new element - if (_newElement != null) - { - // Destroy mock control - _newElement.ParentFolder = null; - _newElement.Dispose(); - _newElement = null; - } - }; + popup.Closed += OnRenameClosed; // For new asset we want to mock the initial value so user can press just Enter to use default name if (_newElement != null) @@ -368,6 +352,25 @@ namespace FlaxEditor.Windows return Editor.ContentEditing.IsValidAssetName((ContentItem)popup.Tag, value, out _); } + private void OnRenameClosed(RenamePopup popup) + { + // Restore scrolling in content view + if (_contentViewPanel.VScrollBar != null) + _contentViewPanel.VScrollBar.ThumbEnabled = true; + if (_contentViewPanel.HScrollBar != null) + _contentViewPanel.HScrollBar.ThumbEnabled = true; + ScrollingOnContentView(true); + + // Check if was creating new element + if (_newElement != null) + { + // Destroy mock control + _newElement.ParentFolder = null; + _newElement.Dispose(); + _newElement = null; + } + } + /// /// Renames the specified item. /// @@ -486,7 +489,7 @@ namespace FlaxEditor.Windows /// The item to delete. public void Delete(ContentItem item) { - Delete(new List(1) { item }); + Delete(Editor.Instance.Windows.ContentWin.View.Selection); } /// @@ -537,7 +540,7 @@ namespace FlaxEditor.Windows destinationName = Utilities.Utils.IncrementNameNumber(item.ShortName, x => !File.Exists(StringUtils.CombinePaths(sourceFolder, x + extension))) + extension; } - return StringUtils.CombinePaths(sourceFolder, destinationName); + return StringUtils.NormalizePath(StringUtils.CombinePaths(sourceFolder, destinationName)); } /// @@ -564,6 +567,7 @@ namespace FlaxEditor.Windows // Start renaming it if (targetItem != null) { + Select(targetItem); Rename(targetItem); } } @@ -645,7 +649,8 @@ namespace FlaxEditor.Windows /// The argument passed to the proxy for the item creation. In most cases it is null. /// The event called when the item is crated by the user. The argument is the new item. /// The initial item name. - public void NewItem(ContentProxy proxy, object argument = null, Action created = null, string initialName = null) + /// True if start initial item renaming by user, or tru to skip it. + public void NewItem(ContentProxy proxy, object argument = null, Action created = null, string initialName = null, bool withRenaming = true) { Assert.IsNull(_newElement); if (proxy == null) @@ -667,14 +672,52 @@ namespace FlaxEditor.Windows } while (parentFolder.FindChild(path) != null); } - // Create new asset proxy, add to view and rename it - _newElement = new NewItem(path, proxy, argument) + if (withRenaming) { - ParentFolder = parentFolder, - Tag = created, - }; - RefreshView(); - Rename(_newElement); + // Create new asset proxy, add to view and rename it + _newElement = new NewItem(path, proxy, argument) + { + ParentFolder = parentFolder, + Tag = created, + }; + RefreshView(); + Rename(_newElement); + } + else + { + // Create new asset + try + { + Editor.Log(string.Format("Creating asset {0} in {1}", proxy.Name, path)); + proxy.Create(path, argument); + } + catch (Exception ex) + { + Editor.LogWarning(ex); + Editor.LogError("Failed to create asset."); + return; + } + + // Focus content window + Focus(); + RootWindow?.Focus(); + + // Refresh database and view now + Editor.ContentDatabase.RefreshFolder(parentFolder, false); + RefreshView(); + var newItem = parentFolder.FindChild(path); + if (newItem == null) + { + Editor.LogWarning("Failed to find the created new item."); + return; + } + + // Auto-select item + Select(newItem, true); + + // Custom post-action + created?.Invoke(newItem); + } } private void ContentDatabaseOnItemRemoved(ContentItem contentItem) diff --git a/Source/Editor/Windows/Profiler/ProfilerWindow.cs b/Source/Editor/Windows/Profiler/ProfilerWindow.cs index a723d64f2..f5a5c6f86 100644 --- a/Source/Editor/Windows/Profiler/ProfilerWindow.cs +++ b/Source/Editor/Windows/Profiler/ProfilerWindow.cs @@ -93,6 +93,7 @@ namespace FlaxEditor.Windows.Profiler _liveRecordingButton = toolstrip.AddButton(editor.Icons.Play64); _liveRecordingButton.LinkTooltip("Live profiling events recording"); _liveRecordingButton.AutoCheck = true; + _liveRecordingButton.Clicked += () => _liveRecordingButton.Icon = LiveRecording ? editor.Icons.Stop64 : editor.Icons.Play64; _clearButton = toolstrip.AddButton(editor.Icons.Rotate32, Clear); _clearButton.LinkTooltip("Clear data"); toolstrip.AddSeparator(); diff --git a/Source/Engine/Core/Math/Double2.cs b/Source/Engine/Core/Math/Double2.cs index 91f2cba9e..02669a869 100644 --- a/Source/Engine/Core/Math/Double2.cs +++ b/Source/Engine/Core/Math/Double2.cs @@ -269,7 +269,7 @@ namespace FlaxEngine public void Normalize() { double length = Length; - if (!Mathd.IsZero(length)) + if (length >= Mathd.Epsilon) { double inv = 1.0 / length; X *= inv; diff --git a/Source/Engine/Core/Math/Double3.cs b/Source/Engine/Core/Math/Double3.cs index b6e5d213c..eb41ad400 100644 --- a/Source/Engine/Core/Math/Double3.cs +++ b/Source/Engine/Core/Math/Double3.cs @@ -346,7 +346,7 @@ namespace FlaxEngine public void Normalize() { double length = Length; - if (!Mathd.IsZero(length)) + if (length >= Mathd.Epsilon) { double inv = 1.0 / length; X *= inv; diff --git a/Source/Engine/Core/Math/Double4.cs b/Source/Engine/Core/Math/Double4.cs index 9adef5aad..68c9a005c 100644 --- a/Source/Engine/Core/Math/Double4.cs +++ b/Source/Engine/Core/Math/Double4.cs @@ -323,7 +323,7 @@ namespace FlaxEngine public void Normalize() { double length = Length; - if (!Mathd.IsZero(length)) + if (length >= Mathd.Epsilon) { double inverse = 1.0 / length; X *= inverse; diff --git a/Source/Engine/Core/Math/Float2.cs b/Source/Engine/Core/Math/Float2.cs index adff7c4d3..3ca6f51d7 100644 --- a/Source/Engine/Core/Math/Float2.cs +++ b/Source/Engine/Core/Math/Float2.cs @@ -287,7 +287,7 @@ namespace FlaxEngine public void Normalize() { float length = Length; - if (!Mathf.IsZero(length)) + if (length >= Mathf.Epsilon) { float inv = 1.0f / length; X *= inv; diff --git a/Source/Engine/Core/Math/Float3.cs b/Source/Engine/Core/Math/Float3.cs index 275325df1..1831a3b99 100644 --- a/Source/Engine/Core/Math/Float3.cs +++ b/Source/Engine/Core/Math/Float3.cs @@ -340,7 +340,7 @@ namespace FlaxEngine public void Normalize() { float length = Length; - if (!Mathf.IsZero(length)) + if (length >= Mathf.Epsilon) { float inv = 1.0f / length; X *= inv; diff --git a/Source/Engine/Core/Math/Float4.cs b/Source/Engine/Core/Math/Float4.cs index f88674d9d..ff84a07b2 100644 --- a/Source/Engine/Core/Math/Float4.cs +++ b/Source/Engine/Core/Math/Float4.cs @@ -305,7 +305,7 @@ namespace FlaxEngine public void Normalize() { float length = Length; - if (!Mathf.IsZero(length)) + if (length >= Mathf.Epsilon) { float inverse = 1.0f / length; X *= inverse; diff --git a/Source/Engine/Core/Math/Plane.cs b/Source/Engine/Core/Math/Plane.cs index 66d6a7376..1da997cab 100644 --- a/Source/Engine/Core/Math/Plane.cs +++ b/Source/Engine/Core/Math/Plane.cs @@ -2,8 +2,10 @@ #if USE_LARGE_WORLDS using Real = System.Double; +using Mathr = FlaxEngine.Mathd; #else using Real = System.Single; +using Mathr = FlaxEngine.Mathf; #endif // ----------------------------------------------------------------------------- @@ -175,7 +177,7 @@ namespace FlaxEngine public void Normalize() { Real length = Normal.Length; - if (!Mathf.IsZero(length)) + if (length >= Mathr.Epsilon) { Real rcp = 1.0f / length; Normal.X *= rcp; diff --git a/Source/Engine/Core/Math/Quaternion.cs b/Source/Engine/Core/Math/Quaternion.cs index e1f27c047..ff50a98bf 100644 --- a/Source/Engine/Core/Math/Quaternion.cs +++ b/Source/Engine/Core/Math/Quaternion.cs @@ -337,7 +337,7 @@ namespace FlaxEngine public void Normalize() { float length = Length; - if (!Mathf.IsZero(length)) + if (length >= Mathf.Epsilon) { float inverse = 1.0f / length; X *= inverse; diff --git a/Source/Engine/Core/Math/Vector2.cs b/Source/Engine/Core/Math/Vector2.cs index 50086b6e8..1120bce2f 100644 --- a/Source/Engine/Core/Math/Vector2.cs +++ b/Source/Engine/Core/Math/Vector2.cs @@ -2,8 +2,10 @@ #if USE_LARGE_WORLDS using Real = System.Double; +using Mathr = FlaxEngine.Mathd; #else using Real = System.Single; +using Mathr = FlaxEngine.Mathf; #endif // ----------------------------------------------------------------------------- @@ -204,22 +206,22 @@ namespace FlaxEngine /// /// Gets a value indicting whether this instance is normalized. /// - public bool IsNormalized => Mathf.IsOne(X * X + Y * Y); + public bool IsNormalized => Mathr.IsOne(X * X + Y * Y); /// /// Gets a value indicting whether this vector is zero /// - public bool IsZero => Mathf.IsZero(X) && Mathf.IsZero(Y); + public bool IsZero => Mathr.IsZero(X) && Mathr.IsZero(Y); /// /// Gets a minimum component value /// - public Real MinValue => Mathf.Min(X, Y); + public Real MinValue => Mathr.Min(X, Y); /// /// Gets a maximum component value /// - public Real MaxValue => Mathf.Max(X, Y); + public Real MaxValue => Mathr.Max(X, Y); /// /// Gets an arithmetic average value of all vector components. @@ -234,7 +236,7 @@ namespace FlaxEngine /// /// Gets a vector with values being absolute values of that vector. /// - public Vector2 Absolute => new Vector2(Mathf.Abs(X), Mathf.Abs(Y)); + public Vector2 Absolute => new Vector2(Mathr.Abs(X), Mathr.Abs(Y)); /// /// Gets a vector with values being opposite to values of that vector. @@ -293,8 +295,8 @@ namespace FlaxEngine /// public void Normalize() { - Real length = Length; - if (!Mathf.IsZero(length)) + Real length = (Real)Math.Sqrt(X * X + Y * Y); + if (length >= Mathr.Epsilon) { Real inv = 1.0f / length; X *= inv; @@ -905,8 +907,8 @@ namespace FlaxEngine /// Passing a value of 0 will cause to be returned; a value of 1 will cause to be returned. public static void Lerp(ref Vector2 start, ref Vector2 end, float amount, out Vector2 result) { - result.X = Mathf.Lerp(start.X, end.X, amount); - result.Y = Mathf.Lerp(start.Y, end.Y, amount); + result.X = Mathr.Lerp(start.X, end.X, amount); + result.Y = Mathr.Lerp(start.Y, end.Y, amount); } /// @@ -933,8 +935,8 @@ namespace FlaxEngine /// Passing a value of 0 will cause to be returned; a value of 1 will cause to be returned. public static void Lerp(ref Vector2 start, ref Vector2 end, ref Vector2 amount, out Vector2 result) { - result.X = Mathf.Lerp(start.X, end.X, amount.X); - result.Y = Mathf.Lerp(start.Y, end.Y, amount.Y); + result.X = Mathr.Lerp(start.X, end.X, amount.X); + result.Y = Mathr.Lerp(start.Y, end.Y, amount.Y); } /// @@ -960,7 +962,7 @@ namespace FlaxEngine /// When the method completes, contains the cubic interpolation of the two vectors. public static void SmoothStep(ref Vector2 start, ref Vector2 end, float amount, out Vector2 result) { - amount = Mathf.SmoothStep(amount); + amount = Mathr.SmoothStep(amount); Lerp(ref start, ref end, amount, out result); } @@ -1553,7 +1555,7 @@ namespace FlaxEngine [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool operator ==(Vector2 left, Vector2 right) { - return Mathf.NearEqual(left.X, right.X) && Mathf.NearEqual(left.Y, right.Y); + return Mathr.NearEqual(left.X, right.X) && Mathr.NearEqual(left.Y, right.Y); } /// @@ -1565,7 +1567,7 @@ namespace FlaxEngine [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool operator !=(Vector2 left, Vector2 right) { - return !Mathf.NearEqual(left.X, right.X) || !Mathf.NearEqual(left.Y, right.Y); + return !Mathr.NearEqual(left.X, right.X) || !Mathr.NearEqual(left.Y, right.Y); } /// @@ -1671,7 +1673,7 @@ namespace FlaxEngine [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Equals(ref Vector2 other) { - return Mathf.NearEqual(other.X, X) && Mathf.NearEqual(other.Y, Y); + return Mathr.NearEqual(other.X, X) && Mathr.NearEqual(other.Y, Y); } /// @@ -1679,7 +1681,7 @@ namespace FlaxEngine /// public static bool Equals(ref Vector2 a, ref Vector2 b) { - return Mathf.NearEqual(a.X, b.X) && Mathf.NearEqual(a.Y, b.Y); + return Mathr.NearEqual(a.X, b.X) && Mathr.NearEqual(a.Y, b.Y); } /// @@ -1690,7 +1692,7 @@ namespace FlaxEngine [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Equals(Vector2 other) { - return Mathf.NearEqual(other.X, X) && Mathf.NearEqual(other.Y, Y); + return Mathr.NearEqual(other.X, X) && Mathr.NearEqual(other.Y, Y); } /// @@ -1700,7 +1702,7 @@ namespace FlaxEngine /// true if the specified is equal to this instance; otherwise, false. public override bool Equals(object value) { - return value is Vector2 other && Mathf.NearEqual(other.X, X) && Mathf.NearEqual(other.Y, Y); + return value is Vector2 other && Mathr.NearEqual(other.X, X) && Mathr.NearEqual(other.Y, Y); } } } diff --git a/Source/Engine/Core/Math/Vector2.h b/Source/Engine/Core/Math/Vector2.h index cff32539d..87ced1de6 100644 --- a/Source/Engine/Core/Math/Vector2.h +++ b/Source/Engine/Core/Math/Vector2.h @@ -240,9 +240,9 @@ public: void Normalize() { const T length = Math::Sqrt(X * X + Y * Y); - if (!Math::IsZero(length)) + if (length >= ZeroTolerance) { - const T invLength = 1.0f / length; + const T invLength = (T)1.0f / length; X *= invLength; Y *= invLength; } @@ -547,9 +547,9 @@ public: { Vector2Base r = v; const T length = Math::Sqrt(r.X * r.X + r.Y * r.Y); - if (Math::Abs(length) >= ZeroTolerance) + if (length >= ZeroTolerance) { - const T inv = 1.0f / length; + const T inv = (T)1.0f / length; r.X *= inv; r.Y *= inv; } diff --git a/Source/Engine/Core/Math/Vector3.cs b/Source/Engine/Core/Math/Vector3.cs index 1da905681..0cfa0e248 100644 --- a/Source/Engine/Core/Math/Vector3.cs +++ b/Source/Engine/Core/Math/Vector3.cs @@ -2,8 +2,10 @@ #if USE_LARGE_WORLDS using Real = System.Double; +using Mathr = FlaxEngine.Mathd; #else using Real = System.Single; +using Mathr = FlaxEngine.Mathf; #endif // ----------------------------------------------------------------------------- @@ -253,7 +255,7 @@ namespace FlaxEngine /// /// Gets a value indicting whether this instance is normalized. /// - public bool IsNormalized => Mathf.IsOne(X * X + Y * Y + Z * Z); + public bool IsNormalized => Mathr.IsOne(X * X + Y * Y + Z * Z); /// /// Gets the normalized vector. Returned vector has length equal 1. @@ -271,22 +273,22 @@ namespace FlaxEngine /// /// Gets a value indicting whether this vector is zero /// - public bool IsZero => Mathf.IsZero(X) && Mathf.IsZero(Y) && Mathf.IsZero(Z); + public bool IsZero => Mathr.IsZero(X) && Mathr.IsZero(Y) && Mathr.IsZero(Z); /// /// Gets a value indicting whether this vector is one /// - public bool IsOne => Mathf.IsOne(X) && Mathf.IsOne(Y) && Mathf.IsOne(Z); + public bool IsOne => Mathr.IsOne(X) && Mathr.IsOne(Y) && Mathr.IsOne(Z); /// /// Gets a minimum component value /// - public Real MinValue => Mathf.Min(X, Mathf.Min(Y, Z)); + public Real MinValue => Mathr.Min(X, Mathr.Min(Y, Z)); /// /// Gets a maximum component value /// - public Real MaxValue => Mathf.Max(X, Mathf.Max(Y, Z)); + public Real MaxValue => Mathr.Max(X, Mathr.Max(Y, Z)); /// /// Gets an arithmetic average value of all vector components. @@ -301,7 +303,7 @@ namespace FlaxEngine /// /// Gets a vector with values being absolute values of that vector. /// - public Vector3 Absolute => new Vector3(Mathf.Abs(X), Mathf.Abs(Y), Mathf.Abs(Z)); + public Vector3 Absolute => new Vector3(Mathr.Abs(X), Mathr.Abs(Y), Mathr.Abs(Z)); /// /// Gets a vector with values being opposite to values of that vector. @@ -364,8 +366,8 @@ namespace FlaxEngine /// public void Normalize() { - Real length = Length; - if (!Mathf.IsZero(length)) + Real length = (Real)Math.Sqrt(X * X + Y * Y + Z * Z); + if (length >= Mathr.Epsilon) { Real inv = 1.0f / length; X *= inv; @@ -1021,9 +1023,9 @@ namespace FlaxEngine /// Passing a value of 0 will cause to be returned; a value of 1 will cause to be returned. public static void Lerp(ref Vector3 start, ref Vector3 end, float amount, out Vector3 result) { - result.X = Mathf.Lerp(start.X, end.X, amount); - result.Y = Mathf.Lerp(start.Y, end.Y, amount); - result.Z = Mathf.Lerp(start.Z, end.Z, amount); + result.X = Mathr.Lerp(start.X, end.X, amount); + result.Y = Mathr.Lerp(start.Y, end.Y, amount); + result.Z = Mathr.Lerp(start.Z, end.Z, amount); } /// @@ -1049,7 +1051,7 @@ namespace FlaxEngine /// When the method completes, contains the cubic interpolation of the two vectors. public static void SmoothStep(ref Vector3 start, ref Vector3 end, float amount, out Vector3 result) { - amount = Mathf.SmoothStep(amount); + amount = Mathr.SmoothStep(amount); Lerp(ref start, ref end, amount, out result); } @@ -1211,7 +1213,7 @@ namespace FlaxEngine public static Vector3 Project(Vector3 vector, Vector3 onNormal) { Real sqrMag = Dot(onNormal, onNormal); - if (sqrMag < Mathf.Epsilon) + if (sqrMag < Mathr.Epsilon) return Zero; return onNormal * Dot(vector, onNormal) / sqrMag; } @@ -1235,10 +1237,10 @@ namespace FlaxEngine /// The angle (in degrees). public static Real Angle(Vector3 from, Vector3 to) { - Real dot = Mathf.Clamp(Dot(from.Normalized, to.Normalized), -1.0f, 1.0f); - if (Mathf.Abs(dot) > (1.0f - Mathf.Epsilon)) + Real dot = Mathr.Clamp(Dot(from.Normalized, to.Normalized), -1.0f, 1.0f); + if (Mathr.Abs(dot) > (1.0f - Mathr.Epsilon)) return dot > 0.0f ? 0.0f : 180.0f; - return (Real)Math.Acos(dot) * Mathf.RadiansToDegrees; + return (Real)Math.Acos(dot) * Mathr.RadiansToDegrees; } /// @@ -1826,7 +1828,7 @@ namespace FlaxEngine [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool operator ==(Vector3 left, Vector3 right) { - return Mathf.NearEqual(left.X, right.X) && Mathf.NearEqual(left.Y, right.Y) && Mathf.NearEqual(left.Z, right.Z); + return Mathr.NearEqual(left.X, right.X) && Mathr.NearEqual(left.Y, right.Y) && Mathr.NearEqual(left.Z, right.Z); } /// @@ -1838,7 +1840,7 @@ namespace FlaxEngine [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool operator !=(Vector3 left, Vector3 right) { - return !Mathf.NearEqual(left.X, right.X) || !Mathf.NearEqual(left.Y, right.Y) || !Mathf.NearEqual(left.Z, right.Z); + return !Mathr.NearEqual(left.X, right.X) || !Mathr.NearEqual(left.Y, right.Y) || !Mathr.NearEqual(left.Z, right.Z); } /// @@ -1947,7 +1949,7 @@ namespace FlaxEngine [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Equals(ref Vector3 other) { - return Mathf.NearEqual(other.X, X) && Mathf.NearEqual(other.Y, Y) && Mathf.NearEqual(other.Z, Z); + return Mathr.NearEqual(other.X, X) && Mathr.NearEqual(other.Y, Y) && Mathr.NearEqual(other.Z, Z); } /// @@ -1958,7 +1960,7 @@ namespace FlaxEngine [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Equals(Vector3 other) { - return Mathf.NearEqual(other.X, X) && Mathf.NearEqual(other.Y, Y) && Mathf.NearEqual(other.Z, Z); + return Mathr.NearEqual(other.X, X) && Mathr.NearEqual(other.Y, Y) && Mathr.NearEqual(other.Z, Z); } /// @@ -1968,7 +1970,7 @@ namespace FlaxEngine /// true if the specified is equal to this instance; otherwise, false. public override bool Equals(object value) { - return value is Vector3 other && Mathf.NearEqual(other.X, X) && Mathf.NearEqual(other.Y, Y) && Mathf.NearEqual(other.Z, Z); + return value is Vector3 other && Mathr.NearEqual(other.X, X) && Mathr.NearEqual(other.Y, Y) && Mathr.NearEqual(other.Z, Z); } } } diff --git a/Source/Engine/Core/Math/Vector3.h b/Source/Engine/Core/Math/Vector3.h index f2547f47d..33be7f2d6 100644 --- a/Source/Engine/Core/Math/Vector3.h +++ b/Source/Engine/Core/Math/Vector3.h @@ -266,9 +266,9 @@ public: void Normalize() { const T length = Math::Sqrt(X * X + Y * Y + Z * Z); - if (Math::Abs(length) >= ZeroTolerance) + if (length >= ZeroTolerance) { - const T inv = 1.0f / length; + const T inv = (T)1.0f / length; X *= inv; Y *= inv; Z *= inv; @@ -566,6 +566,56 @@ public: result = Vector3Base(Math::Clamp(v.X, min.X, max.X), Math::Clamp(v.Y, min.Y, max.Y), Math::Clamp(v.Z, min.Z, max.Z)); } + /// + /// Makes sure that Length of the output vector is always below max and above 0. + /// + /// Input Vector. + /// Max Length + static Vector3Base ClampLength(const Vector3Base& v, float max) + { + return ClampLength(v, 0, max); + } + + /// + /// Makes sure that Length of the output vector is always below max and above min. + /// + /// Input Vector. + /// Min Length + /// Max Length + static Vector3Base ClampLength(const Vector3Base& v, float min, float max) + { + Vector3Base result; + ClampLength(v, min, max, result); + return result; + } + + /// + /// Makes sure that Length of the output vector is always below max and above min. + /// + /// Input Vector. + /// Min Length + /// Max Length + /// The result vector. + static void ClampLength(const Vector3Base& v, float min, float max, Vector3Base& result) + { + result = v; + T lenSq = result.LengthSquared(); + if (lenSq > max * max) + { + T scaleFactor = max / (T)Math::Sqrt(lenSq); + result.X *= scaleFactor; + result.Y *= scaleFactor; + result.Z *= scaleFactor; + } + if (lenSq < min * min) + { + T scaleFactor = min / (T)Math::Sqrt(lenSq); + result.X *= scaleFactor; + result.Y *= scaleFactor; + result.Z *= scaleFactor; + } + } + // Calculates the distance between two vectors // @param a The first vector // @param b The second vector @@ -595,9 +645,9 @@ public: { Vector3Base r = v; const T length = Math::Sqrt(r.X * r.X + r.Y * r.Y + r.Z * r.Z); - if (Math::Abs(length) >= ZeroTolerance) + if (length >= ZeroTolerance) { - const T inv = 1.0f / length; + const T inv = (T)1.0f / length; r.X *= inv; r.Y *= inv; r.Z *= inv; diff --git a/Source/Engine/Core/Math/Vector4.cs b/Source/Engine/Core/Math/Vector4.cs index cce13fc61..ceeafe88f 100644 --- a/Source/Engine/Core/Math/Vector4.cs +++ b/Source/Engine/Core/Math/Vector4.cs @@ -2,8 +2,10 @@ #if USE_LARGE_WORLDS using Real = System.Double; +using Mathr = FlaxEngine.Mathd; #else using Real = System.Single; +using Mathr = FlaxEngine.Mathf; #endif // ----------------------------------------------------------------------------- @@ -256,27 +258,27 @@ namespace FlaxEngine /// /// Gets a value indicting whether this instance is normalized. /// - public bool IsNormalized => Mathf.IsOne(X * X + Y * Y + Z * Z + W * W); + public bool IsNormalized => Mathr.IsOne(X * X + Y * Y + Z * Z + W * W); /// /// Gets a value indicting whether this vector is zero /// - public bool IsZero => Mathf.IsZero(X) && Mathf.IsZero(Y) && Mathf.IsZero(Z) && Mathf.IsZero(W); + public bool IsZero => Mathr.IsZero(X) && Mathr.IsZero(Y) && Mathr.IsZero(Z) && Mathr.IsZero(W); /// /// Gets a value indicting whether this vector is one /// - public bool IsOne => Mathf.IsOne(X) && Mathf.IsOne(Y) && Mathf.IsOne(Z) && Mathf.IsOne(W); + public bool IsOne => Mathr.IsOne(X) && Mathr.IsOne(Y) && Mathr.IsOne(Z) && Mathr.IsOne(W); /// /// Gets a minimum component value /// - public Real MinValue => Mathf.Min(X, Mathf.Min(Y, Mathf.Min(Z, W))); + public Real MinValue => Mathr.Min(X, Mathr.Min(Y, Mathr.Min(Z, W))); /// /// Gets a maximum component value /// - public Real MaxValue => Mathf.Max(X, Mathf.Max(Y, Mathf.Max(Z, W))); + public Real MaxValue => Mathr.Max(X, Mathr.Max(Y, Mathr.Max(Z, W))); /// /// Gets an arithmetic average value of all vector components. @@ -291,7 +293,7 @@ namespace FlaxEngine /// /// Gets a vector with values being absolute values of that vector. /// - public Vector4 Absolute => new Vector4(Mathf.Abs(X), Mathf.Abs(Y), Mathf.Abs(Z), Mathf.Abs(W)); + public Vector4 Absolute => new Vector4(Mathr.Abs(X), Mathr.Abs(Y), Mathr.Abs(Z), Mathr.Abs(W)); /// /// Gets a vector with values being opposite to values of that vector. @@ -358,8 +360,8 @@ namespace FlaxEngine /// public void Normalize() { - Real length = Length; - if (!Mathf.IsZero(length)) + Real length = (Real)Math.Sqrt(X * X + Y * Y + Z * Z + W * W); + if (length >= Mathr.Epsilon) { Real inverse = 1.0f / length; X *= inverse; @@ -856,10 +858,10 @@ namespace FlaxEngine /// Passing a value of 0 will cause to be returned; a value of 1 will cause to be returned. public static void Lerp(ref Vector4 start, ref Vector4 end, Real amount, out Vector4 result) { - result.X = Mathf.Lerp(start.X, end.X, amount); - result.Y = Mathf.Lerp(start.Y, end.Y, amount); - result.Z = Mathf.Lerp(start.Z, end.Z, amount); - result.W = Mathf.Lerp(start.W, end.W, amount); + result.X = Mathr.Lerp(start.X, end.X, amount); + result.Y = Mathr.Lerp(start.Y, end.Y, amount); + result.Z = Mathr.Lerp(start.Z, end.Z, amount); + result.W = Mathr.Lerp(start.W, end.W, amount); } /// @@ -885,7 +887,7 @@ namespace FlaxEngine /// When the method completes, contains the cubic interpolation of the two vectors. public static void SmoothStep(ref Vector4 start, ref Vector4 end, Real amount, out Vector4 result) { - amount = Mathf.SmoothStep(amount); + amount = Mathr.SmoothStep(amount); Lerp(ref start, ref end, amount, out result); } @@ -1360,7 +1362,7 @@ namespace FlaxEngine [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool operator ==(Vector4 left, Vector4 right) { - return Mathf.NearEqual(left.X, right.X) && Mathf.NearEqual(left.Y, right.Y) && Mathf.NearEqual(left.Z, right.Z) && Mathf.NearEqual(left.W, right.W); + return Mathr.NearEqual(left.X, right.X) && Mathr.NearEqual(left.Y, right.Y) && Mathr.NearEqual(left.Z, right.Z) && Mathr.NearEqual(left.W, right.W); } /// @@ -1481,7 +1483,7 @@ namespace FlaxEngine /// true if the specified is equal to this instance; otherwise, false. public bool Equals(ref Vector4 other) { - return Mathf.NearEqual(other.X, X) && Mathf.NearEqual(other.Y, Y) && Mathf.NearEqual(other.Z, Z) && Mathf.NearEqual(other.W, W); + return Mathr.NearEqual(other.X, X) && Mathr.NearEqual(other.Y, Y) && Mathr.NearEqual(other.Z, Z) && Mathr.NearEqual(other.W, W); } /// @@ -1492,7 +1494,7 @@ namespace FlaxEngine [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Equals(Vector4 other) { - return Mathf.NearEqual(other.X, X) && Mathf.NearEqual(other.Y, Y) && Mathf.NearEqual(other.Z, Z) && Mathf.NearEqual(other.W, W); + return Mathr.NearEqual(other.X, X) && Mathr.NearEqual(other.Y, Y) && Mathr.NearEqual(other.Z, Z) && Mathr.NearEqual(other.W, W); } /// @@ -1502,7 +1504,7 @@ namespace FlaxEngine /// true if the specified is equal to this instance; otherwise, false. public override bool Equals(object value) { - return value is Vector4 other && Mathf.NearEqual(other.X, X) && Mathf.NearEqual(other.Y, Y) && Mathf.NearEqual(other.Z, Z) && Mathf.NearEqual(other.W, W); + return value is Vector4 other && Mathr.NearEqual(other.X, X) && Mathr.NearEqual(other.Y, Y) && Mathr.NearEqual(other.Z, Z) && Mathr.NearEqual(other.W, W); } } } diff --git a/Source/Engine/Core/Types/Span.h b/Source/Engine/Core/Types/Span.h index d0cbd7993..678550e85 100644 --- a/Source/Engine/Core/Types/Span.h +++ b/Source/Engine/Core/Types/Span.h @@ -8,7 +8,7 @@ /// Universal representation of a contiguous region of arbitrary memory. /// template -class Span +API_CLASS(InBuild) class Span { protected: T* _data; diff --git a/Source/Engine/Networking/Components/NetworkTransform.cpp b/Source/Engine/Networking/Components/NetworkTransform.cpp index 58c6481f6..2d2334c46 100644 --- a/Source/Engine/Networking/Components/NetworkTransform.cpp +++ b/Source/Engine/Networking/Components/NetworkTransform.cpp @@ -156,7 +156,6 @@ void NetworkTransform::Serialize(NetworkStream* stream) transform = Transform::Identity; // Encode data - const NetworkObjectRole role = NetworkReplicator::GetObjectRole(this); Data data; data.LocalSpace = LocalSpace; data.HasSequenceIndex = Mode == ReplicationModes::Prediction; diff --git a/Source/Engine/Networking/NetworkManager.cpp b/Source/Engine/Networking/NetworkManager.cpp index c2b6fd2da..88a104c3e 100644 --- a/Source/Engine/Networking/NetworkManager.cpp +++ b/Source/Engine/Networking/NetworkManager.cpp @@ -236,6 +236,16 @@ NetworkClient* NetworkManager::GetClient(const NetworkConnection& connection) return nullptr; } +NetworkClient* NetworkManager::GetClient(uint32 clientId) +{ + for (NetworkClient* client : Clients) + { + if (client->ClientId == clientId) + return client; + } + return nullptr; +} + bool NetworkManager::StartServer() { PROFILE_CPU(); diff --git a/Source/Engine/Networking/NetworkManager.h b/Source/Engine/Networking/NetworkManager.h index db4deac12..c18a58d96 100644 --- a/Source/Engine/Networking/NetworkManager.h +++ b/Source/Engine/Networking/NetworkManager.h @@ -153,6 +153,13 @@ public: /// Found client or null. API_FUNCTION() static NetworkClient* GetClient(API_PARAM(Ref) const NetworkConnection& connection); + /// + /// Gets the network client with a given identifier. Returns null if failed to find it. + /// + /// Network client identifier (synchronized on all peers). + /// Found client or null. + API_FUNCTION() static NetworkClient* GetClient(uint32 clientId); + public: /// /// Starts the network in server mode. Returns true if failed (eg. invalid config). diff --git a/Source/Engine/Networking/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp index fba3ff025..7ce2022a4 100644 --- a/Source/Engine/Networking/NetworkReplicator.cpp +++ b/Source/Engine/Networking/NetworkReplicator.cpp @@ -29,12 +29,12 @@ #include "Engine/Threading/Threading.h" #include "Engine/Threading/ThreadLocal.h" -// Enables verbose logging for Network Replicator actions (dev-only) -#define NETWORK_REPLICATOR_DEBUG_LOG 0 - -#if NETWORK_REPLICATOR_DEBUG_LOG +#if !BUILD_RELEASE +bool NetworkReplicator::EnableLog = false; #include "Engine/Core/Log.h" -#define NETWORK_REPLICATOR_LOG(messageType, format, ...) LOG(messageType, format, ##__VA_ARGS__) +#include "Engine/Content/Content.h" +#define NETWORK_REPLICATOR_LOG(messageType, format, ...) if (NetworkReplicator::EnableLog) { LOG(messageType, format, ##__VA_ARGS__); } +#define USE_NETWORK_REPLICATOR_LOG 1 #else #define NETWORK_REPLICATOR_LOG(messageType, format, ...) #endif @@ -107,10 +107,15 @@ struct NetworkReplicatedObject uint32 OwnerClientId; uint32 LastOwnerFrame = 0; NetworkObjectRole Role; - uint8 Spawned = false; + uint8 Spawned : 1; DataContainer TargetClientIds; INetworkObject* AsNetworkObject; + NetworkReplicatedObject() + { + Spawned = 0; + } + bool operator==(const NetworkReplicatedObject& other) const { return Object == other.Object; @@ -149,6 +154,7 @@ struct ReplicateItem Guid ObjectId; uint16 PartsLeft; uint32 OwnerFrame; + uint32 OwnerClientId; Array Data; }; @@ -179,6 +185,7 @@ struct RpcItem NetworkRpcName Name; NetworkRpcInfo Info; BytesContainer ArgsData; + DataContainer Targets; }; namespace @@ -330,6 +337,46 @@ void BuildCachedTargets(const Array& clients, const DataContaine } } +void BuildCachedTargets(const Array& clients, const DataContainer& clientIds1, const Span& clientIds2, const uint32 excludedClientId = NetworkManager::ServerClientId) +{ + CachedTargets.Clear(); + if (clientIds1.IsValid()) + { + if (clientIds2.IsValid()) + { + for (const NetworkClient* client : clients) + { + if (client->State == NetworkConnectionState::Connected && client->ClientId != excludedClientId) + { + for (int32 i = 0; i < clientIds1.Length(); i++) + { + if (clientIds1[i] == client->ClientId) + { + for (int32 j = 0; j < clientIds2.Length(); j++) + { + if (clientIds2[j] == client->ClientId) + { + CachedTargets.Add(client->Connection); + break; + } + } + break; + } + } + } + } + } + else + { + BuildCachedTargets(clients, clientIds1, excludedClientId); + } + } + else + { + BuildCachedTargets(clients, clientIds2, excludedClientId); + } +} + FORCE_INLINE void BuildCachedTargets(const NetworkReplicatedObject& item) { // By default send object to all connected clients excluding the owner but with optional TargetClientIds list @@ -484,13 +531,43 @@ void SetupObjectSpawnGroupItem(ScriptingObject* obj, ArrayItems.Add(&spawnItem); } +void FindObjectsForSpawn(SpawnGroup& group, ChunkedArray& spawnItems, ScriptingObject* obj) +{ + // Add any registered network objects + auto it = Objects.Find(obj->GetID()); + if (it != Objects.End()) + { + auto& item = it->Item; + if (!item.Spawned) + { + // One of the parents of this object is being spawned so spawn it too + item.Spawned = true; + auto& spawnItem = spawnItems.AddOne(); + spawnItem.Object = obj; + spawnItem.Targets.Link(item.TargetClientIds); + spawnItem.OwnerClientId = item.OwnerClientId; + spawnItem.Role = item.Role; + group.Items.Add(&spawnItem); + } + } + + // Iterate over children + if (auto* actor = ScriptingObject::Cast(obj)) + { + for (auto* script : actor->Scripts) + FindObjectsForSpawn(group, spawnItems, script); + for (auto* child : actor->Children) + FindObjectsForSpawn(group, spawnItems, child); + } +} + void DirtyObjectImpl(NetworkReplicatedObject& item, ScriptingObject* obj) { // TODO: implement objects state replication frequency and dirtying } template -ReplicateItem* AddObjectReplicateItem(NetworkEvent& event, const MessageType& msgData, uint16 partStart, uint16 partSize) +ReplicateItem* AddObjectReplicateItem(NetworkEvent& event, const MessageType& msgData, uint16 partStart, uint16 partSize, uint32 senderClientId) { // Reuse or add part item ReplicateItem* replicateItem = nullptr; @@ -510,6 +587,7 @@ ReplicateItem* AddObjectReplicateItem(NetworkEvent& event, const MessageType& ms replicateItem->ObjectId = msgData.ObjectId; replicateItem->PartsLeft = msgData.PartsCount; replicateItem->OwnerFrame = msgData.OwnerFrame; + replicateItem->OwnerClientId = senderClientId; replicateItem->Data.Resize(msgData.DataSize); } @@ -523,7 +601,7 @@ ReplicateItem* AddObjectReplicateItem(NetworkEvent& event, const MessageType& ms return replicateItem; } -void InvokeObjectReplication(NetworkReplicatedObject& item, uint32 ownerFrame, byte* data, uint32 dataSize) +void InvokeObjectReplication(NetworkReplicatedObject& item, uint32 ownerFrame, byte* data, uint32 dataSize, uint32 senderClientId) { ScriptingObject* obj = item.Object.Get(); if (!obj) @@ -543,6 +621,7 @@ void InvokeObjectReplication(NetworkReplicatedObject& item, uint32 ownerFrame, b CachedReadStream = New(); NetworkStream* stream = CachedReadStream; stream->Initialize(data, dataSize); + stream->SenderId = senderClientId; // Deserialize object const bool failed = NetworkReplicator::InvokeSerializer(obj->GetTypeHandle(), obj, stream, false); @@ -559,6 +638,11 @@ void InvokeObjectReplication(NetworkReplicatedObject& item, uint32 ownerFrame, b DirtyObjectImpl(item, obj); } +NetworkRpcParams::NetworkRpcParams(const NetworkStream* stream) + : SenderId(stream->SenderId) +{ +} + #if !COMPILE_WITHOUT_CSHARP #include "Engine/Scripting/ManagedCLR/MUtils.h" @@ -600,9 +684,9 @@ void NetworkReplicator::AddRPC(const ScriptingTypeHandle& typeHandle, const Stri NetworkRpcInfo::RPCsTable[rpcName] = rpcInfo; } -void NetworkReplicator::CSharpEndInvokeRPC(ScriptingObject* obj, const ScriptingTypeHandle& type, const StringAnsiView& name, NetworkStream* argsStream) +void NetworkReplicator::CSharpEndInvokeRPC(ScriptingObject* obj, const ScriptingTypeHandle& type, const StringAnsiView& name, NetworkStream* argsStream, MonoArray* targetIds) { - EndInvokeRPC(obj, type, GetCSharpCachedName(name), argsStream); + EndInvokeRPC(obj, type, GetCSharpCachedName(name), argsStream, MUtils::ToSpan(targetIds)); } StringAnsiView NetworkReplicator::GetCSharpCachedName(const StringAnsiView& name) @@ -881,10 +965,11 @@ NetworkStream* NetworkReplicator::BeginInvokeRPC() if (CachedWriteStream == nullptr) CachedWriteStream = New(); CachedWriteStream->Initialize(); + CachedWriteStream->SenderId = NetworkManager::LocalClientId; return CachedWriteStream; } -void NetworkReplicator::EndInvokeRPC(ScriptingObject* obj, const ScriptingTypeHandle& type, const StringAnsiView& name, NetworkStream* argsStream) +void NetworkReplicator::EndInvokeRPC(ScriptingObject* obj, const ScriptingTypeHandle& type, const StringAnsiView& name, NetworkStream* argsStream, Span targetIds) { const NetworkRpcInfo* info = NetworkRpcInfo::RPCsTable.TryGet(NetworkRpcName(type, name)); if (!info || !obj || NetworkManager::IsOffline()) @@ -895,8 +980,8 @@ void NetworkReplicator::EndInvokeRPC(ScriptingObject* obj, const ScriptingTypeHa rpc.Name.First = type; rpc.Name.Second = name; rpc.Info = *info; - const Span argsData(argsStream->GetBuffer(), argsStream->GetPosition()); - rpc.ArgsData.Copy(argsData); + rpc.ArgsData.Copy(Span(argsStream->GetBuffer(), argsStream->GetPosition())); + rpc.Targets.Copy(targetIds); #if USE_EDITOR || !BUILD_RELEASE auto it = Objects.Find(obj->GetID()); if (it == Objects.End()) @@ -983,12 +1068,9 @@ void NetworkInternal::NetworkReplicatorUpdate() ScopeLock lock(ObjectsLock); if (Objects.Count() == 0) return; - if (CachedWriteStream == nullptr) - CachedWriteStream = New(); const bool isClient = NetworkManager::IsClient(); const bool isServer = NetworkManager::IsServer(); const bool isHost = NetworkManager::IsHost(); - NetworkStream* stream = CachedWriteStream; NetworkPeer* peer = NetworkManager::Peer; if (!isClient && NewClients.Count() != 0) @@ -1114,9 +1196,16 @@ void NetworkInternal::NetworkReplicatorUpdate() } // Spawn groups of objects + ChunkedArray spawnItems; for (SpawnGroup& g : spawnGroups) { + // Include any added objects within spawn group that were not spawned manually (eg. AddObject for script/actor attached to spawned actor) + ScriptingObject* groupRoot = g.Items[0]->Object.Get(); + FindObjectsForSpawn(g, spawnItems, groupRoot); + SendObjectSpawnMessage(g, NetworkManager::Clients); + + spawnItems.Clear(); } SpawnQueue.Clear(); } @@ -1139,7 +1228,7 @@ void NetworkInternal::NetworkReplicatorUpdate() auto& item = it->Item; // Replicate from all collected parts data - InvokeObjectReplication(item, e.OwnerFrame, e.Data.Get(), e.Data.Count()); + InvokeObjectReplication(item, e.OwnerFrame, e.Data.Get(), e.Data.Count(), e.OwnerClientId); } } @@ -1147,6 +1236,10 @@ 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) @@ -1276,12 +1369,16 @@ void NetworkInternal::NetworkReplicatorUpdate() 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()); +#endif peer->EndSendMessage(channel, msg); } else if (e.Info.Client && (isServer || isHost)) { // Server -> Client(s) - BuildCachedTargets(NetworkManager::Clients, item.TargetClientIds, NetworkManager::LocalClientId); + BuildCachedTargets(NetworkManager::Clients, item.TargetClientIds, e.Targets, NetworkManager::LocalClientId); peer->EndSendMessage(channel, msg, CachedTargets); } } @@ -1307,16 +1404,17 @@ void NetworkInternal::OnNetworkMessageObjectReplicate(NetworkEvent& event, Netwo if (client && item.OwnerClientId != client->ClientId) return; + const uint32 senderClientId = client ? client->ClientId : NetworkManager::ServerClientId; if (msgData.PartsCount == 1) { // Replicate - InvokeObjectReplication(item, msgData.OwnerFrame, event.Message.Buffer + event.Message.Position, msgData.DataSize); + InvokeObjectReplication(item, msgData.OwnerFrame, event.Message.Buffer + event.Message.Position, msgData.DataSize, senderClientId); } else { // Add to replication from multiple parts const uint16 msgMaxData = peer->Config.MessageSize - sizeof(NetworkMessageObjectReplicate); - ReplicateItem* replicateItem = AddObjectReplicateItem(event, msgData, 0, msgMaxData); + ReplicateItem* replicateItem = AddObjectReplicateItem(event, msgData, 0, msgMaxData, senderClientId); replicateItem->Object = e->Object; } } @@ -1329,7 +1427,8 @@ void NetworkInternal::OnNetworkMessageObjectReplicatePart(NetworkEvent& event, N if (DespawnedObjects.Contains(msgData.ObjectId)) return; // Skip replicating not-existing objects - AddObjectReplicateItem(event, msgData, msgData.PartStart, msgData.PartSize); + const uint32 senderClientId = client ? client->ClientId : NetworkManager::ServerClientId; + AddObjectReplicateItem(event, msgData, msgData.PartStart, msgData.PartSize, senderClientId); } void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer) @@ -1465,10 +1564,6 @@ void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkCl if (!obj->IsRegistered()) obj->RegisterObject(); const NetworkReplicatedObject* parent = ResolveObject(msgDataItem.ParentId); - if (!parent && msgDataItem.ParentId.IsValid()) - { - NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find object {} as parent to spawned object", msgDataItem.ParentId.ToString()); - } // Add object to the list NetworkReplicatedObject item; @@ -1499,6 +1594,21 @@ void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkCl sceneObject->SetParent(parent->Object.As()); else if (auto* parentActor = Scripting::TryFindObject(msgDataItem.ParentId)) sceneObject->SetParent(parentActor); + else if (msgDataItem.ParentId.IsValid()) + { +#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")) + { + NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find object {} as parent to spawned object", msgDataItem.ParentId.ToString()); + } +#endif + } + } + else if (!parent && msgDataItem.ParentId.IsValid()) + { + NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find object {} as parent to spawned object", msgDataItem.ParentId.ToString()); } if (item.AsNetworkObject) @@ -1602,7 +1712,7 @@ void NetworkInternal::OnNetworkMessageObjectRpc(NetworkEvent& event, NetworkClie const NetworkRpcInfo* info = NetworkRpcInfo::RPCsTable.TryGet(name); if (!info) { - NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Unknown object {} RPC {}::{}", msgData.ObjectId, String(msgData.RpcTypeName), String(msgData.RpcName)); + NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Unknown RPC {}::{} for object {}", String(msgData.RpcTypeName), String(msgData.RpcName), msgData.ObjectId); return; } @@ -1622,6 +1732,7 @@ void NetworkInternal::OnNetworkMessageObjectRpc(NetworkEvent& event, NetworkClie if (CachedReadStream == nullptr) CachedReadStream = New(); NetworkStream* stream = CachedReadStream; + stream->SenderId = client ? client->ClientId : NetworkManager::ServerClientId; stream->Initialize(event.Message.Buffer + event.Message.Position, msgData.ArgsSize); // Execute RPC diff --git a/Source/Engine/Networking/NetworkReplicator.cs b/Source/Engine/Networking/NetworkReplicator.cs index 54e0e893e..1ca251f75 100644 --- a/Source/Engine/Networking/NetworkReplicator.cs +++ b/Source/Engine/Networking/NetworkReplicator.cs @@ -119,10 +119,11 @@ namespace FlaxEngine.Networking /// The RPC type. /// The RPC name. /// The RPC serialized arguments stream returned from BeginInvokeRPC. + /// Optional list with network client IDs that should receive RPC. Empty to send on all clients. Ignored by Server RPCs. [Unmanaged] - public static void EndInvokeRPC(Object obj, Type type, string name, NetworkStream argsStream) + public static void EndInvokeRPC(Object obj, Type type, string name, NetworkStream argsStream, uint[] targetIds = null) { - Internal_CSharpEndInvokeRPC(FlaxEngine.Object.GetUnmanagedPtr(obj), type, name, FlaxEngine.Object.GetUnmanagedPtr(argsStream)); + Internal_CSharpEndInvokeRPC(FlaxEngine.Object.GetUnmanagedPtr(obj), type, name, FlaxEngine.Object.GetUnmanagedPtr(argsStream), targetIds); } /// diff --git a/Source/Engine/Networking/NetworkReplicator.h b/Source/Engine/Networking/NetworkReplicator.h index 0957094d0..e0893cc64 100644 --- a/Source/Engine/Networking/NetworkReplicator.h +++ b/Source/Engine/Networking/NetworkReplicator.h @@ -3,6 +3,7 @@ #pragma once #include "Types.h" +#include "Engine/Core/Types/Span.h" #include "Engine/Scripting/ScriptingObject.h" #include "Engine/Scripting/ScriptingType.h" @@ -34,6 +35,13 @@ API_CLASS(static, Namespace = "FlaxEngine.Networking") class FLAXENGINE_API Netw typedef void (*SerializeFunc)(void* instance, NetworkStream* stream, void* tag); public: +#if !BUILD_RELEASE + /// + /// Enables verbose logging of the networking runtime. Can be used to debug problems of missing RPC invoke or object replication issues. + /// + API_FIELD() static bool EnableLog; +#endif + /// /// Adds the network replication serializer for a given type. /// @@ -168,13 +176,14 @@ public: /// The RPC type. /// The RPC name. /// The RPC serialized arguments stream returned from BeginInvokeRPC. - static void EndInvokeRPC(ScriptingObject* obj, const ScriptingTypeHandle& type, const StringAnsiView& name, NetworkStream* argsStream); + /// Optional list with network client IDs that should receive RPC. Empty to send on all clients. Ignored by Server RPCs. + static void EndInvokeRPC(ScriptingObject* obj, const ScriptingTypeHandle& type, const StringAnsiView& name, NetworkStream* argsStream, Span targetIds = Span()); private: #if !COMPILE_WITHOUT_CSHARP API_FUNCTION(NoProxy) static void AddSerializer(const ScriptingTypeHandle& typeHandle, const Function& serialize, const Function& deserialize); API_FUNCTION(NoProxy) static void AddRPC(const ScriptingTypeHandle& typeHandle, const StringAnsiView& name, const Function& execute, bool isServer, bool isClient, NetworkChannelType channel); - API_FUNCTION(NoProxy) static void CSharpEndInvokeRPC(ScriptingObject* obj, const ScriptingTypeHandle& type, const StringAnsiView& name, NetworkStream* argsStream); + API_FUNCTION(NoProxy) static void CSharpEndInvokeRPC(ScriptingObject* obj, const ScriptingTypeHandle& type, const StringAnsiView& name, NetworkStream* argsStream, MonoArray* targetIds); static StringAnsiView GetCSharpCachedName(const StringAnsiView& name); #endif }; diff --git a/Source/Engine/Networking/NetworkRpc.h b/Source/Engine/Networking/NetworkRpc.h index dc485c3bb..c0c8a558e 100644 --- a/Source/Engine/Networking/NetworkRpc.h +++ b/Source/Engine/Networking/NetworkRpc.h @@ -5,6 +5,7 @@ #include "Engine/Core/Log.h" #include "Engine/Core/Types/StringView.h" #include "Engine/Core/Types/Pair.h" +#include "Engine/Core/Types/Span.h" #include "Engine/Core/Collections/Array.h" #include "Engine/Core/Collections/Dictionary.h" #include "Engine/Scripting/ScriptingType.h" @@ -12,6 +13,24 @@ class NetworkStream; +// Additional context parameters for Network RPC execution (eg. to identify who sends the data). +API_STRUCT(NoDefault, Namespace="FlaxEngine.Networking") struct FLAXENGINE_API NetworkRpcParams +{ + DECLARE_SCRIPTING_TYPE_MINIMAL(NetworkRpcParams); + NetworkRpcParams() = default; + NetworkRpcParams(const NetworkStream* stream); + + /// + /// The ClientId of the network client that is a data sender. Can be used to detect who send the incoming RPC or replication data. Ignored when sending data. + /// + API_FIELD() uint32 SenderId = 0; + + /// + /// The list of ClientId of the network clients that should receive RPC. Can be used to send RPC to a specific client(s). Ignored when receiving data. + /// + API_FIELD() Span TargetIds; +}; + // Network RPC identifier name (pair of type and function name) typedef Pair NetworkRpcName; diff --git a/Source/Engine/Networking/NetworkStream.h b/Source/Engine/Networking/NetworkStream.h index 190bd0dcb..fa91d4912 100644 --- a/Source/Engine/Networking/NetworkStream.h +++ b/Source/Engine/Networking/NetworkStream.h @@ -23,6 +23,11 @@ private: public: ~NetworkStream(); + /// + /// The ClientId of the network client that is a data sender. Can be used to detect who send the incoming RPC or replication data. Set to the current client when writing data. + /// + API_FIELD(ReadOnly) uint32 SenderId = 0; + /// /// Gets the pointer to the native stream memory buffer. /// diff --git a/Source/Engine/Networking/Types.h b/Source/Engine/Networking/Types.h index 5dd9fc140..11675023a 100644 --- a/Source/Engine/Networking/Types.h +++ b/Source/Engine/Networking/Types.h @@ -17,3 +17,4 @@ struct NetworkConnection; struct NetworkMessage; struct NetworkConfig; struct NetworkDriverStats; +struct NetworkRpcParams; diff --git a/Source/Engine/Render2D/Font.cpp b/Source/Engine/Render2D/Font.cpp index 0cbc30701..5881c2322 100644 --- a/Source/Engine/Render2D/Font.cpp +++ b/Source/Engine/Render2D/Font.cpp @@ -122,6 +122,12 @@ void Font::ProcessText(const StringView& text, Array& outputLines float lastWhitespaceX = 0; bool lastMoveLine = false; + int32 lastUpperIndex = INVALID_INDEX; + float lastUpperX = 0; + + int32 lastUnderscoreIndex = INVALID_INDEX; + float lastUnderscoreX = 0; + // Process each character to split text into single lines for (int32 currentIndex = 0; currentIndex < textLength;) { @@ -141,6 +147,22 @@ void Font::ProcessText(const StringView& text, Array& outputLines lastWhitespaceX = cursorX; } + // Check if character is an upper case letter + const bool isUpper = StringUtils::IsUpper(currentChar); + if (isUpper && currentIndex != 0) + { + lastUpperIndex = currentIndex; + lastUpperX = cursorX; + } + + // Check if character is an underscore + const bool isUnderscore = currentChar == '_'; + if (isUnderscore) + { + lastUnderscoreIndex = currentIndex; + lastUnderscoreX = cursorX; + } + // Check if it's a newline character if (currentChar == '\n') { @@ -185,6 +207,20 @@ void Font::ProcessText(const StringView& text, Array& outputLines currentIndex = lastWhitespaceIndex + 1; nextCharIndex = currentIndex; } + else if (lastUpperIndex != INVALID_INDEX) + { + cursorX = lastUpperX; + tmpLine.LastCharIndex = lastUpperIndex - 1; + currentIndex = lastUpperIndex; + nextCharIndex = currentIndex; + } + else if (lastUnderscoreIndex != INVALID_INDEX) + { + cursorX = lastUnderscoreX; + tmpLine.LastCharIndex = lastUnderscoreIndex; + currentIndex = lastUnderscoreIndex + 1; + nextCharIndex = currentIndex; + } else { nextCharIndex = currentIndex; @@ -224,6 +260,12 @@ void Font::ProcessText(const StringView& text, Array& outputLines lastWhitespaceIndex = INVALID_INDEX; lastWhitespaceX = 0; + lastUpperIndex = INVALID_INDEX; + lastUpperX = 0; + + lastUnderscoreIndex = INVALID_INDEX; + lastUnderscoreX = 0; + previous.IsValid = false; } diff --git a/Source/Engine/Scripting/ManagedCLR/MUtils.h b/Source/Engine/Scripting/ManagedCLR/MUtils.h index fa59973ab..8e7b65a60 100644 --- a/Source/Engine/Scripting/ManagedCLR/MUtils.h +++ b/Source/Engine/Scripting/ManagedCLR/MUtils.h @@ -448,8 +448,8 @@ namespace MUtils /// /// The native array object. /// The output array pointer and size. - template - FORCE_INLINE Span ToSpan(const Array& data) + template + FORCE_INLINE Span ToSpan(const Array& data) { return Span(data.Get(), data.Count()); } diff --git a/Source/Engine/UI/GUI/Control.cs b/Source/Engine/UI/GUI/Control.cs index 8b2401428..427c1aa8f 100644 --- a/Source/Engine/UI/GUI/Control.cs +++ b/Source/Engine/UI/GUI/Control.cs @@ -987,7 +987,7 @@ namespace FlaxEngine.GUI { // Hide tooltip Tooltip?.Hide(); - Root.DoDragDrop(data); + Root?.DoDragDrop(data); } #endregion diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs index a3718ae81..c9e91b3a8 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs @@ -175,6 +175,10 @@ namespace Flax.Build.Bindings CppVariantFromTypes[wrapperName] = typeInfo; return $"VariantFrom{wrapperName}Dictionary({value})"; } + if (typeInfo.Type == "Span" && typeInfo.GenericArgs != null) + { + return "Variant()"; // TODO: Span to Variant converting (use utility method the same way as for arrays) + } var apiType = FindApiTypeInfo(buildData, typeInfo, caller); if (apiType != null) @@ -221,6 +225,10 @@ namespace Flax.Build.Bindings CppVariantToTypes.Add(typeInfo); return $"MoveTemp(VariantTo{GenerateCppWrapperNativeToVariantMethodName(typeInfo)}({value}))"; } + if (typeInfo.Type == "Span" && typeInfo.GenericArgs != null) + { + return $"{typeInfo}()"; // Cannot be implemented since Variant stores array of Variants thus cannot get linear span of data + } var apiType = FindApiTypeInfo(buildData, typeInfo, caller); if (apiType != null) @@ -487,7 +495,7 @@ namespace Flax.Build.Bindings return "{0}.GetManagedInstance()"; } - // Array or Span or DataContainer + // Array or DataContainer if ((typeInfo.Type == "Array" || typeInfo.Type == "Span" || typeInfo.Type == "DataContainer") && typeInfo.GenericArgs != null) { #if USE_NETCORE @@ -502,6 +510,13 @@ namespace Flax.Build.Bindings return "MUtils::ToArray({0}, " + GenerateCppGetMClass(buildData, typeInfo.GenericArgs[0], caller, functionInfo) + ")"; } + // Span + if (typeInfo.Type == "Span" && typeInfo.GenericArgs != null) + { + type = "MonoArray*"; + return "MUtils::Span({0}, " + GenerateCppGetNativeClass(buildData, typeInfo.GenericArgs[0], caller, functionInfo) + ")"; + } + // BytesContainer if (typeInfo.Type == "BytesContainer" && typeInfo.GenericArgs == null) { diff --git a/Source/Tools/Flax.Build/Build/Plugins/NetworkingPlugin.cs b/Source/Tools/Flax.Build/Build/Plugins/NetworkingPlugin.cs index 9dd19c3b9..33d5f6c08 100644 --- a/Source/Tools/Flax.Build/Build/Plugins/NetworkingPlugin.cs +++ b/Source/Tools/Flax.Build/Build/Plugins/NetworkingPlugin.cs @@ -231,9 +231,16 @@ namespace Flax.Build.Plugins var arg = functionInfo.Parameters[i]; if (i != 0) argNames += ", "; - argNames += arg.Name; + + // Special handling of Rpc Params + if (!arg.Type.IsPtr && arg.Type.Type == "NetworkRpcParams") + { + argNames += "NetworkRpcParams(stream)"; + continue; + } // Deserialize arguments + argNames += arg.Name; contents.AppendLine($" {arg.Type.Type} {arg.Name};"); contents.AppendLine($" stream->Read({arg.Name});"); } @@ -250,16 +257,24 @@ namespace Flax.Build.Plugins contents.Append(" static void ").Append(functionInfo.Name).AppendLine("_Invoke(ScriptingObject* obj, void** args)"); contents.AppendLine(" {"); contents.AppendLine(" NetworkStream* stream = NetworkReplicator::BeginInvokeRPC();"); + contents.AppendLine(" Span targetIds;"); for (int i = 0; i < functionInfo.Parameters.Count; i++) { var arg = functionInfo.Parameters[i]; + // Special handling of Rpc Params + if (!arg.Type.IsPtr && arg.Type.Type == "NetworkRpcParams") + { + contents.AppendLine($" targetIds = ((NetworkRpcParams*)args[{i}])->TargetIds;"); + continue; + } + // Serialize arguments contents.AppendLine($" stream->Write(*({arg.Type.Type}*)args[{i}]);"); } // Invoke RPC - contents.AppendLine($" NetworkReplicator::EndInvokeRPC(obj, {typeInfo.NativeName}::TypeInitializer, StringAnsiView(\"{functionInfo.Name}\", {functionInfo.Name.Length}), stream);"); + contents.AppendLine($" NetworkReplicator::EndInvokeRPC(obj, {typeInfo.NativeName}::TypeInitializer, StringAnsiView(\"{functionInfo.Name}\", {functionInfo.Name.Length}), stream, targetIds);"); contents.AppendLine(" }"); } contents.AppendLine(); @@ -332,14 +347,14 @@ namespace Flax.Build.Plugins contents.AppendLine(); } - private bool IsRawPOD(Builder.BuildData buildData, ApiTypeInfo type) + private static bool IsRawPOD(Builder.BuildData buildData, ApiTypeInfo type) { // TODO: what if type fields have custom replication settings (eg. compression)? type.EnsureInited(buildData); return type.IsPod; } - private bool IsRawPOD(Builder.BuildData buildData, ApiTypeInfo caller, ApiTypeInfo apiType, TypeInfo type) + private static bool IsRawPOD(Builder.BuildData buildData, ApiTypeInfo caller, ApiTypeInfo apiType, TypeInfo type) { if (type.IsPod(buildData, caller)) { @@ -350,6 +365,12 @@ namespace Flax.Build.Plugins return false; } + private static bool IsRawPOD(TypeReference type) + { + // TODO: + return type.IsValueType; + } + private void OnGenerateCppTypeSerializeData(Builder.BuildData buildData, ApiTypeInfo caller, StringBuilder contents, TypeInfo type, string name, bool serialize) { var apiType = BindingsGenerator.FindApiTypeInfo(buildData, type, caller); @@ -557,10 +578,9 @@ namespace Flax.Build.Plugins if (context.AddSerializers.Count != 0 || context.MethodRPCs.Count != 0) { // Create class - var name = "Initializer"; - var idx = 0; - while (module.Types.Any(x => x.Name == name)) - name = "Initializer" + idx++; + var name = "NetworkingPlugin"; + if (module.Types.Any(x => x.Name == name)) + throw new Exception($"Failed to generate network replication for assembly '{Path.GetFileName(assemblyPath)}' that already has net code generated. Rebuild project."); var c = new TypeDefinition("", name, TypeAttributes.Class | TypeAttributes.AutoLayout | TypeAttributes.AnsiClass | TypeAttributes.Abstract | TypeAttributes.Sealed | TypeAttributes.BeforeFieldInit); module.GetType("System.Object", out var objectType); c.BaseType = module.ImportReference(objectType); @@ -774,7 +794,7 @@ namespace Flax.Build.Plugins { if (!f.HasAttribute(NetworkReplicatedAttribute)) continue; - GenerateSerializerType(ref context, type, serialize, f, null, f.FieldType, il); + GenerateSerializerType(ref context, type, serialize, f.FieldType, il, new DotnetValueContext(f)); } // Serialize all type properties marked with NetworkReplicated attribute @@ -782,7 +802,7 @@ namespace Flax.Build.Plugins { if (!p.HasAttribute(NetworkReplicatedAttribute)) continue; - GenerateSerializerType(ref context, type, serialize, null, p, p.PropertyType, il); + GenerateSerializerType(ref context, type, serialize, p.PropertyType, il, new DotnetValueContext(p)); } if (serialize) @@ -862,53 +882,384 @@ namespace Flax.Build.Plugins } } - private static void GenerateSerializerType(ref DotnetContext context, TypeDefinition type, bool serialize, FieldReference field, PropertyDefinition property, TypeReference valueType, ILProcessor il) + private struct DotnetValueContext { - if (field == null && property == null) - throw new ArgumentException(); - TypeDefinition networkStreamType = context.NetworkStreamType.Resolve(); - var propertyGetOpCode = OpCodes.Call; - var propertySetOpCode = OpCodes.Call; - if (property != null) + public FieldReference Field; + public PropertyDefinition Property; + public int LocalVarIndex; + + public OpCode PropertyGetOpCode { - if (property.GetMethod == null) + get { - MonoCecil.CompilationError($"Missing getter method for property '{property.Name}' of type {valueType.FullName} in {type.FullName} for automatic replication.", property); + var propertyGetOpCode = OpCodes.Call; + if (Property != null && Property.GetMethod.IsVirtual) + propertyGetOpCode = OpCodes.Callvirt; + return propertyGetOpCode; + } + } + + public OpCode PropertySetOpCode + { + get + { + var propertyGetOpCode = OpCodes.Call; + if (Property != null && Property.GetMethod.IsVirtual) + propertyGetOpCode = OpCodes.Callvirt; + return propertyGetOpCode; + } + } + + public DotnetValueContext(FieldDefinition field) + { + Field = field; + Property = null; + LocalVarIndex = -1; + } + + public DotnetValueContext(PropertyDefinition property) + { + Field = null; + Property = property; + LocalVarIndex = -1; + } + + public DotnetValueContext(int localVarIndex) + { + Field = null; + Property = null; + LocalVarIndex = localVarIndex; + } + + public void GetProperty(ILProcessor il, int propertyVar) + { + if (Property != null) + { + // [] array = ArrayProperty; + il.Emit(OpCodes.Ldarg_0); + il.Emit(PropertyGetOpCode, Property.GetMethod); + il.Emit(OpCodes.Stloc, propertyVar); + LocalVarIndex = propertyVar; + } + } + + public void SetProperty(ILProcessor il) + { + if (Property != null) + { + // ArrayProperty = array + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldloc, LocalVarIndex); + il.Emit(PropertySetOpCode, Property.SetMethod); + } + } + + public void Load(ILProcessor il) + { + if (Field != null) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, Field); + } + else if (Property != null && LocalVarIndex == -1) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(PropertyGetOpCode, Property.GetMethod); + } + else + { + il.Emit(OpCodes.Ldloc, LocalVarIndex); + } + } + + public void LoadAddress(ILProcessor il) + { + if (Field != null) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldflda, Field); + } + else if (Property != null && LocalVarIndex == -1) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(PropertyGetOpCode, Property.GetMethod); + } + else + { + il.Emit(OpCodes.Ldloca_S, (byte)LocalVarIndex); + } + } + + public void Store(ILProcessor il) + { + if (Field != null) + { + il.Emit(OpCodes.Stfld, Field); + } + else if (Property != null) + { + il.Emit(PropertySetOpCode, Property.SetMethod); + } + else + { + il.Emit(OpCodes.Stloc, LocalVarIndex); + } + } + } + + private static void GenerateSerializerType(ref DotnetContext context, TypeDefinition type, bool serialize, TypeReference valueType, ILProcessor il, DotnetValueContext valueContext) + { + if (valueContext.Property != null) + { + if (valueContext.Property.GetMethod == null) + { + MonoCecil.CompilationError($"Missing getter method for property '{valueContext.Property.Name}' of type {valueType.FullName} in {type.FullName} for automatic replication.", valueContext.Property); context.Failed = true; return; } - if (property.SetMethod == null) + if (valueContext.Property.SetMethod == null) { - MonoCecil.CompilationError($"Missing setter method for property '{property.Name}' of type {valueType.FullName} in {type.FullName} for automatic replication.", property); + MonoCecil.CompilationError($"Missing setter method for property '{valueContext.Property.Name}' of type {valueType.FullName} in {type.FullName} for automatic replication.", valueContext.Property); context.Failed = true; return; } - - if (property.GetMethod.IsVirtual) - propertyGetOpCode = OpCodes.Callvirt; - if (property.SetMethod.IsVirtual) - propertySetOpCode = OpCodes.Callvirt; } ModuleDefinition module = type.Module; TypeDefinition valueTypeDef = valueType.Resolve(); + TypeDefinition networkStreamType = context.NetworkStreamType.Resolve(); // Ensure to have valid serialization already generated for that value type (eg. when using custom structure field serialization) GenerateTypeSerialization(ref context, valueTypeDef); - if (_inBuildSerializers.TryGetValue(valueType.FullName, out var serializer)) + if (valueType.IsArray) + { + var elementType = valueType.GetElementType(); + var isRawPod = IsRawPOD(elementType); // Whether to use raw memory copy (eg. int, enum, Vector2) + var varStart = il.Body.Variables.Count; + module.GetType("System.Int32", out var intType); + il.Body.Variables.Add(new VariableDefinition(intType)); // [0] int length + if (isRawPod) + { + il.Body.Variables.Add(new VariableDefinition(new PointerType(elementType))); // [1] * + il.Body.Variables.Add(new VariableDefinition(new PinnedType(valueType))); // [2] [] pinned + } + else + { + il.Body.Variables.Add(new VariableDefinition(intType)); // [1] int idx + il.Body.Variables.Add(new VariableDefinition(elementType)); // [2] + } + + if (valueContext.Property != null) + il.Body.Variables.Add(new VariableDefinition(valueType)); // [3] [] + il.Body.InitLocals = true; + valueContext.GetProperty(il, varStart + 3); + if (serialize) + { + // [] array = Array; + il.Emit(OpCodes.Nop); + valueContext.Load(il); + + // int length = ((array != null) ? array.Length : 0); + il.Emit(OpCodes.Dup); + Instruction jmp1 = il.Create(OpCodes.Nop); + il.Emit(OpCodes.Brtrue_S, jmp1); + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldc_I4_0); + Instruction jmp2 = il.Create(OpCodes.Nop); + il.Emit(OpCodes.Br_S, jmp2); + il.Append(jmp1); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + il.Append(jmp2); + il.Emit(OpCodes.Stloc, varStart + 0); + + // stream.WriteInt32(length); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloc, varStart + 0); + var m = networkStreamType.GetMethod("WriteInt32"); + il.Emit(OpCodes.Callvirt, module.ImportReference(m)); + + il.Emit(OpCodes.Nop); + if (isRawPod) + { + // fixed (* bytes2 = Array) + valueContext.Load(il); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Stloc, varStart + 2); + Instruction jmp3 = il.Create(OpCodes.Nop); + il.Emit(OpCodes.Brfalse_S, jmp3); + il.Emit(OpCodes.Ldloc_2); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + Instruction jmp4 = il.Create(OpCodes.Nop); + il.Emit(OpCodes.Brtrue_S, jmp4); + il.Append(jmp3); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Conv_U); + il.Emit(OpCodes.Stloc, varStart + 1); // * + Instruction jmp5 = il.Create(OpCodes.Nop); + il.Emit(OpCodes.Br_S, jmp5); + + // stream.WriteBytes((byte*)bytes, length * sizeof())); + il.Append(jmp4); + il.Emit(OpCodes.Ldloc, varStart + 2); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldelema, elementType); + il.Emit(OpCodes.Conv_U); + il.Emit(OpCodes.Stloc, varStart + 1); // * + il.Append(jmp5); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloc, varStart + 1); // * + il.Emit(OpCodes.Ldloc, varStart + 0); + il.Emit(OpCodes.Sizeof, elementType); + il.Emit(OpCodes.Mul); + m = networkStreamType.GetMethod("WriteBytes", 2); + il.Emit(OpCodes.Callvirt, module.ImportReference(m)); + il.Emit(OpCodes.Nop); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Stloc, varStart + 2); + } + else + { + // int idx = 0 + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc, varStart + 1); // idx + Instruction jmp3 = il.Create(OpCodes.Nop); + il.Emit(OpCodes.Br_S, jmp3); + + // element = array[idx] + Instruction jmp4 = il.Create(OpCodes.Nop); + il.Append(jmp4); + valueContext.Load(il); + il.Emit(OpCodes.Ldloc, varStart + 1); // idx + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Stloc, varStart + 2); // + + // Serialize item value + il.Emit(OpCodes.Nop); + GenerateSerializerType(ref context, type, serialize, elementType, il, new DotnetValueContext(varStart + 2)); + + // idx++ + il.Emit(OpCodes.Nop); + il.Emit(OpCodes.Ldloc, varStart + 1); // idx + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Stloc, varStart + 1); // idx + + // idx < length + il.Append(jmp3); + il.Emit(OpCodes.Ldloc, varStart + 1); // idx + il.Emit(OpCodes.Ldloc, varStart + 0); // length + il.Emit(OpCodes.Clt); + il.Emit(OpCodes.Brtrue_S, jmp4); + } + } + else + { + // int length = stream.ReadInt32(); + il.Emit(OpCodes.Nop); + il.Emit(OpCodes.Ldarg_1); + var m = networkStreamType.GetMethod("ReadInt32"); + il.Emit(OpCodes.Callvirt, module.ImportReference(m)); + il.Emit(OpCodes.Stloc, varStart + 0); // length + + // System.Array.Resize(ref Array, length); + valueContext.LoadAddress(il); + il.Emit(OpCodes.Ldloc, varStart + 0); // length + module.TryGetTypeReference("System.Array", out var arrayType); + if (arrayType == null) + module.GetType("System.Array", out arrayType); + m = arrayType.Resolve().GetMethod("Resize", 2); + il.Emit(OpCodes.Call, module.ImportReference(m.InflateGeneric(elementType))); + + il.Emit(OpCodes.Nop); + if (isRawPod) + { + // fixed (* buffer = Array) + valueContext.Load(il); + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Stloc, varStart + 2); + Instruction jmp1 = il.Create(OpCodes.Nop); + il.Emit(OpCodes.Brfalse_S, jmp1); + il.Emit(OpCodes.Ldloc, varStart + 2); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Conv_I4); + Instruction jmp2 = il.Create(OpCodes.Nop); + il.Emit(OpCodes.Brtrue_S, jmp2); + il.Append(jmp1); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Conv_U); + il.Emit(OpCodes.Stloc, varStart + 1); // * buffer + Instruction jmp3 = il.Create(OpCodes.Nop); + il.Emit(OpCodes.Br_S, jmp3); + + // stream.ReadBytes((byte*)buffer, length * sizeof()); + il.Append(jmp2); + il.Emit(OpCodes.Ldloc, varStart + 2); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ldelema, elementType); + il.Emit(OpCodes.Conv_U); + il.Emit(OpCodes.Stloc, varStart + 1); // * buffer + il.Append(jmp3); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldloc, varStart + 1); // * buffer + il.Emit(OpCodes.Ldloc, varStart + 0); // length + il.Emit(OpCodes.Sizeof, elementType); + il.Emit(OpCodes.Mul); + m = networkStreamType.GetMethod("ReadBytes", 2); + il.Emit(OpCodes.Callvirt, module.ImportReference(m)); + il.Emit(OpCodes.Nop); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Stloc, varStart + 2); + } + else + { + // int idx = 0 + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc, varStart + 1); // idx + Instruction jmp3 = il.Create(OpCodes.Nop); + il.Emit(OpCodes.Br_S, jmp3); + + // Deserialize item value + Instruction jmp4 = il.Create(OpCodes.Nop); + il.Append(jmp4); + GenerateSerializerType(ref context, type, serialize, elementType, il, new DotnetValueContext(varStart + 2)); + + // array[idx] = element + il.Emit(OpCodes.Nop); + valueContext.Load(il); + il.Emit(OpCodes.Ldloc, varStart + 1); // idx + il.Emit(OpCodes.Ldloc, varStart + 2); // + il.Emit(OpCodes.Stelem_Ref); + + // idx++ + il.Emit(OpCodes.Nop); + il.Emit(OpCodes.Ldloc, varStart + 1); // idx + il.Emit(OpCodes.Ldc_I4_1); + il.Emit(OpCodes.Add); + il.Emit(OpCodes.Stloc, varStart + 1); // idx + + // idx < length + il.Append(jmp3); + il.Emit(OpCodes.Ldloc, varStart + 1); // idx + il.Emit(OpCodes.Ldloc, varStart + 0); // length + il.Emit(OpCodes.Clt); + il.Emit(OpCodes.Brtrue_S, jmp4); + } + + valueContext.SetProperty(il); + } + } + else if (_inBuildSerializers.TryGetValue(valueType.FullName, out var serializer)) { // Call NetworkStream method to write/read data MethodDefinition m; if (serialize) { il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldarg_0); - if (field != null) - il.Emit(OpCodes.Ldfld, field); - else - il.Emit(propertyGetOpCode, property.GetMethod); + valueContext.Load(il); m = networkStreamType.GetMethod(serializer.WriteMethod); } else @@ -921,10 +1272,7 @@ namespace Flax.Build.Plugins il.Emit(OpCodes.Callvirt, module.ImportReference(m)); if (!serialize) { - if (field != null) - il.Emit(OpCodes.Stfld, field); - else - il.Emit(propertySetOpCode, property.SetMethod); + valueContext.Store(il); } } else if (valueType.IsScriptingObject()) @@ -935,11 +1283,7 @@ namespace Flax.Build.Plugins if (serialize) { il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldarg_0); - if (field != null) - il.Emit(OpCodes.Ldfld, field); - else - il.Emit(propertyGetOpCode, property.GetMethod); + valueContext.Load(il); il.Emit(OpCodes.Dup); Instruction jmp1 = il.Create(OpCodes.Nop); il.Emit(OpCodes.Brtrue_S, jmp1); @@ -957,17 +1301,16 @@ namespace Flax.Build.Plugins } else { - var m = networkStreamType.GetMethod("ReadGuid"); - module.GetType("System.Type", out var typeType); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Callvirt, module.ImportReference(m)); - il.Emit(OpCodes.Stloc_0); - il.Emit(OpCodes.Ldarg_0); var varStart = il.Body.Variables.Count; var reference = module.ImportReference(guidType); reference.IsValueType = true; // Fix locals init to have valuetype for Guid instead of class il.Body.Variables.Add(new VariableDefinition(reference)); il.Body.InitLocals = true; + var m = networkStreamType.GetMethod("ReadGuid"); + module.GetType("System.Type", out var typeType); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Callvirt, module.ImportReference(m)); + il.Emit(OpCodes.Stloc_S, (byte)varStart); il.Emit(OpCodes.Ldloca_S, (byte)varStart); il.Emit(OpCodes.Ldtoken, valueType); var getTypeFromHandle = typeType.Resolve().GetMethod("GetTypeFromHandle"); @@ -975,10 +1318,7 @@ namespace Flax.Build.Plugins var tryFind = scriptingObjectType.Resolve().GetMethod("TryFind", 2); il.Emit(OpCodes.Call, module.ImportReference(tryFind)); il.Emit(OpCodes.Castclass, valueType); - if (field != null) - il.Emit(OpCodes.Stfld, field); - else - il.Emit(propertySetOpCode, property.SetMethod); + valueContext.Store(il); } } else if (valueTypeDef.IsEnum) @@ -988,11 +1328,7 @@ namespace Flax.Build.Plugins if (serialize) { il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldarg_0); - if (field != null) - il.Emit(OpCodes.Ldfld, field); - else - il.Emit(propertyGetOpCode, property.GetMethod); + valueContext.Load(il); var m = networkStreamType.GetMethod("WriteUInt32"); il.Emit(OpCodes.Callvirt, module.ImportReference(m)); } @@ -1002,174 +1338,24 @@ namespace Flax.Build.Plugins il.Emit(OpCodes.Ldarg_1); var m = networkStreamType.GetMethod("ReadUInt32"); il.Emit(OpCodes.Callvirt, module.ImportReference(m)); - if (field != null) - il.Emit(OpCodes.Stfld, field); - else - il.Emit(propertySetOpCode, property.SetMethod); + valueContext.Store(il); } } else if (valueType.IsValueType) { // Invoke structure generated serializer - // TODO: check if this type has generated serialization code - il.Emit(OpCodes.Ldarg_0); - if (field != null) - il.Emit(OpCodes.Ldflda, field); - else - il.Emit(propertyGetOpCode, property.GetMethod); + valueContext.LoadAddress(il); il.Emit(OpCodes.Ldarg_1); var m = valueTypeDef.GetMethod(serialize ? Thunk1 : Thunk2); il.Emit(OpCodes.Call, module.ImportReference(m)); } - else if (valueType.IsArray && valueType.GetElementType().IsValueType) - { - // TODO: support any array type by iterating over elements (separate serialize for each one) - var elementType = valueType.GetElementType(); - var varStart = il.Body.Variables.Count; - module.GetType("System.Int32", out var intType); - il.Body.Variables.Add(new VariableDefinition(intType)); - il.Body.Variables.Add(new VariableDefinition(new PointerType(elementType))); - il.Body.Variables.Add(new VariableDefinition(new PinnedType(valueType))); - il.Body.InitLocals = true; - if (serialize) - { - // [] array2 = Array1; - il.Emit(OpCodes.Nop); - il.Emit(OpCodes.Ldarg_0); - if (field != null) - il.Emit(OpCodes.Ldfld, field); - else - il.Emit(propertyGetOpCode, property.GetMethod); - - // int num2 = ((array2 != null) ? array2.Length : 0); - il.Emit(OpCodes.Dup); - Instruction jmp1 = il.Create(OpCodes.Nop); - il.Emit(OpCodes.Brtrue_S, jmp1); - il.Emit(OpCodes.Pop); - il.Emit(OpCodes.Ldc_I4_0); - Instruction jmp2 = il.Create(OpCodes.Nop); - il.Emit(OpCodes.Br_S, jmp2); - il.Append(jmp1); - il.Emit(OpCodes.Ldlen); - il.Emit(OpCodes.Conv_I4); - il.Append(jmp2); - il.Emit(OpCodes.Stloc, varStart + 0); - - // stream.WriteInt32(num2); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldloc, varStart + 0); - var m = networkStreamType.GetMethod("WriteInt32"); - il.Emit(OpCodes.Callvirt, module.ImportReference(m)); - - // fixed (* bytes2 = Array1) - il.Emit(OpCodes.Nop); - il.Emit(OpCodes.Ldarg_0); - if (field != null) - il.Emit(OpCodes.Ldfld, field); - else - il.Emit(propertyGetOpCode, property.GetMethod); - il.Emit(OpCodes.Dup); - il.Emit(OpCodes.Stloc, varStart + 2); - Instruction jmp3 = il.Create(OpCodes.Nop); - il.Emit(OpCodes.Brfalse_S, jmp3); - il.Emit(OpCodes.Ldloc_2); - il.Emit(OpCodes.Ldlen); - il.Emit(OpCodes.Conv_I4); - Instruction jmp4 = il.Create(OpCodes.Nop); - il.Emit(OpCodes.Brtrue_S, jmp4); - il.Append(jmp3); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Conv_U); - il.Emit(OpCodes.Stloc, varStart + 1); - Instruction jmp5 = il.Create(OpCodes.Nop); - il.Emit(OpCodes.Br_S, jmp5); - - // stream.WriteBytes((byte*)bytes, num * sizeof())); - il.Append(jmp4); - il.Emit(OpCodes.Ldloc, varStart + 2); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ldelema, elementType); - il.Emit(OpCodes.Conv_U); - il.Emit(OpCodes.Stloc, varStart + 1); - il.Append(jmp5); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldloc, varStart + 1); - il.Emit(OpCodes.Ldloc, varStart + 0); - il.Emit(OpCodes.Sizeof, elementType); - il.Emit(OpCodes.Mul); - m = networkStreamType.GetMethod("WriteBytes", 2); - il.Emit(OpCodes.Callvirt, module.ImportReference(m)); - il.Emit(OpCodes.Nop); - il.Emit(OpCodes.Ldnull); - il.Emit(OpCodes.Stloc, varStart + 2); - } - else - { - if (field == null) - throw new NotImplementedException("TODO: add support for array property replication"); - - // int num = stream.ReadInt32(); - il.Emit(OpCodes.Nop); - il.Emit(OpCodes.Ldarg_1); - var m = networkStreamType.GetMethod("ReadInt32"); - il.Emit(OpCodes.Callvirt, module.ImportReference(m)); - il.Emit(OpCodes.Stloc, varStart + 0); - - // System.Array.Resize(ref Array1, num); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldflda, field); - il.Emit(OpCodes.Ldloc, varStart + 0); - module.TryGetTypeReference("System.Array", out var arrayType); - m = arrayType.Resolve().GetMethod("Resize", 2); - il.Emit(OpCodes.Call, module.ImportReference(m.InflateGeneric(elementType))); - - // fixed (int* buffer = Array1) - il.Emit(OpCodes.Nop); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldfld, field); - il.Emit(OpCodes.Dup); - il.Emit(OpCodes.Stloc, varStart + 2); - Instruction jmp1 = il.Create(OpCodes.Nop); - il.Emit(OpCodes.Brfalse_S, jmp1); - il.Emit(OpCodes.Ldloc, varStart + 2); - il.Emit(OpCodes.Ldlen); - il.Emit(OpCodes.Conv_I4); - Instruction jmp2 = il.Create(OpCodes.Nop); - il.Emit(OpCodes.Brtrue_S, jmp2); - il.Append(jmp1); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Conv_U); - il.Emit(OpCodes.Stloc, varStart + 1); - Instruction jmp3 = il.Create(OpCodes.Nop); - il.Emit(OpCodes.Br_S, jmp3); - - // stream.ReadBytes((byte*)buffer, num * sizeof()); - il.Append(jmp2); - il.Emit(OpCodes.Ldloc, varStart + 2); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Ldelema, elementType); - il.Emit(OpCodes.Conv_U); - il.Emit(OpCodes.Stloc, varStart + 1); - il.Append(jmp3); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldloc, varStart + 1); - il.Emit(OpCodes.Ldloc, varStart + 0); - il.Emit(OpCodes.Sizeof, elementType); - il.Emit(OpCodes.Mul); - m = networkStreamType.GetMethod("ReadBytes", 2); - il.Emit(OpCodes.Callvirt, module.ImportReference(m)); - il.Emit(OpCodes.Nop); - il.Emit(OpCodes.Ldnull); - il.Emit(OpCodes.Stloc, varStart + 2); - } - } else { // Unknown type - if (property != null) - MonoCecil.CompilationError($"Not supported type '{valueType.FullName}' on {property.Name} in {type.FullName} for automatic replication.", property); - else if (field != null) - MonoCecil.CompilationError($"Not supported type '{valueType.FullName}' on {field.Name} in {type.FullName} for automatic replication.", field.Resolve()); + if (valueContext.Property != null) + MonoCecil.CompilationError($"Not supported type '{valueType.FullName}' on {valueContext.Property.Name} in {type.FullName} for automatic replication.", valueContext.Property); + else if (valueContext.Field != null) + MonoCecil.CompilationError($"Not supported type '{valueType.FullName}' on {valueContext.Field.Name} in {type.FullName} for automatic replication.", valueContext.Field.Resolve()); else MonoCecil.CompilationError($"Not supported type '{valueType.FullName}' for automatic replication."); context.Failed = true; @@ -1184,7 +1370,13 @@ namespace Flax.Build.Plugins // Ensure to have valid serialization already generated for that value type GenerateTypeSerialization(ref context, valueTypeDef); - if (_inBuildSerializers.TryGetValue(valueType.FullName, out var serializer)) + if (type.IsArray) + { + // TODO: refactor network stream read/write to share code between replication and rpcs + Log.Error($"Not supported type '{valueType.FullName}' for RPC parameter in {type.FullName}."); + context.Failed = true; + } + else if (_inBuildSerializers.TryGetValue(valueType.FullName, out var serializer)) { // Call NetworkStream method to write/read data if (serialize) @@ -1402,6 +1594,22 @@ namespace Flax.Build.Plugins { var parameter = method.Parameters[i]; var parameterType = parameter.ParameterType; + + // Special handling of Rpc Params + if (string.Equals(parameterType.FullName, "FlaxEngine.Networking.NetworkRpcParams", StringComparison.OrdinalIgnoreCase)) + { + // new NetworkRpcParams { SenderId = networkStream.SenderId } + il.Emit(OpCodes.Ldloca_S, (byte)(argsStart + i)); + il.Emit(OpCodes.Initobj, parameterType); + il.Emit(OpCodes.Ldloca_S, (byte)(argsStart + i)); + il.Emit(OpCodes.Ldloc_1); + var getSenderId = networkStreamType.Resolve().GetMethod("get_SenderId"); + il.Emit(OpCodes.Callvirt, module.ImportReference(getSenderId)); + var senderId = parameterType.Resolve().GetField("SenderId"); + il.Emit(OpCodes.Stfld, module.ImportReference(senderId)); + continue; + } + GenerateDotNetRPCSerializerType(ref context, type, false, argsStart + i, parameterType, il, networkStream.Resolve(), 1, null); } @@ -1431,6 +1639,8 @@ namespace Flax.Build.Plugins il.Body.InitLocals = true; var varsStart = il.Body.Variables.Count; + il.InsertBefore(ilStart, il.Create(OpCodes.Nop)); + // Is Server/Is Client boolean constants il.Body.Variables.Add(new VariableDefinition(module.ImportReference(boolType))); // [0] il.Body.Variables.Add(new VariableDefinition(module.ImportReference(boolType))); // [1] @@ -1475,14 +1685,25 @@ namespace Flax.Build.Plugins il.InsertBefore(ilStart, il.Create(OpCodes.Stloc, streamLocalIndex)); // stream loc=3 // Serialize all RPC parameters + var targetIdsArgIndex = -1; + FieldDefinition targetIdsField = null; for (int i = 0; i < method.Parameters.Count; i++) { var parameter = method.Parameters[i]; var parameterType = parameter.ParameterType; + + // Special handling of Rpc Params + if (string.Equals(parameterType.FullName, "FlaxEngine.Networking.NetworkRpcParams", StringComparison.OrdinalIgnoreCase)) + { + targetIdsArgIndex = i + 1; // NetworkRpcParams value argument index (starts at 1, 0 holds this) + targetIdsField = parameterType.Resolve().GetField("TargetIds"); + continue; + } + GenerateDotNetRPCSerializerType(ref context, type, true, i + 1, parameterType, il, networkStream.Resolve(), streamLocalIndex, ilStart); } - // NetworkReplicator.EndInvokeRPC(this, typeof(), "", stream); + // NetworkReplicator.EndInvokeRPC(this, typeof(), "", stream, targetIds); il.InsertBefore(ilStart, il.Create(OpCodes.Nop)); il.InsertBefore(ilStart, il.Create(OpCodes.Ldarg_0)); il.InsertBefore(ilStart, il.Create(OpCodes.Ldtoken, type)); @@ -1491,7 +1712,14 @@ namespace Flax.Build.Plugins il.InsertBefore(ilStart, il.Create(OpCodes.Call, module.ImportReference(getTypeFromHandle))); il.InsertBefore(ilStart, il.Create(OpCodes.Ldstr, method.Name)); il.InsertBefore(ilStart, il.Create(OpCodes.Ldloc, streamLocalIndex)); - var endInvokeRPC = networkReplicatorType.Resolve().GetMethod("EndInvokeRPC", 4); + if (targetIdsArgIndex != -1) + { + il.InsertBefore(ilStart, il.Create(OpCodes.Ldarg, targetIdsArgIndex)); + il.InsertBefore(ilStart, il.Create(OpCodes.Ldfld, module.ImportReference(targetIdsField))); + } + else + il.InsertBefore(ilStart, il.Create(OpCodes.Ldnull)); + var endInvokeRPC = networkReplicatorType.Resolve().GetMethod("EndInvokeRPC", 5); il.InsertBefore(ilStart, il.Create(OpCodes.Call, module.ImportReference(endInvokeRPC))); // if (server && networkMode == NetworkManagerMode.Client) return; diff --git a/Source/flax.natvis b/Source/flax.natvis index 750fd68f5..6942745c2 100644 --- a/Source/flax.natvis +++ b/Source/flax.natvis @@ -221,4 +221,16 @@ Tag={TagsListDebug[Index - 1]} + + + {{ Length={_length} }} + + _length + + _length + _data + + + +