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
+
+
+
+