Merge remote-tracking branch 'origin/master' into sdl_platform
This commit is contained in:
2
.github/ISSUE_TEMPLATE/1-bug.yaml
vendored
2
.github/ISSUE_TEMPLATE/1-bug.yaml
vendored
@@ -31,7 +31,7 @@ body:
|
||||
- '1.10'
|
||||
- '1.11'
|
||||
- master branch
|
||||
default: 2
|
||||
default: 3
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"Major": 1,
|
||||
"Minor": 11,
|
||||
"Revision": 0,
|
||||
"Build": 6802
|
||||
"Build": 6804
|
||||
},
|
||||
"Company": "Flax",
|
||||
"Copyright": "Copyright (c) 2012-2025 Wojciech Figat. All rights reserved.",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Wojciech Figat. All rights reserved.
|
||||
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.CustomEditors.Dedicated
|
||||
{
|
||||
@@ -11,7 +12,7 @@ namespace FlaxEditor.CustomEditors.Dedicated
|
||||
[CustomEditor(typeof(EnvironmentProbe)), DefaultEditor]
|
||||
public class EnvironmentProbeEditor : ActorEditor
|
||||
{
|
||||
private FlaxEngine.GUI.Button _bake;
|
||||
private Button _bake;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Initialize(LayoutElementsContainer layout)
|
||||
@@ -20,8 +21,9 @@ namespace FlaxEditor.CustomEditors.Dedicated
|
||||
|
||||
if (Values.HasDifferentTypes == false)
|
||||
{
|
||||
layout.Space(10);
|
||||
_bake = layout.Button("Bake").Button;
|
||||
var group = layout.Group("Bake");
|
||||
group.Panel.ItemsMargin = new Margin(Utilities.Constants.UIMargin * 2);
|
||||
_bake = group.Button("Bake").Button;
|
||||
_bake.Clicked += BakeButtonClicked;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -914,9 +914,11 @@ namespace FlaxEditor.CustomEditors.Dedicated
|
||||
// Remove drop down arrows and containment lines if no objects in the group
|
||||
if (group.Children.Count == 0)
|
||||
{
|
||||
group.Panel.Close();
|
||||
group.Panel.ArrowImageOpened = null;
|
||||
group.Panel.ArrowImageClosed = null;
|
||||
group.Panel.EnableContainmentLines = false;
|
||||
group.Panel.CanOpenClose = false;
|
||||
}
|
||||
|
||||
// Scripts arrange bar
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Wojciech Figat. All rights reserved.
|
||||
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.CustomEditors.Dedicated
|
||||
{
|
||||
@@ -19,8 +20,9 @@ namespace FlaxEditor.CustomEditors.Dedicated
|
||||
if (Values.HasDifferentTypes == false)
|
||||
{
|
||||
// Add 'Bake' button
|
||||
layout.Space(10);
|
||||
var button = layout.Button("Bake");
|
||||
var group = layout.Group("Bake");
|
||||
group.Panel.ItemsMargin = new Margin(Utilities.Constants.UIMargin * 2);
|
||||
var button = group.Button("Bake");
|
||||
button.Button.Clicked += BakeButtonClicked;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -650,7 +650,7 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
panel.Panel.Size = new Float2(0, 18);
|
||||
panel.Panel.Margin = new Margin(0, 0, Utilities.Constants.UIMargin, 0);
|
||||
|
||||
var removeButton = panel.Button("-", "Remove the last item");
|
||||
var removeButton = panel.Button("-", "Remove the last item.");
|
||||
removeButton.Button.Size = new Float2(16, 16);
|
||||
removeButton.Button.Enabled = size > _minCount;
|
||||
removeButton.Button.AnchorPreset = AnchorPresets.TopRight;
|
||||
@@ -661,7 +661,7 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
Resize(Count - 1);
|
||||
};
|
||||
|
||||
var addButton = panel.Button("+", "Add a new item");
|
||||
var addButton = panel.Button("+", "Add a new item.");
|
||||
addButton.Button.Size = new Float2(16, 16);
|
||||
addButton.Button.Enabled = (!NotNullItems || size > 0) && size < _maxCount;
|
||||
addButton.Button.AnchorPreset = AnchorPresets.TopRight;
|
||||
|
||||
@@ -104,7 +104,7 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
public event Action<TypePickerControl> TypePickerValueChanged;
|
||||
|
||||
/// <summary>
|
||||
/// The custom callback for types validation. Cane be used to implement a rule for types to pick.
|
||||
/// The custom callback for types validation. Can be used to implement a rule for types to pick.
|
||||
/// </summary>
|
||||
public Func<ScriptType, bool> CheckValid;
|
||||
|
||||
@@ -353,7 +353,13 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
}
|
||||
if (!string.IsNullOrEmpty(typeReference.CheckMethod))
|
||||
{
|
||||
var parentType = ParentEditor.Values[0].GetType();
|
||||
var parentEditor = ParentEditor;
|
||||
// Find actual parent editor if parent editor is collection editor
|
||||
while (parentEditor.GetType().IsAssignableTo(typeof(CollectionEditor)))
|
||||
parentEditor = parentEditor.ParentEditor;
|
||||
|
||||
var parentType = parentEditor.Values[0].GetType();
|
||||
|
||||
var method = parentType.GetMethod(typeReference.CheckMethod, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
if (method != null)
|
||||
{
|
||||
|
||||
@@ -1587,7 +1587,7 @@ namespace FlaxEditor
|
||||
if (dockedTo != null && dockedTo.SelectedTab != gameWin && dockedTo.SelectedTab != null)
|
||||
result = dockedTo.SelectedTab.Size;
|
||||
else
|
||||
result = gameWin.Viewport.Size;
|
||||
result = gameWin.Viewport.ContentSize;
|
||||
|
||||
result *= root.DpiScale;
|
||||
result = Float2.Round(result);
|
||||
|
||||
@@ -726,7 +726,7 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
|
||||
private void OnSurfaceMouseUp(ref Float2 mouse, MouseButton buttons, ref bool handled)
|
||||
{
|
||||
if (handled)
|
||||
if (handled || Surface.Context != Context)
|
||||
return;
|
||||
|
||||
// Check click over the connection
|
||||
@@ -751,7 +751,7 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
|
||||
private void OnSurfaceMouseDoubleClick(ref Float2 mouse, MouseButton buttons, ref bool handled)
|
||||
{
|
||||
if (handled)
|
||||
if (handled || Surface.Context != Context)
|
||||
return;
|
||||
|
||||
// Check double click over the connection
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using System.Runtime.Serialization.Formatters.Binary;
|
||||
using FlaxEditor.CustomEditors;
|
||||
using FlaxEditor.CustomEditors.Editors;
|
||||
using FlaxEditor.GUI.ContextMenu;
|
||||
@@ -18,6 +15,7 @@ namespace FlaxEditor.Surface
|
||||
class AttributesEditor : ContextMenuBase
|
||||
{
|
||||
private CustomEditorPresenter _presenter;
|
||||
private Proxy _proxy;
|
||||
private byte[] _oldData;
|
||||
|
||||
private class Proxy
|
||||
@@ -72,11 +70,11 @@ namespace FlaxEditor.Surface
|
||||
/// Initializes a new instance of the <see cref="AttributesEditor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="attributes">The attributes list to edit.</param>
|
||||
/// <param name="attributeType">The allowed attribute types to use.</param>
|
||||
public AttributesEditor(Attribute[] attributes, IList<Type> attributeType)
|
||||
/// <param name="attributeTypes">The allowed attribute types to use.</param>
|
||||
public AttributesEditor(Attribute[] attributes, IList<Type> attributeTypes)
|
||||
{
|
||||
// Context menu dimensions
|
||||
const float width = 340.0f;
|
||||
const float width = 375.0f;
|
||||
const float height = 370.0f;
|
||||
Size = new Float2(width, height);
|
||||
|
||||
@@ -88,61 +86,68 @@ namespace FlaxEditor.Surface
|
||||
Parent = this
|
||||
};
|
||||
|
||||
// Buttons
|
||||
float buttonsWidth = (width - 16.0f) * 0.5f;
|
||||
// Ok and Cancel Buttons
|
||||
float buttonsWidth = (width - 12.0f) * 0.5f;
|
||||
float buttonsHeight = 20.0f;
|
||||
var cancelButton = new Button(4.0f, title.Bottom + 4.0f, buttonsWidth, buttonsHeight)
|
||||
var okButton = new Button(4.0f, Bottom - 4.0f - buttonsHeight, buttonsWidth, buttonsHeight)
|
||||
{
|
||||
Text = "Ok",
|
||||
Parent = this
|
||||
};
|
||||
okButton.Clicked += OnOkButtonClicked;
|
||||
var cancelButton = new Button(okButton.Right + 4.0f, okButton.Y, buttonsWidth, buttonsHeight)
|
||||
{
|
||||
Text = "Cancel",
|
||||
Parent = this
|
||||
};
|
||||
cancelButton.Clicked += Hide;
|
||||
var okButton = new Button(cancelButton.Right + 4.0f, cancelButton.Y, buttonsWidth, buttonsHeight)
|
||||
{
|
||||
Text = "OK",
|
||||
Parent = this
|
||||
};
|
||||
okButton.Clicked += OnOkButtonClicked;
|
||||
|
||||
// Actual panel
|
||||
// Actual panel used to display attributes
|
||||
var panel1 = new Panel(ScrollBars.Vertical)
|
||||
{
|
||||
Bounds = new Rectangle(0, okButton.Bottom + 4.0f, width, height - okButton.Bottom - 2.0f),
|
||||
Bounds = new Rectangle(0, title.Bottom + 4.0f, width, height - buttonsHeight - title.Height - 14.0f),
|
||||
Parent = this
|
||||
};
|
||||
var editor = new CustomEditorPresenter(null);
|
||||
editor.Panel.AnchorPreset = AnchorPresets.HorizontalStretchTop;
|
||||
editor.Panel.IsScrollable = true;
|
||||
editor.Panel.Parent = panel1;
|
||||
editor.Panel.Tag = attributeType;
|
||||
editor.Panel.Tag = attributeTypes;
|
||||
_presenter = editor;
|
||||
|
||||
// Cache 'previous' state to check if attributes were edited after operation
|
||||
_oldData = SurfaceMeta.GetAttributesData(attributes);
|
||||
|
||||
editor.Select(new Proxy
|
||||
_proxy = new Proxy
|
||||
{
|
||||
Value = attributes,
|
||||
});
|
||||
};
|
||||
editor.Select(_proxy);
|
||||
|
||||
_presenter.Modified += OnPresenterModified;
|
||||
OnPresenterModified();
|
||||
}
|
||||
|
||||
private void OnPresenterModified()
|
||||
{
|
||||
if (_proxy.Value.Length == 0)
|
||||
{
|
||||
var label = _presenter.Label("No attributes.\nPress the \"+\" button to add a new one and then select an attribute type using the \"Type\" dropdown.", TextAlignment.Center);
|
||||
label.Label.Wrapping = TextWrapping.WrapWords;
|
||||
label.Control.Height = 35f;
|
||||
label.Label.Margin = new Margin(10f);
|
||||
label.Label.TextColor = label.Label.TextColorHighlighted = Style.Current.ForegroundGrey;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOkButtonClicked()
|
||||
{
|
||||
var newValue = ((Proxy)_presenter.Selection[0]).Value;
|
||||
for (int i = 0; i < newValue.Length; i++)
|
||||
{
|
||||
if (newValue[i] == null)
|
||||
{
|
||||
MessageBox.Show("One of the attributes is null. Please set it to the valid object.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
newValue = newValue.Where(v => v != null).ToArray();
|
||||
|
||||
var newData = SurfaceMeta.GetAttributesData(newValue);
|
||||
if (!_oldData.SequenceEqual(newData))
|
||||
{
|
||||
Edited?.Invoke(newValue);
|
||||
}
|
||||
|
||||
Hide();
|
||||
}
|
||||
@@ -183,7 +188,9 @@ namespace FlaxEditor.Surface
|
||||
{
|
||||
_presenter = null;
|
||||
_oldData = null;
|
||||
_proxy = null;
|
||||
Edited = null;
|
||||
_presenter.Modified -= OnPresenterModified;
|
||||
|
||||
base.OnDestroy();
|
||||
}
|
||||
|
||||
@@ -214,22 +214,25 @@ namespace FlaxEditor.Surface
|
||||
if (!_isRenaming)
|
||||
Render2D.DrawText(style.FontLarge, Title, _headerRect, style.Foreground, TextAlignment.Center, TextAlignment.Center);
|
||||
|
||||
// Close button
|
||||
Render2D.DrawSprite(style.Cross, _closeButtonRect, _closeButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey);
|
||||
|
||||
// Color button
|
||||
Render2D.DrawSprite(style.Settings, _colorButtonRect, _colorButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey);
|
||||
|
||||
// Check if is resizing
|
||||
if (_isResizing)
|
||||
if (Surface.CanEdit)
|
||||
{
|
||||
// Draw overlay
|
||||
Render2D.FillRectangle(_resizeButtonRect, style.Selection);
|
||||
Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder);
|
||||
}
|
||||
// Close button
|
||||
Render2D.DrawSprite(style.Cross, _closeButtonRect, _closeButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey);
|
||||
|
||||
// Resize button
|
||||
Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey);
|
||||
// Color button
|
||||
Render2D.DrawSprite(style.Settings, _colorButtonRect, _colorButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey);
|
||||
|
||||
// Check if is resizing
|
||||
if (_isResizing)
|
||||
{
|
||||
// Draw overlay
|
||||
Render2D.FillRectangle(_resizeButtonRect, style.Selection);
|
||||
Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder);
|
||||
}
|
||||
|
||||
// Resize button
|
||||
Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey);
|
||||
}
|
||||
|
||||
// Selection outline
|
||||
if (_isSelected)
|
||||
|
||||
@@ -140,7 +140,7 @@ namespace FlaxEditor.Windows
|
||||
}
|
||||
|
||||
private string _cacheFolder;
|
||||
private Guid _assetId;
|
||||
private AssetItem _item;
|
||||
private Surface _surface;
|
||||
private Label _loadingLabel;
|
||||
private CancellationTokenSource _token;
|
||||
@@ -163,13 +163,13 @@ namespace FlaxEditor.Windows
|
||||
public AssetReferencesGraphWindow(Editor editor, AssetItem assetItem)
|
||||
: base(editor, false, ScrollBars.None)
|
||||
{
|
||||
Title = assetItem.ShortName + " References";
|
||||
_item = assetItem;
|
||||
Title = _item.ShortName + " References";
|
||||
|
||||
_tempFolder = StringUtils.NormalizePath(Path.GetDirectoryName(Globals.TemporaryFolder));
|
||||
_cacheFolder = Path.Combine(Globals.ProjectCacheFolder, "References");
|
||||
if (!Directory.Exists(_cacheFolder))
|
||||
Directory.CreateDirectory(_cacheFolder);
|
||||
_assetId = assetItem.ID;
|
||||
_surface = new Surface(this)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
@@ -194,6 +194,7 @@ namespace FlaxEditor.Windows
|
||||
_nodesAssets.Add(assetId);
|
||||
var node = new AssetNode((uint)_nodes.Count + 1, _surface.Context, GraphNodes[0], GraphGroups[0], assetId);
|
||||
_nodes.Add(node);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
@@ -392,8 +393,7 @@ namespace FlaxEditor.Windows
|
||||
_nodesAssets = new HashSet<Guid>();
|
||||
var searchLevel = 4; // TODO: make it as an option (somewhere in window UI)
|
||||
// TODO: add option to filter assets by type (eg. show only textures as leaf nodes)
|
||||
var assetNode = SpawnNode(_assetId);
|
||||
// TODO: add some outline or tint color to the main node
|
||||
var assetNode = SpawnNode(_item.ID);
|
||||
BuildGraph(assetNode, searchLevel, false);
|
||||
ArrangeGraph(assetNode, false);
|
||||
BuildGraph(assetNode, searchLevel, true);
|
||||
@@ -402,6 +402,10 @@ namespace FlaxEditor.Windows
|
||||
return;
|
||||
_progress = 100.0f;
|
||||
|
||||
var commentRect = assetNode.EditorBounds;
|
||||
commentRect.Expand(80f);
|
||||
_surface.Context.CreateComment(ref commentRect, _item.ShortName, Color.Green);
|
||||
|
||||
// Update UI
|
||||
FlaxEngine.Scripting.InvokeOnUpdate(() =>
|
||||
{
|
||||
|
||||
@@ -142,6 +142,7 @@ namespace FlaxEditor.Windows
|
||||
{
|
||||
Title = "Content";
|
||||
Icon = editor.Icons.Folder32;
|
||||
var style = Style.Current;
|
||||
|
||||
FlaxEditor.Utilities.Utils.SetupCommonInputActions(this);
|
||||
|
||||
@@ -164,6 +165,8 @@ namespace FlaxEditor.Windows
|
||||
_navigationBar = new NavigationBar
|
||||
{
|
||||
Parent = _toolStrip,
|
||||
ScrollbarTrackColor = style.Background,
|
||||
ScrollbarThumbColor = style.ForegroundGrey,
|
||||
};
|
||||
|
||||
// Split panel
|
||||
@@ -179,7 +182,7 @@ namespace FlaxEditor.Windows
|
||||
var headerPanel = new ContainerControl
|
||||
{
|
||||
AnchorPreset = AnchorPresets.HorizontalStretchTop,
|
||||
BackgroundColor = Style.Current.Background,
|
||||
BackgroundColor = style.Background,
|
||||
IsScrollable = false,
|
||||
Offsets = new Margin(0, 0, 0, 18 + 6),
|
||||
};
|
||||
|
||||
@@ -10,17 +10,117 @@ using FlaxEditor.Modules;
|
||||
using FlaxEditor.Options;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
using FlaxEngine.Json;
|
||||
|
||||
namespace FlaxEditor.Windows
|
||||
{
|
||||
/// <summary>
|
||||
/// Render output control with content scaling support.
|
||||
/// </summary>
|
||||
public class ScaledRenderOutputControl : RenderOutputControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom scale.
|
||||
/// </summary>
|
||||
public float ContentScale = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// Actual bounds size for content (incl. scale).
|
||||
/// </summary>
|
||||
public Float2 ContentSize => Size / ContentScale;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ScaledRenderOutputControl(SceneRenderTask task)
|
||||
: base(task)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Draw()
|
||||
{
|
||||
DrawSelf();
|
||||
|
||||
// Draw children with scale
|
||||
var scaling = new Float3(ContentScale, ContentScale, 1);
|
||||
Matrix3x3.Scaling(ref scaling, out Matrix3x3 scale);
|
||||
Render2D.PushTransform(scale);
|
||||
if (ClipChildren)
|
||||
{
|
||||
GetDesireClientArea(out var clientArea);
|
||||
Render2D.PushClip(ref clientArea);
|
||||
DrawChildren();
|
||||
Render2D.PopClip();
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawChildren();
|
||||
}
|
||||
|
||||
Render2D.PopTransform();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void GetDesireClientArea(out Rectangle rect)
|
||||
{
|
||||
// Scale the area for the client controls
|
||||
rect = new Rectangle(Float2.Zero, Size / ContentScale);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IntersectsContent(ref Float2 locationParent, out Float2 location)
|
||||
{
|
||||
// Skip local PointFromParent but use base code
|
||||
location = base.PointFromParent(ref locationParent);
|
||||
return ContainsPoint(ref location);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IntersectsChildContent(Control child, Float2 location, out Float2 childSpaceLocation)
|
||||
{
|
||||
location /= ContentScale;
|
||||
return base.IntersectsChildContent(child, location, out childSpaceLocation);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool ContainsPoint(ref Float2 location, bool precise = false)
|
||||
{
|
||||
if (precise) // Ignore as utility-only element
|
||||
return false;
|
||||
return base.ContainsPoint(ref location, precise);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool RayCast(ref Float2 location, out Control hit)
|
||||
{
|
||||
var p = location / ContentScale;
|
||||
if (RayCastChildren(ref p, out hit))
|
||||
return true;
|
||||
return base.RayCast(ref location, out hit);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Float2 PointToParent(ref Float2 location)
|
||||
{
|
||||
var result = base.PointToParent(ref location);
|
||||
result *= ContentScale;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Float2 PointFromParent(ref Float2 location)
|
||||
{
|
||||
var result = base.PointFromParent(ref location);
|
||||
result /= ContentScale;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides in-editor play mode simulation.
|
||||
/// </summary>
|
||||
/// <seealso cref="FlaxEditor.Windows.EditorWindow" />
|
||||
public class GameWindow : EditorWindow
|
||||
{
|
||||
private readonly RenderOutputControl _viewport;
|
||||
private readonly ScaledRenderOutputControl _viewport;
|
||||
private readonly GameRoot _guiRoot;
|
||||
private bool _showGUI = true, _editGUI = true;
|
||||
private bool _showDebugDraw = false;
|
||||
@@ -77,7 +177,7 @@ namespace FlaxEditor.Windows
|
||||
/// <summary>
|
||||
/// Gets the viewport.
|
||||
/// </summary>
|
||||
public RenderOutputControl Viewport => _viewport;
|
||||
public ScaledRenderOutputControl Viewport => _viewport;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether show game GUI in the view or keep it hidden.
|
||||
@@ -295,7 +395,7 @@ namespace FlaxEditor.Windows
|
||||
var task = MainRenderTask.Instance;
|
||||
|
||||
// Setup viewport
|
||||
_viewport = new RenderOutputControl(task)
|
||||
_viewport = new ScaledRenderOutputControl(task)
|
||||
{
|
||||
AnchorPreset = AnchorPresets.StretchAll,
|
||||
Offsets = Margin.Zero,
|
||||
@@ -396,11 +496,8 @@ namespace FlaxEditor.Windows
|
||||
{
|
||||
if (v == null)
|
||||
return;
|
||||
|
||||
if (v.Size.Y <= 0 || v.Size.X <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(v.Label, "Free Aspect", StringComparison.Ordinal) && v.Size == new Int2(1, 1))
|
||||
{
|
||||
@@ -448,15 +545,7 @@ namespace FlaxEditor.Windows
|
||||
|
||||
private void ResizeViewport()
|
||||
{
|
||||
if (!_freeAspect)
|
||||
{
|
||||
_windowAspectRatio = Width / Height;
|
||||
}
|
||||
else
|
||||
{
|
||||
_windowAspectRatio = 1;
|
||||
}
|
||||
|
||||
_windowAspectRatio = _freeAspect ? 1 : Width / Height;
|
||||
var scaleWidth = _viewportAspectRatio / _windowAspectRatio;
|
||||
var scaleHeight = _windowAspectRatio / _viewportAspectRatio;
|
||||
|
||||
@@ -468,6 +557,24 @@ namespace FlaxEditor.Windows
|
||||
{
|
||||
_viewport.Bounds = new Rectangle(Width * (1 - scaleWidth) / 2, 0, Width * scaleWidth, Height);
|
||||
}
|
||||
|
||||
if (_viewport.KeepAspectRatio)
|
||||
{
|
||||
var resolution = _viewport.CustomResolution.HasValue ? (Float2)_viewport.CustomResolution.Value : Size;
|
||||
if (scaleHeight < 1)
|
||||
{
|
||||
_viewport.ContentScale = _viewport.Width / resolution.X;
|
||||
}
|
||||
else
|
||||
{
|
||||
_viewport.ContentScale = _viewport.Height / resolution.Y;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_viewport.ContentScale = 1;
|
||||
}
|
||||
|
||||
_viewport.SyncBackbufferSize();
|
||||
PerformLayout();
|
||||
}
|
||||
@@ -911,6 +1018,7 @@ namespace FlaxEditor.Windows
|
||||
return child.OnNavigate(direction, Float2.Zero, this, visited);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -961,7 +1069,7 @@ namespace FlaxEditor.Windows
|
||||
else
|
||||
_defaultScaleActiveIndex = 0;
|
||||
}
|
||||
|
||||
|
||||
if (_customScaleActiveIndex != -1)
|
||||
{
|
||||
var options = Editor.UI.CustomViewportScaleOptions;
|
||||
|
||||
@@ -666,7 +666,7 @@ void Asset::onLoaded()
|
||||
{
|
||||
onLoaded_MainThread();
|
||||
}
|
||||
else if (OnLoaded.IsBinded())
|
||||
else if (OnLoaded.IsBinded() || _references.HasItems())
|
||||
{
|
||||
Function<void()> action;
|
||||
action.Bind<Asset, &Asset::onLoaded>(this);
|
||||
|
||||
@@ -218,10 +218,14 @@ Asset::LoadResult MaterialInstance::load()
|
||||
Guid baseMaterialId;
|
||||
headerStream.Read(baseMaterialId);
|
||||
auto baseMaterial = Content::LoadAsync<MaterialBase>(baseMaterialId);
|
||||
if (baseMaterial)
|
||||
baseMaterial->AddReference();
|
||||
|
||||
// Load parameters
|
||||
if (Params.Load(&headerStream))
|
||||
{
|
||||
if (baseMaterial)
|
||||
baseMaterial->RemoveReference();
|
||||
LOG(Warning, "Cannot load material parameters.");
|
||||
return LoadResult::CannotLoadData;
|
||||
}
|
||||
@@ -239,6 +243,7 @@ Asset::LoadResult MaterialInstance::load()
|
||||
ParamsChanged();
|
||||
}
|
||||
|
||||
baseMaterial->RemoveReference();
|
||||
return LoadResult::Ok;
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,10 @@ public:
|
||||
/// </summary>
|
||||
double LastAccessTime = 0.0;
|
||||
|
||||
/// <summary>
|
||||
/// Flag set to indicate that chunk is during loading (atomic access to sync multiple reading threads).
|
||||
/// </summary>
|
||||
int64 IsLoading = 0;
|
||||
/// <summary>
|
||||
/// The chunk data.
|
||||
/// </summary>
|
||||
@@ -146,7 +150,7 @@ public:
|
||||
/// </summary>
|
||||
FORCE_INLINE bool IsLoaded() const
|
||||
{
|
||||
return Data.IsValid();
|
||||
return Data.IsValid() && Platform::AtomicRead(&IsLoading) == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -154,7 +158,7 @@ public:
|
||||
/// </summary>
|
||||
FORCE_INLINE bool IsMissing() const
|
||||
{
|
||||
return Data.IsInvalid();
|
||||
return !IsLoaded();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "FlaxPackage.h"
|
||||
#include "ContentStorageManager.h"
|
||||
#include "Engine/Core/Log.h"
|
||||
#include "Engine/Core/ScopeExit.h"
|
||||
#include "Engine/Core/Types/TimeSpan.h"
|
||||
#include "Engine/Platform/File.h"
|
||||
#include "Engine/Profiler/ProfilerCPU.h"
|
||||
@@ -246,6 +247,7 @@ FlaxStorage::~FlaxStorage()
|
||||
ASSERT(IsDisposed());
|
||||
CHECK(_chunksLock == 0);
|
||||
CHECK(_refCount == 0);
|
||||
CHECK(_isUnloadingData == 0);
|
||||
ASSERT(_chunks.IsEmpty());
|
||||
|
||||
#if USE_EDITOR
|
||||
@@ -261,6 +263,22 @@ FlaxStorage::~FlaxStorage()
|
||||
#endif
|
||||
}
|
||||
|
||||
void FlaxStorage::LockChunks()
|
||||
{
|
||||
RETRY:
|
||||
Platform::InterlockedIncrement(&_chunksLock);
|
||||
if (Platform::AtomicRead(&_isUnloadingData) != 0)
|
||||
{
|
||||
// Someone else is closing file handles or freeing chunks so wait for it to finish and retry
|
||||
Platform::InterlockedDecrement(&_chunksLock);
|
||||
do
|
||||
{
|
||||
Platform::Sleep(1);
|
||||
} while (Platform::AtomicRead(&_isUnloadingData) != 0);
|
||||
goto RETRY;
|
||||
}
|
||||
}
|
||||
|
||||
FlaxStorage::LockData FlaxStorage::LockSafe()
|
||||
{
|
||||
auto lock = LockData(this);
|
||||
@@ -689,7 +707,6 @@ bool FlaxStorage::LoadAssetHeader(const Guid& id, AssetInitData& data)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load header
|
||||
return LoadAssetHeader(e, data);
|
||||
}
|
||||
|
||||
@@ -699,7 +716,10 @@ bool FlaxStorage::LoadAssetChunk(FlaxChunk* chunk)
|
||||
ASSERT(IsLoaded());
|
||||
ASSERT(chunk != nullptr && _chunks.Contains(chunk));
|
||||
|
||||
// Check if already loaded
|
||||
// Protect against loading the same chunk from multiple threads at once
|
||||
while (Platform::InterlockedCompareExchange(&chunk->IsLoading, 1, 0) != 0)
|
||||
Platform::Sleep(1);
|
||||
SCOPE_EXIT{ Platform::AtomicStore(&chunk->IsLoading, 0); };
|
||||
if (chunk->IsLoaded())
|
||||
return false;
|
||||
|
||||
@@ -776,12 +796,10 @@ bool FlaxStorage::LoadAssetChunk(FlaxChunk* chunk)
|
||||
// Raw data
|
||||
chunk->Data.Read(stream, size);
|
||||
}
|
||||
ASSERT(chunk->IsLoaded());
|
||||
chunk->RegisterUsage();
|
||||
}
|
||||
|
||||
UnlockChunks();
|
||||
|
||||
return failed;
|
||||
}
|
||||
|
||||
@@ -1420,10 +1438,12 @@ FileReadStream* FlaxStorage::OpenFile()
|
||||
|
||||
bool FlaxStorage::CloseFileHandles()
|
||||
{
|
||||
// Guard the whole process so if new thread wants to lock the chunks will need to wait for this to end
|
||||
Platform::InterlockedIncrement(&_isUnloadingData);
|
||||
SCOPE_EXIT{ Platform::InterlockedDecrement(&_isUnloadingData); };
|
||||
|
||||
if (Platform::AtomicRead(&_chunksLock) == 0 && Platform::AtomicRead(&_files) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return false; // Early out when no files are opened
|
||||
PROFILE_CPU();
|
||||
PROFILE_MEM(ContentFiles);
|
||||
|
||||
@@ -1496,9 +1516,21 @@ void FlaxStorage::Tick(double time)
|
||||
{
|
||||
auto chunk = _chunks.Get()[i];
|
||||
const bool wasUsed = (time - chunk->LastAccessTime) < unusedDataChunksLifetime;
|
||||
if (!wasUsed && chunk->IsLoaded() && EnumHasNoneFlags(chunk->Flags, FlaxChunkFlags::KeepInMemory))
|
||||
if (!wasUsed &&
|
||||
chunk->IsLoaded() &&
|
||||
EnumHasNoneFlags(chunk->Flags, FlaxChunkFlags::KeepInMemory) &&
|
||||
Platform::AtomicRead(&chunk->IsLoading) == 0)
|
||||
{
|
||||
// Guard the unloading so if other thread wants to lock the chunks will need to wait for this to end
|
||||
Platform::InterlockedIncrement(&_isUnloadingData);
|
||||
if (Platform::AtomicRead(&_chunksLock) != 0 || Platform::AtomicRead(&chunk->IsLoading) != 0)
|
||||
{
|
||||
// Someone started loading so skip ticking
|
||||
Platform::InterlockedDecrement(&_isUnloadingData);
|
||||
return;
|
||||
}
|
||||
chunk->Unload();
|
||||
Platform::InterlockedDecrement(&_isUnloadingData);
|
||||
}
|
||||
wasAnyUsed |= wasUsed;
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ protected:
|
||||
int64 _refCount = 0;
|
||||
int64 _chunksLock = 0;
|
||||
int64 _files = 0;
|
||||
int64 _isUnloadingData = 0;
|
||||
double _lastRefLostTime;
|
||||
CriticalSection _loadLocker;
|
||||
|
||||
@@ -129,10 +130,7 @@ public:
|
||||
/// <summary>
|
||||
/// Locks the storage chunks data to prevent disposing them. Also ensures that file handles won't be closed while chunks are locked.
|
||||
/// </summary>
|
||||
FORCE_INLINE void LockChunks()
|
||||
{
|
||||
Platform::InterlockedIncrement(&_chunksLock);
|
||||
}
|
||||
void LockChunks();
|
||||
|
||||
/// <summary>
|
||||
/// Unlocks the storage chunks data.
|
||||
|
||||
@@ -338,10 +338,10 @@ public:
|
||||
StreamTextureMipTask(StreamingTexture* texture, int32 mipIndex, Task* rootTask)
|
||||
: GPUUploadTextureMipTask(texture->GetTexture(), mipIndex, Span<byte>(nullptr, 0), 0, 0, false)
|
||||
, _streamingTexture(texture)
|
||||
, _rootTask(rootTask ? rootTask : this)
|
||||
, _rootTask(rootTask)
|
||||
, _dataLock(_streamingTexture->GetOwner()->LockData())
|
||||
{
|
||||
_streamingTexture->_streamingTasks.Add(_rootTask);
|
||||
_streamingTexture->_streamingTasks.Add(this);
|
||||
_texture.Released.Bind<StreamTextureMipTask, &StreamTextureMipTask::OnResourceReleased2>(this);
|
||||
}
|
||||
|
||||
@@ -357,7 +357,7 @@ private:
|
||||
if (_streamingTexture)
|
||||
{
|
||||
ScopeLock lock(_streamingTexture->GetOwner()->GetOwnerLocker());
|
||||
_streamingTexture->_streamingTasks.Remove(_rootTask);
|
||||
_streamingTexture->_streamingTasks.Remove(this);
|
||||
_streamingTexture = nullptr;
|
||||
}
|
||||
}
|
||||
@@ -422,6 +422,15 @@ protected:
|
||||
|
||||
GPUUploadTextureMipTask::OnFail();
|
||||
}
|
||||
|
||||
void OnCancel() override
|
||||
{
|
||||
GPUUploadTextureMipTask::OnCancel();
|
||||
|
||||
// Cancel the root task too (eg. mip loading from asset)
|
||||
if (_rootTask != nullptr)
|
||||
_rootTask->Cancel();
|
||||
}
|
||||
};
|
||||
|
||||
Task* StreamingTexture::CreateStreamingTask(int32 residency)
|
||||
|
||||
@@ -42,38 +42,38 @@ public:
|
||||
/// <summary>
|
||||
/// The reflections texture resolution.
|
||||
/// </summary>
|
||||
API_FIELD(Attributes="EditorOrder(0), EditorDisplay(\"Probe\")")
|
||||
API_FIELD(Attributes="EditorOrder(0), EditorDisplay(\"Quality\")")
|
||||
ProbeCubemapResolution CubemapResolution = ProbeCubemapResolution::UseGraphicsSettings;
|
||||
|
||||
/// <summary>
|
||||
/// The probe update mode.
|
||||
/// </summary>
|
||||
API_FIELD(Attributes = "EditorOrder(10), EditorDisplay(\"Quality\")")
|
||||
ProbeUpdateMode UpdateMode = ProbeUpdateMode::Manual;
|
||||
|
||||
/// <summary>
|
||||
/// The reflections brightness.
|
||||
/// </summary>
|
||||
API_FIELD(Attributes="EditorOrder(10), Limit(0, 1000, 0.01f), EditorDisplay(\"Probe\")")
|
||||
API_FIELD(Attributes="EditorOrder(0), Limit(0, 1000, 0.01f), EditorDisplay(\"Probe\")")
|
||||
float Brightness = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// The probe rendering order. The higher values are render later (on top).
|
||||
/// </summary>
|
||||
API_FIELD(Attributes = "EditorOrder(25), EditorDisplay(\"Probe\")")
|
||||
API_FIELD(Attributes = "EditorOrder(20), EditorDisplay(\"Probe\")")
|
||||
int32 SortOrder = 0;
|
||||
|
||||
/// <summary>
|
||||
/// The probe update mode.
|
||||
/// </summary>
|
||||
API_FIELD(Attributes="EditorOrder(30), EditorDisplay(\"Probe\")")
|
||||
ProbeUpdateMode UpdateMode = ProbeUpdateMode::Manual;
|
||||
|
||||
/// <summary>
|
||||
/// The probe capture camera near plane distance.
|
||||
/// </summary>
|
||||
API_FIELD(Attributes="EditorOrder(30), Limit(0, float.MaxValue, 0.01f), EditorDisplay(\"Probe\")")
|
||||
API_FIELD(Attributes="EditorOrder(25), Limit(0, float.MaxValue, 0.01f), EditorDisplay(\"Probe\")")
|
||||
float CaptureNearPlane = 10.0f;
|
||||
|
||||
public:
|
||||
/// <summary>
|
||||
/// Gets the probe radius.
|
||||
/// </summary>
|
||||
API_PROPERTY(Attributes="EditorOrder(20), DefaultValue(3000.0f), Limit(0), EditorDisplay(\"Probe\")")
|
||||
API_PROPERTY(Attributes="EditorOrder(15), DefaultValue(3000.0f), Limit(0), EditorDisplay(\"Probe\")")
|
||||
float GetRadius() const;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#endif
|
||||
|
||||
// Internal version number of networking implementation. Updated once engine changes serialization or connection rules.
|
||||
#define NETWORK_PROTOCOL_VERSION 4
|
||||
#define NETWORK_PROTOCOL_VERSION 5
|
||||
|
||||
// Enables encoding object ids and typenames via uint32 keys rather than full data send.
|
||||
#define USE_NETWORK_KEYS 1
|
||||
@@ -29,6 +29,7 @@ enum class NetworkMessageIDs : uint8
|
||||
ObjectDespawn,
|
||||
ObjectRole,
|
||||
ObjectRpc,
|
||||
ObjectRpcPart,
|
||||
|
||||
MAX,
|
||||
};
|
||||
@@ -48,6 +49,7 @@ public:
|
||||
static void OnNetworkMessageObjectDespawn(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer);
|
||||
static void OnNetworkMessageObjectRole(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer);
|
||||
static void OnNetworkMessageObjectRpc(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer);
|
||||
static void OnNetworkMessageObjectRpcPart(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer);
|
||||
|
||||
#if COMPILE_WITH_PROFILER
|
||||
|
||||
|
||||
@@ -391,6 +391,7 @@ namespace
|
||||
NetworkInternal::OnNetworkMessageObjectDespawn,
|
||||
NetworkInternal::OnNetworkMessageObjectRole,
|
||||
NetworkInternal::OnNetworkMessageObjectRpc,
|
||||
NetworkInternal::OnNetworkMessageObjectRpcPart,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -55,14 +55,14 @@ PACK_STRUCT(struct NetworkMessageObjectReplicate
|
||||
uint32 OwnerFrame;
|
||||
});
|
||||
|
||||
PACK_STRUCT(struct NetworkMessageObjectReplicatePayload
|
||||
PACK_STRUCT(struct NetworkMessageObjectPartPayload
|
||||
{
|
||||
uint16 DataSize;
|
||||
uint16 PartsCount;
|
||||
uint16 PartSize;
|
||||
});
|
||||
|
||||
PACK_STRUCT(struct NetworkMessageObjectReplicatePart
|
||||
PACK_STRUCT(struct NetworkMessageObjectPart
|
||||
{
|
||||
NetworkMessageIDs ID = NetworkMessageIDs::ObjectReplicatePart;
|
||||
uint32 OwnerFrame;
|
||||
@@ -111,7 +111,7 @@ PACK_STRUCT(struct NetworkMessageObjectRole
|
||||
PACK_STRUCT(struct NetworkMessageObjectRpc
|
||||
{
|
||||
NetworkMessageIDs ID = NetworkMessageIDs::ObjectRpc;
|
||||
uint16 ArgsSize;
|
||||
uint32 OwnerFrame;
|
||||
});
|
||||
|
||||
struct NetworkReplicatedObject
|
||||
@@ -182,13 +182,14 @@ struct Serializer
|
||||
void* Tags[2];
|
||||
};
|
||||
|
||||
struct ReplicateItem
|
||||
struct PartsItem
|
||||
{
|
||||
ScriptingObjectReference<ScriptingObject> Object;
|
||||
Guid ObjectId;
|
||||
uint16 PartsLeft;
|
||||
uint32 OwnerFrame;
|
||||
uint32 OwnerClientId;
|
||||
const void* Tag;
|
||||
Array<byte> Data;
|
||||
};
|
||||
|
||||
@@ -220,7 +221,7 @@ struct DespawnItem
|
||||
DataContainer<uint32> Targets;
|
||||
};
|
||||
|
||||
struct RpcItem
|
||||
struct RpcSendItem
|
||||
{
|
||||
ScriptingObjectReference<ScriptingObject> Object;
|
||||
NetworkRpcName Name;
|
||||
@@ -233,11 +234,12 @@ namespace
|
||||
{
|
||||
CriticalSection ObjectsLock;
|
||||
HashSet<NetworkReplicatedObject> Objects;
|
||||
Array<ReplicateItem> ReplicationParts;
|
||||
Array<PartsItem> ReplicationParts;
|
||||
Array<PartsItem> RpcParts;
|
||||
Array<SpawnItemParts> SpawnParts;
|
||||
Array<SpawnItem> SpawnQueue;
|
||||
Array<DespawnItem> DespawnQueue;
|
||||
Array<RpcItem> RpcQueue;
|
||||
Array<RpcSendItem> RpcQueue;
|
||||
Dictionary<Guid, Guid> IdsRemappingTable;
|
||||
NetworkStream* CachedWriteStream = nullptr;
|
||||
NetworkStream* CachedReadStream = nullptr;
|
||||
@@ -251,6 +253,7 @@ namespace
|
||||
#endif
|
||||
Array<Guid> DespawnedObjects;
|
||||
uint32 SpawnId = 0;
|
||||
uint32 RpcId = 0;
|
||||
|
||||
#if USE_EDITOR
|
||||
void OnScriptsReloading()
|
||||
@@ -505,6 +508,76 @@ void SetupObjectSpawnMessageItem(SpawnItem* e, NetworkMessage& msg)
|
||||
msg.WriteStructure(msgDataItem);
|
||||
}
|
||||
|
||||
void SendInParts(NetworkPeer* peer, NetworkChannelType channel, const byte* data, const uint16 dataSize, NetworkMessage& msg, const NetworkRpcName& name, bool toServer, const Guid& objectId, uint32 ownerFrame, NetworkMessageIDs partId)
|
||||
{
|
||||
NetworkMessageObjectPartPayload msgDataPayload;
|
||||
msgDataPayload.DataSize = dataSize;
|
||||
const uint32 networkKeyIdWorstCaseSize = sizeof(uint32) + sizeof(Guid);
|
||||
const uint32 msgMaxData = peer->Config.MessageSize - msg.Position - sizeof(NetworkMessageObjectPartPayload);
|
||||
const uint32 partMaxData = peer->Config.MessageSize - sizeof(NetworkMessageObjectPart) - networkKeyIdWorstCaseSize;
|
||||
uint32 partsCount = 1;
|
||||
uint32 dataStart = 0;
|
||||
uint32 msgDataSize = dataSize;
|
||||
if (dataSize > msgMaxData)
|
||||
{
|
||||
// Send msgMaxData within first message
|
||||
msgDataSize = msgMaxData;
|
||||
dataStart += msgMaxData;
|
||||
|
||||
// Send rest of the data in separate parts
|
||||
partsCount += Math::DivideAndRoundUp(dataSize - dataStart, partMaxData);
|
||||
|
||||
// TODO: promote channel to Ordered when using parts?
|
||||
}
|
||||
else
|
||||
dataStart += dataSize;
|
||||
ASSERT(partsCount <= MAX_uint8);
|
||||
msgDataPayload.PartsCount = partsCount;
|
||||
msgDataPayload.PartSize = msgDataSize;
|
||||
msg.WriteStructure(msgDataPayload);
|
||||
msg.WriteBytes(data, msgDataSize);
|
||||
uint32 messageSize = msg.Length;
|
||||
if (toServer)
|
||||
peer->EndSendMessage(channel, msg);
|
||||
else
|
||||
peer->EndSendMessage(channel, msg, CachedTargets);
|
||||
|
||||
// Send all other parts
|
||||
for (uint32 partIndex = 1; partIndex < partsCount; partIndex++)
|
||||
{
|
||||
NetworkMessageObjectPart msgDataPart;
|
||||
msgDataPart.ID = partId;
|
||||
msgDataPart.OwnerFrame = ownerFrame;
|
||||
msgDataPart.DataSize = msgDataPayload.DataSize;
|
||||
msgDataPart.PartsCount = msgDataPayload.PartsCount;
|
||||
msgDataPart.PartStart = dataStart;
|
||||
msgDataPart.PartSize = Math::Min(dataSize - dataStart, partMaxData);
|
||||
msg = peer->BeginSendMessage();
|
||||
msg.WriteStructure(msgDataPart);
|
||||
msg.WriteNetworkId(objectId);
|
||||
msg.WriteBytes(data + msgDataPart.PartStart, msgDataPart.PartSize);
|
||||
messageSize += msg.Length;
|
||||
dataStart += msgDataPart.PartSize;
|
||||
if (toServer)
|
||||
peer->EndSendMessage(channel, msg);
|
||||
else
|
||||
peer->EndSendMessage(channel, msg, CachedTargets);
|
||||
}
|
||||
ASSERT_LOW_LAYER(dataStart == dataSize);
|
||||
|
||||
#if COMPILE_WITH_PROFILER
|
||||
// Network stats recording
|
||||
if (NetworkInternal::EnableProfiling)
|
||||
{
|
||||
auto& profileEvent = NetworkInternal::ProfilerEvents[name];
|
||||
profileEvent.Count++;
|
||||
profileEvent.DataSize += dataSize;
|
||||
profileEvent.MessageSize += messageSize;
|
||||
profileEvent.Receivers += toServer ? 1 : CachedTargets.Count();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void SendObjectSpawnMessage(const SpawnGroup& group, const Array<NetworkClient*>& clients)
|
||||
{
|
||||
PROFILE_CPU();
|
||||
@@ -589,7 +662,7 @@ void SendObjectRoleMessage(const NetworkReplicatedObject& item, const NetworkCli
|
||||
msg.WriteNetworkId(objectId);
|
||||
if (NetworkManager::IsClient())
|
||||
{
|
||||
NetworkManager::Peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg);
|
||||
peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -598,6 +671,160 @@ void SendObjectRoleMessage(const NetworkReplicatedObject& item, const NetworkCli
|
||||
}
|
||||
}
|
||||
|
||||
void SendDespawn(DespawnItem& e)
|
||||
{
|
||||
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Despawn object ID={}", e.Id.ToString());
|
||||
NetworkMessageObjectDespawn msgData;
|
||||
Guid objectId = e.Id;
|
||||
{
|
||||
// Remap local client object ids into server ids
|
||||
IdsRemappingTable.KeyOf(objectId, &objectId);
|
||||
}
|
||||
auto peer = NetworkManager::Peer;
|
||||
NetworkMessage msg = peer->BeginSendMessage();
|
||||
msg.WriteStructure(msgData);
|
||||
msg.WriteNetworkId(objectId);
|
||||
BuildCachedTargets(NetworkManager::Clients, e.Targets);
|
||||
if (NetworkManager::IsClient())
|
||||
peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg);
|
||||
else
|
||||
peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg, CachedTargets);
|
||||
}
|
||||
|
||||
void SendReplication(ScriptingObject* obj, NetworkClientsMask targetClients)
|
||||
{
|
||||
auto it = Objects.Find(obj->GetID());
|
||||
if (it.IsEnd())
|
||||
return;
|
||||
auto& item = it->Item;
|
||||
const bool isClient = NetworkManager::IsClient();
|
||||
|
||||
// Skip serialization of objects that none will receive
|
||||
if (!isClient)
|
||||
{
|
||||
BuildCachedTargets(item, targetClients);
|
||||
if (CachedTargets.Count() == 0)
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.AsNetworkObject)
|
||||
item.AsNetworkObject->OnNetworkSerialize();
|
||||
|
||||
// Serialize object
|
||||
NetworkStream* stream = CachedWriteStream;
|
||||
stream->Initialize();
|
||||
const bool failed = NetworkReplicator::InvokeSerializer(obj->GetTypeHandle(), obj, stream, true);
|
||||
if (failed)
|
||||
{
|
||||
//NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Cannot serialize object {} of type {} (missing serialization logic)", item.ToString(), obj->GetType().ToString());
|
||||
return;
|
||||
}
|
||||
const uint32 size = stream->GetPosition();
|
||||
if (size > MAX_uint16)
|
||||
{
|
||||
LOG(Error, "Too much data for object {} replication ({} bytes provided while limit is {}).", item.ToString(), size, MAX_uint16);
|
||||
return;
|
||||
}
|
||||
|
||||
#if USE_NETWORK_REPLICATOR_CACHE
|
||||
// Process replication cache to skip sending object data if it didn't change
|
||||
if (item.RepCache.Data.Length() == size &&
|
||||
item.RepCache.Mask == targetClients &&
|
||||
Platform::MemoryCompare(item.RepCache.Data.Get(), stream->GetBuffer(), size) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
item.RepCache.Mask = targetClients;
|
||||
item.RepCache.Data.Copy(stream->GetBuffer(), size);
|
||||
#endif
|
||||
// TODO: use Unreliable for dynamic objects that are replicated every frame? (eg. player state)
|
||||
constexpr NetworkChannelType repChannel = NetworkChannelType::Reliable;
|
||||
|
||||
// Send object to clients
|
||||
NetworkMessageObjectReplicate msgData;
|
||||
msgData.OwnerFrame = NetworkManager::Frame;
|
||||
Guid objectId = item.ObjectId, parentId = item.ParentId;
|
||||
{
|
||||
// Remap local client object ids into server ids
|
||||
IdsRemappingTable.KeyOf(objectId, &objectId);
|
||||
IdsRemappingTable.KeyOf(parentId, &parentId);
|
||||
}
|
||||
NetworkPeer* peer = NetworkManager::Peer;
|
||||
NetworkMessage msg = peer->BeginSendMessage();
|
||||
msg.WriteStructure(msgData);
|
||||
msg.WriteNetworkId(objectId);
|
||||
msg.WriteNetworkId(parentId);
|
||||
msg.WriteNetworkName(obj->GetType().Fullname);
|
||||
const NetworkRpcName name(obj->GetTypeHandle(), StringAnsiView::Empty);
|
||||
SendInParts(peer, repChannel, stream->GetBuffer(), size, msg, name, isClient, objectId, msgData.OwnerFrame, NetworkMessageIDs::ObjectReplicatePart);
|
||||
}
|
||||
|
||||
void SendRpc(RpcSendItem& e)
|
||||
{
|
||||
ScriptingObject* obj = e.Object.Get();
|
||||
if (!obj)
|
||||
return;
|
||||
auto it = Objects.Find(obj->GetID());
|
||||
if (it == Objects.End())
|
||||
{
|
||||
#if !BUILD_RELEASE
|
||||
if (!DespawnedObjects.Contains(obj->GetID()))
|
||||
LOG(Error, "Cannot invoke RPC method '{0}.{1}' on object '{2}' that is not registered in networking (use 'NetworkReplicator.AddObject').", e.Name.First.ToString(), e.Name.Second.ToString(), obj->GetID());
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
auto& item = it->Item;
|
||||
if (e.ArgsData.Length() > MAX_uint16)
|
||||
{
|
||||
LOG(Error, "Too much data for object RPC method '{}.{}' on object '{}' ({} bytes provided while limit is {}).", e.Name.First.ToString(), e.Name.Second.ToString(), obj->GetID(), e.ArgsData.Length(), MAX_uint16);
|
||||
return;
|
||||
}
|
||||
const NetworkManagerMode mode = NetworkManager::Mode;
|
||||
NetworkPeer* peer = NetworkManager::Peer;
|
||||
|
||||
bool toServer;
|
||||
if (e.Info.Server && mode == NetworkManagerMode::Client)
|
||||
{
|
||||
// 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
|
||||
toServer = true;
|
||||
}
|
||||
else if (e.Info.Client && (mode == NetworkManagerMode::Server || mode == NetworkManagerMode::Host))
|
||||
{
|
||||
// Server -> Client(s)
|
||||
BuildCachedTargets(NetworkManager::Clients, item.TargetClientIds, e.Targets, NetworkManager::LocalClientId);
|
||||
if (CachedTargets.IsEmpty())
|
||||
return;
|
||||
toServer = false;
|
||||
}
|
||||
else
|
||||
return;
|
||||
|
||||
// Send RPC message
|
||||
//NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Rpc {}.{} object ID={}", e.Name.First.ToString(), e.Name.Second.ToString(), item.ToString());
|
||||
NetworkMessageObjectRpc msgData;
|
||||
msgData.OwnerFrame = ++RpcId;
|
||||
Guid objectId = item.ObjectId;
|
||||
Guid parentId = item.ParentId;
|
||||
{
|
||||
// Remap local client object ids into server ids
|
||||
IdsRemappingTable.KeyOf(objectId, &objectId);
|
||||
IdsRemappingTable.KeyOf(parentId, &parentId);
|
||||
}
|
||||
NetworkMessage msg = peer->BeginSendMessage();
|
||||
msg.WriteStructure(msgData);
|
||||
msg.WriteNetworkId(objectId);
|
||||
msg.WriteNetworkId(parentId);
|
||||
msg.WriteNetworkName(obj->GetType().Fullname);
|
||||
msg.WriteNetworkName(e.Name.First.GetType().Fullname);
|
||||
msg.WriteNetworkName(e.Name.Second);
|
||||
NetworkChannelType channel = (NetworkChannelType)e.Info.Channel;
|
||||
SendInParts(peer, channel, e.ArgsData.Get(), e.ArgsData.Length(), msg, e.Name, toServer, objectId, msgData.OwnerFrame, NetworkMessageIDs::ObjectRpcPart);
|
||||
}
|
||||
|
||||
void DeleteNetworkObject(ScriptingObject* obj)
|
||||
{
|
||||
// Remove from the mapping table
|
||||
@@ -708,38 +935,43 @@ FORCE_INLINE void DirtyObjectImpl(NetworkReplicatedObject& item, ScriptingObject
|
||||
Hierarchy->DirtyObject(obj);
|
||||
}
|
||||
|
||||
ReplicateItem* AddObjectReplicateItem(NetworkEvent& event, uint32 ownerFrame, uint16 partsCount, uint16 dataSize, const Guid& objectId, uint16 partStart, uint16 partSize, uint32 senderClientId)
|
||||
PartsItem* AddPartsItem(Array<PartsItem>& items, NetworkEvent& event, uint32 ownerFrame, uint16 partsCount, uint16 dataSize, const Guid& objectId, uint16 partStart, uint16 partSize, uint32 senderClientId)
|
||||
{
|
||||
// Reuse or add part item
|
||||
ReplicateItem* replicateItem = nullptr;
|
||||
for (auto& e : ReplicationParts)
|
||||
PartsItem* item = nullptr;
|
||||
for (auto& e : items)
|
||||
{
|
||||
if (e.OwnerFrame == ownerFrame && e.Data.Count() == dataSize && e.ObjectId == objectId)
|
||||
{
|
||||
// Reuse
|
||||
replicateItem = &e;
|
||||
item = &e;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!replicateItem)
|
||||
if (!item)
|
||||
{
|
||||
// Add
|
||||
replicateItem = &ReplicationParts.AddOne();
|
||||
replicateItem->ObjectId = objectId;
|
||||
replicateItem->PartsLeft = partsCount;
|
||||
replicateItem->OwnerFrame = ownerFrame;
|
||||
replicateItem->OwnerClientId = senderClientId;
|
||||
replicateItem->Data.Resize(dataSize);
|
||||
item = &items.AddOne();
|
||||
item->ObjectId = objectId;
|
||||
item->PartsLeft = partsCount;
|
||||
item->OwnerFrame = ownerFrame;
|
||||
item->OwnerClientId = senderClientId;
|
||||
item->Data.Resize(dataSize);
|
||||
}
|
||||
|
||||
// Copy part data
|
||||
ASSERT(replicateItem->PartsLeft > 0);
|
||||
replicateItem->PartsLeft--;
|
||||
ASSERT(partStart + partSize <= replicateItem->Data.Count());
|
||||
ASSERT(item->PartsLeft > 0);
|
||||
item->PartsLeft--;
|
||||
ASSERT(partStart + partSize <= item->Data.Count());
|
||||
const void* partData = event.Message.SkipBytes(partSize);
|
||||
Platform::MemoryCopy(replicateItem->Data.Get() + partStart, partData, partSize);
|
||||
Platform::MemoryCopy(item->Data.Get() + partStart, partData, partSize);
|
||||
|
||||
return replicateItem;
|
||||
return item;
|
||||
}
|
||||
|
||||
FORCE_INLINE PartsItem* AddObjectReplicateItem(NetworkEvent& event, uint32 ownerFrame, uint16 partsCount, uint16 dataSize, const Guid& objectId, uint16 partStart, uint16 partSize, uint32 senderClientId)
|
||||
{
|
||||
return AddPartsItem(ReplicationParts, event, ownerFrame, partsCount, dataSize, objectId, partStart, partSize, senderClientId);
|
||||
}
|
||||
|
||||
void InvokeObjectReplication(NetworkReplicatedObject& item, uint32 ownerFrame, byte* data, uint32 dataSize, uint32 senderClientId)
|
||||
@@ -787,6 +1019,24 @@ void InvokeObjectReplication(NetworkReplicatedObject& item, uint32 ownerFrame, b
|
||||
DirtyObjectImpl(item, obj);
|
||||
}
|
||||
|
||||
FORCE_INLINE PartsItem* AddObjectRpcItem(NetworkEvent& event, uint32 ownerFrame, uint16 partsCount, uint16 dataSize, const Guid& objectId, uint16 partStart, uint16 partSize, uint32 senderClientId)
|
||||
{
|
||||
return AddPartsItem(RpcParts, event, ownerFrame, partsCount, dataSize, objectId, partStart, partSize, senderClientId);
|
||||
}
|
||||
|
||||
void InvokeObjectRpc(const NetworkRpcInfo* info, byte* data, uint32 dataSize, uint32 senderClientId, ScriptingObject* obj)
|
||||
{
|
||||
// Setup message reading stream
|
||||
if (CachedReadStream == nullptr)
|
||||
CachedReadStream = New<NetworkStream>();
|
||||
NetworkStream* stream = CachedReadStream;
|
||||
stream->SenderId = senderClientId;
|
||||
stream->Initialize(data, dataSize);
|
||||
|
||||
// Execute RPC
|
||||
info->Execute(obj, stream, info->Tag);
|
||||
}
|
||||
|
||||
void InvokeObjectSpawn(const NetworkMessageObjectSpawn& msgData, const Guid& prefabId, const NetworkMessageObjectSpawnItem* msgDataItems)
|
||||
{
|
||||
ScopeLock lock(ObjectsLock);
|
||||
@@ -1652,9 +1902,6 @@ void NetworkInternal::NetworkReplicatorUpdate()
|
||||
if (Objects.Count() == 0)
|
||||
return;
|
||||
const bool isClient = NetworkManager::IsClient();
|
||||
const bool isServer = NetworkManager::IsServer();
|
||||
const bool isHost = NetworkManager::IsHost();
|
||||
NetworkPeer* peer = NetworkManager::Peer;
|
||||
|
||||
if (!isClient && NewClients.Count() != 0)
|
||||
{
|
||||
@@ -1694,22 +1941,7 @@ void NetworkInternal::NetworkReplicatorUpdate()
|
||||
PROFILE_CPU_NAMED("DespawnQueue");
|
||||
for (DespawnItem& e : DespawnQueue)
|
||||
{
|
||||
// Send despawn message
|
||||
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Despawn object ID={}", e.Id.ToString());
|
||||
NetworkMessageObjectDespawn msgData;
|
||||
Guid objectId = e.Id;
|
||||
{
|
||||
// Remap local client object ids into server ids
|
||||
IdsRemappingTable.KeyOf(objectId, &objectId);
|
||||
}
|
||||
NetworkMessage msg = peer->BeginSendMessage();
|
||||
msg.WriteStructure(msgData);
|
||||
msg.WriteNetworkId(objectId);
|
||||
BuildCachedTargets(NetworkManager::Clients, e.Targets);
|
||||
if (isClient)
|
||||
peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg);
|
||||
else
|
||||
peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg, CachedTargets);
|
||||
SendDespawn(e);
|
||||
}
|
||||
DespawnQueue.Clear();
|
||||
}
|
||||
@@ -1830,6 +2062,7 @@ void NetworkInternal::NetworkReplicatorUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove items from RpcParts after some TTL to reduce memory usage
|
||||
// TODO: remove items from SpawnParts after some TTL to reduce memory usage
|
||||
|
||||
// Replicate all owned networked objects with other clients or server
|
||||
@@ -1871,136 +2104,11 @@ void NetworkInternal::NetworkReplicatorUpdate()
|
||||
PROFILE_CPU_NAMED("Replication");
|
||||
if (CachedWriteStream == nullptr)
|
||||
CachedWriteStream = New<NetworkStream>();
|
||||
NetworkStream* stream = CachedWriteStream;
|
||||
stream->SenderId = NetworkManager::LocalClientId;
|
||||
CachedWriteStream->SenderId = NetworkManager::LocalClientId;
|
||||
// TODO: use Job System when replicated objects count is large
|
||||
for (auto& e : CachedReplicationResult->_entries)
|
||||
{
|
||||
ScriptingObject* obj = e.Object;
|
||||
auto it = Objects.Find(obj->GetID());
|
||||
if (it.IsEnd())
|
||||
continue;
|
||||
auto& item = it->Item;
|
||||
|
||||
// Skip serialization of objects that none will receive
|
||||
if (!isClient)
|
||||
{
|
||||
BuildCachedTargets(item, e.TargetClients);
|
||||
if (CachedTargets.Count() == 0)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.AsNetworkObject)
|
||||
item.AsNetworkObject->OnNetworkSerialize();
|
||||
|
||||
// Serialize object
|
||||
stream->Initialize();
|
||||
const bool failed = NetworkReplicator::InvokeSerializer(obj->GetTypeHandle(), obj, stream, true);
|
||||
if (failed)
|
||||
{
|
||||
//NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Cannot serialize object {} of type {} (missing serialization logic)", item.ToString(), obj->GetType().ToString());
|
||||
continue;
|
||||
}
|
||||
const uint32 size = stream->GetPosition();
|
||||
if (size > MAX_uint16)
|
||||
{
|
||||
LOG(Error, "Too much data for object {} replication ({} bytes provided while limit is {}).", item.ToString(), size, MAX_uint16);
|
||||
continue;
|
||||
}
|
||||
|
||||
#if USE_NETWORK_REPLICATOR_CACHE
|
||||
// Process replication cache to skip sending object data if it didn't change
|
||||
if (item.RepCache.Data.Length() == size &&
|
||||
item.RepCache.Mask == e.TargetClients &&
|
||||
Platform::MemoryCompare(item.RepCache.Data.Get(), stream->GetBuffer(), size) == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
item.RepCache.Mask = e.TargetClients;
|
||||
item.RepCache.Data.Copy(stream->GetBuffer(), size);
|
||||
#endif
|
||||
// TODO: use Unreliable for dynamic objects that are replicated every frame? (eg. player state)
|
||||
constexpr NetworkChannelType repChannel = NetworkChannelType::Reliable;
|
||||
|
||||
// Send object to clients
|
||||
NetworkMessageObjectReplicate msgData;
|
||||
msgData.OwnerFrame = NetworkManager::Frame;
|
||||
Guid objectId = item.ObjectId, parentId = item.ParentId;
|
||||
{
|
||||
// Remap local client object ids into server ids
|
||||
IdsRemappingTable.KeyOf(objectId, &objectId);
|
||||
IdsRemappingTable.KeyOf(parentId, &parentId);
|
||||
}
|
||||
NetworkMessage msg = peer->BeginSendMessage();
|
||||
msg.WriteStructure(msgData);
|
||||
msg.WriteNetworkId(objectId);
|
||||
msg.WriteNetworkId(parentId);
|
||||
msg.WriteNetworkName(obj->GetType().Fullname);
|
||||
NetworkMessageObjectReplicatePayload msgDataPayload;
|
||||
msgDataPayload.DataSize = size;
|
||||
const uint32 networkKeyIdWorstCaseSize = sizeof(uint32) + sizeof(Guid);
|
||||
const uint32 msgMaxData = peer->Config.MessageSize - msg.Position - sizeof(NetworkMessageObjectReplicatePayload);
|
||||
const uint32 partMaxData = peer->Config.MessageSize - sizeof(NetworkMessageObjectReplicatePart) - networkKeyIdWorstCaseSize;
|
||||
uint32 partsCount = 1;
|
||||
uint32 dataStart = 0;
|
||||
uint32 msgDataSize = size;
|
||||
if (size > msgMaxData)
|
||||
{
|
||||
// Send msgMaxData within first message
|
||||
msgDataSize = msgMaxData;
|
||||
dataStart += msgMaxData;
|
||||
|
||||
// Send rest of the data in separate parts
|
||||
partsCount += Math::DivideAndRoundUp(size - dataStart, partMaxData);
|
||||
}
|
||||
else
|
||||
dataStart += size;
|
||||
ASSERT(partsCount <= MAX_uint8);
|
||||
msgDataPayload.PartsCount = partsCount;
|
||||
msgDataPayload.PartSize = msgDataSize;
|
||||
msg.WriteStructure(msgDataPayload);
|
||||
msg.WriteBytes(stream->GetBuffer(), msgDataSize);
|
||||
uint32 dataSize = msgDataSize, messageSize = msg.Length;
|
||||
if (isClient)
|
||||
peer->EndSendMessage(repChannel, msg);
|
||||
else
|
||||
peer->EndSendMessage(repChannel, msg, CachedTargets);
|
||||
|
||||
// Send all other parts
|
||||
for (uint32 partIndex = 1; partIndex < partsCount; partIndex++)
|
||||
{
|
||||
NetworkMessageObjectReplicatePart msgDataPart;
|
||||
msgDataPart.OwnerFrame = msgData.OwnerFrame;
|
||||
msgDataPart.DataSize = msgDataPayload.DataSize;
|
||||
msgDataPart.PartsCount = msgDataPayload.PartsCount;
|
||||
msgDataPart.PartStart = dataStart;
|
||||
msgDataPart.PartSize = Math::Min(size - dataStart, partMaxData);
|
||||
msg = peer->BeginSendMessage();
|
||||
msg.WriteStructure(msgDataPart);
|
||||
msg.WriteNetworkId(objectId);
|
||||
msg.WriteBytes(stream->GetBuffer() + msgDataPart.PartStart, msgDataPart.PartSize);
|
||||
messageSize += msg.Length;
|
||||
dataSize += msgDataPart.PartSize;
|
||||
dataStart += msgDataPart.PartSize;
|
||||
if (isClient)
|
||||
peer->EndSendMessage(repChannel, msg);
|
||||
else
|
||||
peer->EndSendMessage(repChannel, msg, CachedTargets);
|
||||
}
|
||||
ASSERT_LOW_LAYER(dataStart == size);
|
||||
|
||||
#if COMPILE_WITH_PROFILER
|
||||
// Network stats recording
|
||||
if (EnableProfiling)
|
||||
{
|
||||
const Pair<ScriptingTypeHandle, StringAnsiView> name(obj->GetTypeHandle(), StringAnsiView::Empty);
|
||||
auto& profileEvent = ProfilerEvents[name];
|
||||
profileEvent.Count++;
|
||||
profileEvent.DataSize += dataSize;
|
||||
profileEvent.MessageSize += messageSize;
|
||||
profileEvent.Receivers += isClient ? 1 : CachedTargets.Count();
|
||||
}
|
||||
#endif
|
||||
SendReplication(e.Object, e.TargetClients);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2009,70 +2117,7 @@ void NetworkInternal::NetworkReplicatorUpdate()
|
||||
PROFILE_CPU_NAMED("Rpc");
|
||||
for (auto& e : RpcQueue)
|
||||
{
|
||||
ScriptingObject* obj = e.Object.Get();
|
||||
if (!obj)
|
||||
continue;
|
||||
auto it = Objects.Find(obj->GetID());
|
||||
if (it == Objects.End())
|
||||
{
|
||||
#if USE_EDITOR || !BUILD_RELEASE
|
||||
if (!DespawnedObjects.Contains(obj->GetID()))
|
||||
LOG(Error, "Cannot invoke RPC method '{0}.{1}' on object '{2}' that is not registered in networking (use 'NetworkReplicator.AddObject').", e.Name.First.ToString(), String(e.Name.Second), obj->GetID());
|
||||
#endif
|
||||
continue;
|
||||
}
|
||||
auto& item = it->Item;
|
||||
|
||||
// Send RPC message
|
||||
//NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Rpc {}::{} object ID={}", e.Name.First.ToString(), String(e.Name.Second), item.ToString());
|
||||
NetworkMessageObjectRpc msgData;
|
||||
Guid msgObjectId = item.ObjectId;
|
||||
Guid msgParentId = item.ParentId;
|
||||
{
|
||||
// Remap local client object ids into server ids
|
||||
IdsRemappingTable.KeyOf(msgObjectId, &msgObjectId);
|
||||
IdsRemappingTable.KeyOf(msgParentId, &msgParentId);
|
||||
}
|
||||
msgData.ArgsSize = (uint16)e.ArgsData.Length();
|
||||
NetworkMessage msg = peer->BeginSendMessage();
|
||||
msg.WriteStructure(msgData);
|
||||
msg.WriteNetworkId(msgObjectId);
|
||||
msg.WriteNetworkId(msgParentId);
|
||||
msg.WriteNetworkName(obj->GetType().Fullname);
|
||||
msg.WriteNetworkName(e.Name.First.GetType().Fullname);
|
||||
msg.WriteNetworkName(e.Name.Second);
|
||||
msg.WriteBytes(e.ArgsData.Get(), e.ArgsData.Length());
|
||||
uint32 dataSize = e.ArgsData.Length(), messageSize = msg.Length, receivers = 0;
|
||||
NetworkChannelType channel = (NetworkChannelType)e.Info.Channel;
|
||||
if (e.Info.Server && isClient)
|
||||
{
|
||||
// Client -> Server
|
||||
#if USE_NETWORK_REPLICATOR_LOG
|
||||
if (e.Targets.Length() != 0)
|
||||
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Server RPC '{}::{}' called with non-empty list of targets is not supported (only server will receive it)", e.Name.First.ToString(), e.Name.Second.ToString());
|
||||
#endif
|
||||
peer->EndSendMessage(channel, msg);
|
||||
receivers = 1;
|
||||
}
|
||||
else if (e.Info.Client && (isServer || isHost))
|
||||
{
|
||||
// Server -> Client(s)
|
||||
BuildCachedTargets(NetworkManager::Clients, item.TargetClientIds, e.Targets, NetworkManager::LocalClientId);
|
||||
peer->EndSendMessage(channel, msg, CachedTargets);
|
||||
receivers = CachedTargets.Count();
|
||||
}
|
||||
|
||||
#if COMPILE_WITH_PROFILER
|
||||
// Network stats recording
|
||||
if (EnableProfiling && receivers)
|
||||
{
|
||||
auto& profileEvent = ProfilerEvents[e.Name];
|
||||
profileEvent.Count++;
|
||||
profileEvent.DataSize += dataSize;
|
||||
profileEvent.MessageSize += messageSize;
|
||||
profileEvent.Receivers += receivers;
|
||||
}
|
||||
#endif
|
||||
SendRpc(e);
|
||||
}
|
||||
RpcQueue.Clear();
|
||||
}
|
||||
@@ -2085,7 +2130,7 @@ void NetworkInternal::OnNetworkMessageObjectReplicate(NetworkEvent& event, Netwo
|
||||
{
|
||||
PROFILE_CPU();
|
||||
NetworkMessageObjectReplicate msgData;
|
||||
NetworkMessageObjectReplicatePayload msgDataPayload;
|
||||
NetworkMessageObjectPartPayload msgDataPayload;
|
||||
Guid objectId, parentId;
|
||||
StringAnsiView objectTypeName;
|
||||
event.Message.ReadStructure(msgData);
|
||||
@@ -2095,7 +2140,7 @@ void NetworkInternal::OnNetworkMessageObjectReplicate(NetworkEvent& event, Netwo
|
||||
event.Message.ReadStructure(msgDataPayload);
|
||||
ScopeLock lock(ObjectsLock);
|
||||
if (DespawnedObjects.Contains(objectId))
|
||||
return; // Skip replicating not-existing objects
|
||||
return; // Skip replicating non-existing objects
|
||||
NetworkReplicatedObject* e = ResolveObject(objectId, parentId, objectTypeName);
|
||||
if (!e)
|
||||
return;
|
||||
@@ -2114,7 +2159,7 @@ void NetworkInternal::OnNetworkMessageObjectReplicate(NetworkEvent& event, Netwo
|
||||
else
|
||||
{
|
||||
// Add to replication from multiple parts
|
||||
ReplicateItem* replicateItem = AddObjectReplicateItem(event, msgData.OwnerFrame, msgDataPayload.PartsCount, msgDataPayload.DataSize, objectId, 0, msgDataPayload.PartSize, senderClientId);
|
||||
PartsItem* replicateItem = AddObjectReplicateItem(event, msgData.OwnerFrame, msgDataPayload.PartsCount, msgDataPayload.DataSize, objectId, 0, msgDataPayload.PartSize, senderClientId);
|
||||
replicateItem->Object = e->Object;
|
||||
}
|
||||
}
|
||||
@@ -2122,13 +2167,13 @@ void NetworkInternal::OnNetworkMessageObjectReplicate(NetworkEvent& event, Netwo
|
||||
void NetworkInternal::OnNetworkMessageObjectReplicatePart(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer)
|
||||
{
|
||||
PROFILE_CPU();
|
||||
NetworkMessageObjectReplicatePart msgData;
|
||||
NetworkMessageObjectPart msgData;
|
||||
Guid objectId;
|
||||
event.Message.ReadStructure(msgData);
|
||||
event.Message.ReadNetworkId(objectId);
|
||||
ScopeLock lock(ObjectsLock);
|
||||
if (DespawnedObjects.Contains(objectId))
|
||||
return; // Skip replicating not-existing objects
|
||||
return; // Skip replicating non-existing objects
|
||||
|
||||
const uint32 senderClientId = client ? client->ClientId : NetworkManager::ServerClientId;
|
||||
AddObjectReplicateItem(event, msgData.OwnerFrame, msgData.PartsCount, msgData.DataSize, objectId, msgData.PartStart, msgData.PartSize, senderClientId);
|
||||
@@ -2288,14 +2333,16 @@ void NetworkInternal::OnNetworkMessageObjectRpc(NetworkEvent& event, NetworkClie
|
||||
{
|
||||
PROFILE_CPU();
|
||||
NetworkMessageObjectRpc msgData;
|
||||
Guid msgObjectId, msgParentId;
|
||||
NetworkMessageObjectPartPayload msgDataPayload;
|
||||
Guid objectId, parentId;
|
||||
StringAnsiView objectTypeName, rpcTypeName, rpcName;
|
||||
event.Message.ReadStructure(msgData);
|
||||
event.Message.ReadNetworkId(msgObjectId);
|
||||
event.Message.ReadNetworkId(msgParentId);
|
||||
event.Message.ReadNetworkId(objectId);
|
||||
event.Message.ReadNetworkId(parentId);
|
||||
event.Message.ReadNetworkName(objectTypeName);
|
||||
event.Message.ReadNetworkName(rpcTypeName);
|
||||
event.Message.ReadNetworkName(rpcName);
|
||||
event.Message.ReadStructure(msgDataPayload);
|
||||
ScopeLock lock(ObjectsLock);
|
||||
|
||||
// Find RPC info
|
||||
@@ -2305,11 +2352,11 @@ void NetworkInternal::OnNetworkMessageObjectRpc(NetworkEvent& event, NetworkClie
|
||||
const NetworkRpcInfo* info = NetworkRpcInfo::RPCsTable.TryGet(name);
|
||||
if (!info)
|
||||
{
|
||||
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Unknown RPC {}::{} for object {}", String(rpcTypeName), String(rpcName), msgObjectId);
|
||||
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Unknown RPC {}::{} for object {}", String(rpcTypeName), String(rpcName), objectId);
|
||||
return;
|
||||
}
|
||||
|
||||
NetworkReplicatedObject* e = ResolveObject(msgObjectId, msgParentId, objectTypeName);
|
||||
NetworkReplicatedObject* e = ResolveObject(objectId, parentId, objectTypeName);
|
||||
if (e)
|
||||
{
|
||||
auto& item = *e;
|
||||
@@ -2329,18 +2376,50 @@ void NetworkInternal::OnNetworkMessageObjectRpc(NetworkEvent& event, NetworkClie
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup message reading stream
|
||||
if (CachedReadStream == nullptr)
|
||||
CachedReadStream = New<NetworkStream>();
|
||||
NetworkStream* stream = CachedReadStream;
|
||||
stream->SenderId = client ? client->ClientId : NetworkManager::ServerClientId;
|
||||
stream->Initialize(event.Message.Buffer + event.Message.Position, msgData.ArgsSize);
|
||||
|
||||
// Execute RPC
|
||||
info->Execute(obj, stream, info->Tag);
|
||||
const uint32 senderClientId = client ? client->ClientId : NetworkManager::ServerClientId;
|
||||
if (msgDataPayload.PartsCount == 1)
|
||||
{
|
||||
// Call RPC
|
||||
InvokeObjectRpc(info, event.Message.Buffer + event.Message.Position, msgDataPayload.DataSize, senderClientId, obj);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add to RPC from multiple parts
|
||||
PartsItem* rpcItem = AddObjectRpcItem(event, msgData.OwnerFrame, msgDataPayload.PartsCount, msgDataPayload.DataSize, objectId, 0, msgDataPayload.PartSize, senderClientId);
|
||||
rpcItem->Object = e->Object;
|
||||
rpcItem->Tag = info;
|
||||
}
|
||||
}
|
||||
else if (info->Channel != static_cast<uint8>(NetworkChannelType::Unreliable) && info->Channel != static_cast<uint8>(NetworkChannelType::UnreliableOrdered))
|
||||
{
|
||||
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Unknown object {} RPC {}::{}", msgObjectId, String(rpcTypeName), String(rpcName));
|
||||
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Unknown object {} RPC {}::{}", objectId, String(rpcTypeName), String(rpcName));
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkInternal::OnNetworkMessageObjectRpcPart(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer)
|
||||
{
|
||||
PROFILE_CPU();
|
||||
NetworkMessageObjectPart msgData;
|
||||
Guid objectId;
|
||||
event.Message.ReadStructure(msgData);
|
||||
event.Message.ReadNetworkId(objectId);
|
||||
ScopeLock lock(ObjectsLock);
|
||||
if (DespawnedObjects.Contains(objectId))
|
||||
return; // Skip replicating non-existing objects
|
||||
|
||||
const uint32 senderClientId = client ? client->ClientId : NetworkManager::ServerClientId;
|
||||
PartsItem* rpcItem = AddObjectRpcItem(event, msgData.OwnerFrame, msgData.PartsCount, msgData.DataSize, objectId, msgData.PartStart, msgData.PartSize, senderClientId);
|
||||
if (rpcItem && rpcItem->PartsLeft == 0)
|
||||
{
|
||||
// Got all parts so invoke RPC
|
||||
ScriptingObject* obj = rpcItem->Object.Get();
|
||||
if (obj)
|
||||
{
|
||||
InvokeObjectRpc((const NetworkRpcInfo*)rpcItem->Tag, rpcItem->Data.Get(), rpcItem->Data.Count(), rpcItem->OwnerClientId, obj);
|
||||
}
|
||||
|
||||
// Remove item
|
||||
int32 partIndex = (int32)((RpcParts.Get() - rpcItem) / sizeof(rpcItem));
|
||||
RpcParts.RemoveAt(partIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
#include "Types.h"
|
||||
#include "Engine/Core/Types/StringView.h"
|
||||
#include "Engine/Core/Types/Guid.h"
|
||||
#if PLATFORM_ARCH_ARM64
|
||||
#include "Engine/Core/Core.h"
|
||||
#endif
|
||||
|
||||
class MMethod;
|
||||
class BinaryModule;
|
||||
|
||||
@@ -104,13 +104,20 @@ namespace FlaxEngine.GUI
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether enable drop down icon drawing.
|
||||
/// </summary>
|
||||
[EditorOrder(1)]
|
||||
[EditorOrder(2)]
|
||||
public bool EnableDropDownIcon { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get or sets a value indicating whether the panel can be opened or closed via the user interacting with the ui.
|
||||
/// Changing the open/ closed state from code or the Properties panel will still work regardless.
|
||||
/// </summary>
|
||||
[EditorOrder(1)]
|
||||
public bool CanOpenClose { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to enable containment line drawing,
|
||||
/// </summary>
|
||||
[EditorOrder(2)]
|
||||
[EditorOrder(3)]
|
||||
public bool EnableContainmentLines { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
@@ -369,7 +376,7 @@ namespace FlaxEngine.GUI
|
||||
}
|
||||
|
||||
// Header
|
||||
var color = _mouseOverHeader ? HeaderColorMouseOver : HeaderColor;
|
||||
var color = _mouseOverHeader && CanOpenClose ? HeaderColorMouseOver : HeaderColor;
|
||||
if (color.A > 0.0f)
|
||||
{
|
||||
Render2D.FillRectangle(new Rectangle(0, 0, Width, HeaderHeight), color);
|
||||
@@ -510,7 +517,7 @@ namespace FlaxEngine.GUI
|
||||
if (button == MouseButton.Left && _mouseButtonLeftDown)
|
||||
{
|
||||
_mouseButtonLeftDown = false;
|
||||
if (_mouseOverHeader)
|
||||
if (_mouseOverHeader && CanOpenClose)
|
||||
Toggle();
|
||||
return true;
|
||||
}
|
||||
@@ -540,7 +547,7 @@ namespace FlaxEngine.GUI
|
||||
if (button == MouseButton.Left && _mouseButtonLeftDown)
|
||||
{
|
||||
_mouseButtonLeftDown = false;
|
||||
if (_mouseOverHeader)
|
||||
if (_mouseOverHeader && CanOpenClose)
|
||||
Toggle();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ namespace FlaxEngine.GUI
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Draw()
|
||||
public override void DrawSelf()
|
||||
{
|
||||
var bounds = new Rectangle(Float2.Zero, Size);
|
||||
|
||||
@@ -205,21 +205,6 @@ namespace FlaxEngine.GUI
|
||||
Render2D.DrawTexture(buffer, bounds, color);
|
||||
else
|
||||
Render2D.FillRectangle(bounds, Color.Black);
|
||||
|
||||
// Push clipping mask
|
||||
if (ClipChildren)
|
||||
{
|
||||
GetDesireClientArea(out var clientArea);
|
||||
Render2D.PushClip(ref clientArea);
|
||||
}
|
||||
|
||||
DrawChildren();
|
||||
|
||||
// Pop clipping mask
|
||||
if (ClipChildren)
|
||||
{
|
||||
Render2D.PopClip();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -37,22 +37,22 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Ionic.Zip.Reduced">
|
||||
<HintPath>..\..\..\Source\Platforms\DotNet\Ionic.Zip.Reduced.dll</HintPath>
|
||||
<HintPath>..\..\Platforms\DotNet\Ionic.Zip.Reduced.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Text.Encoding.CodePages">
|
||||
<HintPath>..\..\..\Source\Platforms\DotNet\System.Text.Encoding.CodePages.dll</HintPath>
|
||||
<HintPath>..\..\Platforms\DotNet\System.Text.Encoding.CodePages.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Mono.Cecil">
|
||||
<HintPath>..\..\..\Source\Platforms\DotNet\Mono.Cecil.dll</HintPath>
|
||||
<HintPath>..\..\Platforms\DotNet\Mono.Cecil.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.VisualStudio.Setup.Configuration.Interop">
|
||||
<HintPath>..\..\..\Source\Platforms\DotNet\Microsoft.VisualStudio.Setup.Configuration.Interop.dll</HintPath>
|
||||
<HintPath>..\..\Platforms\DotNet\Microsoft.VisualStudio.Setup.Configuration.Interop.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.CodeAnalysis.CSharp">
|
||||
<HintPath>..\..\..\Source\Platforms\DotNet\Microsoft.CodeAnalysis.CSharp.dll</HintPath>
|
||||
<HintPath>..\..\Platforms\DotNet\Microsoft.CodeAnalysis.CSharp.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.CodeAnalysis">
|
||||
<HintPath>..\..\..\Source\Platforms\DotNet\Microsoft.CodeAnalysis.dll</HintPath>
|
||||
<HintPath>..\..\Platforms\DotNet\Microsoft.CodeAnalysis.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Flax.Build
|
||||
/// <summary>
|
||||
/// Specifies the minimum CPU architecture type to support (on x86/x64).
|
||||
/// </summary>
|
||||
[CommandLine("winCpuArch", "<arch>", "Specifies the minimum CPU architecture type to support (om x86/x64).")]
|
||||
[CommandLine("winCpuArch", "<arch>", "Specifies the minimum CPU architecture type to support (on x86/x64).")]
|
||||
public static CpuArchitecture WindowsCpuArch = CpuArchitecture.SSE4_2; // 99.78% support on PC according to Steam Hardware & Software Survey: September 2025 (https://store.steampowered.com/hwsurvey/)
|
||||
}
|
||||
}
|
||||
@@ -76,22 +76,27 @@ namespace Flax.Build.Platforms
|
||||
options.LinkEnv.InputLibraries.Add("oleaut32.lib");
|
||||
options.LinkEnv.InputLibraries.Add("delayimp.lib");
|
||||
|
||||
if (options.Architecture == TargetArchitecture.ARM64)
|
||||
options.CompileEnv.CpuArchitecture = Configuration.WindowsCpuArch;
|
||||
|
||||
if (options.Architecture == TargetArchitecture.x64)
|
||||
{
|
||||
if (_minVersion.Major <= 7 && options.CompileEnv.CpuArchitecture == CpuArchitecture.AVX2)
|
||||
{
|
||||
// Old Windows had lower support ratio for latest CPU features
|
||||
options.CompileEnv.CpuArchitecture = CpuArchitecture.AVX;
|
||||
}
|
||||
if (_minVersion.Major >= 11 && options.CompileEnv.CpuArchitecture == CpuArchitecture.AVX)
|
||||
{
|
||||
// Windows 11 has hard requirement on SSE4.2
|
||||
options.CompileEnv.CpuArchitecture = CpuArchitecture.SSE4_2;
|
||||
}
|
||||
}
|
||||
else if (options.Architecture == TargetArchitecture.ARM64)
|
||||
{
|
||||
options.CompileEnv.PreprocessorDefinitions.Add("USE_SOFT_INTRINSICS");
|
||||
options.LinkEnv.InputLibraries.Add("softintrin.lib");
|
||||
}
|
||||
|
||||
options.CompileEnv.CpuArchitecture = Configuration.WindowsCpuArch;
|
||||
if (_minVersion.Major <= 7 && options.CompileEnv.CpuArchitecture == CpuArchitecture.AVX2)
|
||||
{
|
||||
// Old Windows had lower support ratio for latest CPU features
|
||||
options.CompileEnv.CpuArchitecture = CpuArchitecture.AVX;
|
||||
}
|
||||
if (_minVersion.Major >= 11 && options.CompileEnv.CpuArchitecture == CpuArchitecture.AVX)
|
||||
{
|
||||
// Windows 11 has hard requirement on SSE4.2
|
||||
options.CompileEnv.CpuArchitecture = CpuArchitecture.SSE4_2;
|
||||
if (options.CompileEnv.CpuArchitecture != CpuArchitecture.None)
|
||||
options.CompileEnv.CpuArchitecture = CpuArchitecture.NEON;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user