Merge remote-tracking branch 'origin/master' into sdl_platform

This commit is contained in:
2025-08-24 09:44:28 +03:00
99 changed files with 1300 additions and 1136 deletions

BIN
Content/Editor/Camera/M_Camera.flax (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
Content/Editor/DefaultFontMaterial.flax (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

BIN
Content/Editor/Gizmo/Material.flax (Stored with Git LFS)

Binary file not shown.

BIN
Content/Editor/Gizmo/MaterialWire.flax (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
Content/Editor/Highlight Material.flax (Stored with Git LFS)

Binary file not shown.

BIN
Content/Editor/Icons/IconsMaterial.flax (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

View File

@@ -13,7 +13,7 @@
META_CB_BEGIN(0, Data)
float4x4 WorldMatrix;
float4x4 InvWorld;
float4x4 SVPositionToWorld;
float4x4 SvPositionToWorld;
@1META_CB_END
// Use depth buffer for per-pixel decal layering
@@ -27,12 +27,63 @@ struct MaterialInput
float3 WorldPosition;
float TwoSidedSign;
float2 TexCoord;
float4 TexCoord_DDX_DDY;
float3x3 TBN;
float4 SvPosition;
float3 PreSkinnedPosition;
float3 PreSkinnedNormal;
};
// Calculates decal texcoords for a given pixel position (sampels depth buffer and projects value to decal space).
float2 SvPositionToDecalUV(float4 svPosition)
{
float2 screenUV = svPosition.xy * ScreenSize.zw;
svPosition.z = SAMPLE_RT(DepthBuffer, screenUV).r;
float4 positionHS = mul(float4(svPosition.xyz, 1), SvPositionToWorld);
float3 positionWS = positionHS.xyz / positionHS.w;
float3 positionOS = mul(float4(positionWS, 1), InvWorld).xyz;
return positionOS.xz + 0.5f;
}
// Manually compute ddx/ddy for decal texture cooordinates to avoid the 2x2 pixels artifacts on the edges of geometry under decal
// [Reference: https://www.humus.name/index.php?page=3D&ID=84]
float4 CalculateTextureDerivatives(float4 svPosition, float2 texCoord)
{
float4 svDiffX = float4(1, 0, 0, 0);
float2 uvDiffX0 = texCoord - SvPositionToDecalUV(svPosition - svDiffX);
float2 uvDiffX1 = SvPositionToDecalUV(svPosition + svDiffX) - texCoord;
float2 dx = dot(uvDiffX0, uvDiffX0) < dot(uvDiffX1, uvDiffX1) ? uvDiffX0 : uvDiffX1;
float4 svDiffY = float4(0, 1, 0, 0);
float2 uvDiffY0 = texCoord - SvPositionToDecalUV(svPosition - svDiffY);
float2 uvDiffY1 = SvPositionToDecalUV(svPosition + svDiffY) - texCoord;
float2 dy = dot(uvDiffY0, uvDiffY0) < dot(uvDiffY1, uvDiffY1) ? uvDiffY0 : uvDiffY1;
return float4(dx, dy);
}
// Computes the mipmap level for a specific texture dimensions to be sampled at decal texture cooordinates.
// [Reference: https://hugi.scene.org/online/coding/hugi%2014%20-%20comipmap.htm]
float CalculateTextureMipmap(MaterialInput input, float2 textureSize)
{
float2 dx = input.TexCoord_DDX_DDY.xy * textureSize;
float2 dy = input.TexCoord_DDX_DDY.zw * textureSize;
float d = max(dot(dx, dx), dot(dy, dy));
return (0.5 * 0.5) * log2(d); // Hardcoded half-mip rate reduction to avoid artifacts when decal is moved over dither texture
}
float CalculateTextureMipmap(MaterialInput input, Texture2D t)
{
float2 textureSize;
t.GetDimensions(textureSize.x, textureSize.y);
return CalculateTextureMipmap(input, textureSize);
}
float CalculateTextureMipmap(MaterialInput input, TextureCube t)
{
float2 textureSize;
t.GetDimensions(textureSize.x, textureSize.y);
return CalculateTextureMipmap(input, textureSize);
}
// Transforms a vector from tangent space to world space
float3 TransformTangentVectorToWorld(MaterialInput input, float3 tangentVector)
{
@@ -116,7 +167,6 @@ Material GetMaterialPS(MaterialInput input)
}
// Input macro specified by the material: DECAL_BLEND_MODE
#define DECAL_BLEND_MODE_TRANSLUCENT 0
#define DECAL_BLEND_MODE_STAIN 1
#define DECAL_BLEND_MODE_NORMAL 2
@@ -153,7 +203,7 @@ void PS_Decal(
float2 screenUV = SvPosition.xy * ScreenSize.zw;
SvPosition.z = SAMPLE_RT(DepthBuffer, screenUV).r;
float4 positionHS = mul(float4(SvPosition.xyz, 1), SVPositionToWorld);
float4 positionHS = mul(float4(SvPosition.xyz, 1), SvPositionToWorld);
float3 positionWS = positionHS.xyz / positionHS.w;
float3 positionOS = mul(float4(positionWS, 1), InvWorld).xyz;
@@ -166,8 +216,9 @@ void PS_Decal(
materialInput.TexCoord = decalUVs;
materialInput.TwoSidedSign = 1;
materialInput.SvPosition = SvPosition;
// Build tangent to world transformation matrix
materialInput.TexCoord_DDX_DDY = CalculateTextureDerivatives(materialInput.SvPosition, materialInput.TexCoord);
// Calculate tangent-space
float3 ddxWp = ddx(positionWS);
float3 ddyWp = ddy(positionWS);
materialInput.TBN[0] = normalize(ddyWp);

View File

@@ -20,6 +20,8 @@ float TimeParam;
float4 ViewInfo;
float4 ScreenSize;
float4 ViewSize;
float3 ViewPadding0;
float UnscaledTimeParam;
@1META_CB_END
// Shader resources

View File

@@ -19,6 +19,8 @@ float4 ViewInfo;
float4 ScreenSize;
float4 TemporalAAJitter;
float4x4 InverseViewProjectionMatrix;
float3 ViewPadding0;
float UnscaledTimeParam;
@1META_CB_END
// Shader resources

Binary file not shown.

Binary file not shown.

BIN
Content/Editor/SpriteMaterial.flax (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
Content/Editor/TexturePreviewMaterial.flax (Stored with Git LFS)

Binary file not shown.

BIN
Content/Editor/Wires Debug Material.flax (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

BIN
Content/Engine/DefaultMaterial.flax (Stored with Git LFS)

Binary file not shown.

BIN
Content/Engine/DefaultRadialMenu.flax (Stored with Git LFS)

Binary file not shown.

BIN
Content/Engine/DefaultTerrainMaterial.flax (Stored with Git LFS)

Binary file not shown.

BIN
Content/Engine/SingleColorMaterial.flax (Stored with Git LFS)

Binary file not shown.

BIN
Content/Engine/SkyboxMaterial.flax (Stored with Git LFS)

Binary file not shown.

BIN
Content/Shaders/SSAO.flax (Stored with Git LFS)

Binary file not shown.

View File

@@ -528,7 +528,11 @@ namespace FlaxEditor
var timeSinceLastSave = Time.UnscaledGameTime - _lastAutoSaveTimer;
var timeToNextSave = options.AutoSaveFrequency * 60.0f - timeSinceLastSave;
if (timeToNextSave <= 0.0f || _autoSaveNow)
if (timeToNextSave <= 0.0f && GetWindows().Any(x => x.GUI.Children.Any(c => c is GUI.ContextMenu.ContextMenuBase)))
{
// Skip aut-save if any context menu is opened to wait for user to end interaction
}
else if (timeToNextSave <= 0.0f || _autoSaveNow)
{
Log("Auto save");
_lastAutoSaveTimer = Time.UnscaledGameTime;

View File

@@ -514,20 +514,15 @@ namespace FlaxEditor.GUI
var items = ItemsPanel.Children;
for (int i = 0; i < items.Count; i++)
{
if (items[i] is Item item && item.Visible)
var currentItem = items[i];
if (currentItem is Item item && item.Visible)
result.Add(item);
}
if (_categoryPanels != null)
{
for (int i = 0; i < _categoryPanels.Count; i++)
else if (currentItem is DropPanel category && (!ignoreFoldedCategories || !category.IsClosed) && currentItem.Visible)
{
var category = _categoryPanels[i];
if (!category.Visible || (ignoreFoldedCategories && category is DropPanel panel && panel.IsClosed))
continue;
for (int j = 0; j < category.Children.Count; j++)
{
if (category.Children[j] is Item item2 && item2.Visible)
result.Add(item2);
if (category.Children[j] is Item categoryItem && categoryItem.Visible)
result.Add(categoryItem);
}
}
}
@@ -591,10 +586,6 @@ namespace FlaxEditor.GUI
var items = GetVisibleItems(!controlDown);
var focusedIndex = items.IndexOf(focusedItem);
// If the user hasn't selected anything yet and is holding control, focus first folded item
if (focusedIndex == -1 && controlDown)
focusedIndex = GetVisibleItems(true).Count - 1;
int delta = key == KeyboardKeys.ArrowDown ? -1 : 1;
int nextIndex = Mathf.Wrap(focusedIndex - delta, 0, items.Count - 1);
var nextItem = items[nextIndex];

View File

@@ -130,6 +130,10 @@ namespace FlaxEditor.GUI
/// <returns>Created popup.</returns>
public static RenamePopup Show(Control control, Rectangle area, string value, bool isMultiline)
{
// hardcoded flushing layout for tree controls
if (control is Tree.TreeNode treeNode && treeNode.ParentTree != null)
treeNode.ParentTree.FlushPendingPerformLayout();
// Calculate the control size in the window space to handle scaled controls
var upperLeft = control.PointToWindow(area.UpperLeft);
var bottomRight = control.PointToWindow(area.BottomRight);

View File

@@ -41,6 +41,7 @@ namespace FlaxEditor.GUI.Tree
private Margin _margin;
private bool _autoSize = true;
private bool _deferLayoutUpdate = false;
private TreeNode _lastSelectedNode;
/// <summary>
/// The TreeNode that is being dragged over. This could have a value when not dragging.
@@ -67,7 +68,7 @@ namespace FlaxEditor.GUI.Tree
/// Gets the first selected node or null.
/// </summary>
public TreeNode SelectedNode => Selection.Count > 0 ? Selection[0] : null;
/// <summary>
/// Allow nodes to Draw the root tree line.
/// </summary>
@@ -364,6 +365,19 @@ namespace FlaxEditor.GUI.Tree
BulkSelectUpdateExpanded(false);
}
/// <summary>
/// Flushes any pending layout perming action that has been delayed until next update to optimize performance of the complex tree hierarchy.
/// </summary>
public void FlushPendingPerformLayout()
{
if (_deferLayoutUpdate)
{
base.PerformLayout();
AfterDeferredLayout?.Invoke();
_deferLayoutUpdate = false;
}
}
/// <inheritdoc />
public override void PerformLayout(bool force = false)
{
@@ -378,25 +392,31 @@ namespace FlaxEditor.GUI.Tree
public override void Update(float deltaTime)
{
if (_deferLayoutUpdate)
{
base.PerformLayout();
AfterDeferredLayout?.Invoke();
_deferLayoutUpdate = false;
}
FlushPendingPerformLayout();
var window = Root;
bool shiftDown = window.GetKey(KeyboardKeys.Shift);
bool keyUpArrow = window.GetKey(KeyboardKeys.ArrowUp);
bool keyDownArrow = window.GetKey(KeyboardKeys.ArrowDown);
var node = SelectedNode;
// Use last selection for last selected node if sift is down
if (Selection.Count < 2)
_lastSelectedNode = null;
else if (shiftDown)
_lastSelectedNode ??= Selection[^1];
// Skip root to prevent blocking input
if (_lastSelectedNode != null && _lastSelectedNode.IsRoot)
_lastSelectedNode = null;
var node = _lastSelectedNode ?? SelectedNode;
// Check if has focus and if any node is focused and it isn't a root
if (ContainsFocus && node != null && node.AutoFocus)
{
var window = Root;
if (window.GetKeyDown(KeyboardKeys.ArrowUp) || window.GetKeyDown(KeyboardKeys.ArrowDown))
_keyUpdateTime = KeyUpdateTimeout;
if (_keyUpdateTime >= KeyUpdateTimeout && window is WindowRootControl windowRoot && windowRoot.Window.IsFocused)
{
bool keyUpArrow = window.GetKey(KeyboardKeys.ArrowUp);
bool keyDownArrow = window.GetKey(KeyboardKeys.ArrowDown);
// Check if arrow flags are different
if (keyDownArrow != keyUpArrow)
{
@@ -406,24 +426,38 @@ namespace FlaxEditor.GUI.Tree
Assert.AreNotEqual(-1, myIndex);
// Up
TreeNode toSelect = null;
List<TreeNode> toSelect = new List<TreeNode>();
if (shiftDown && _supportMultiSelect)
{
toSelect.AddRange(Selection);
}
if (keyUpArrow)
{
if (myIndex == 0)
{
// Select parent
toSelect = parentNode;
if (toSelect.Contains(parentNode))
toSelect.Remove(node);
else if (parentNode != null)
toSelect.Add(parentNode);
_lastSelectedNode = parentNode;
}
else
{
// Select previous parent child
toSelect = nodeParent.GetChild(myIndex - 1) as TreeNode;
var select = nodeParent.GetChild(myIndex - 1) as TreeNode;
// Select last child if is valid and expanded and has any children
if (toSelect != null && toSelect.IsExpanded && toSelect.HasAnyVisibleChild)
if (select != null && select.IsExpanded && select.HasAnyVisibleChild)
{
toSelect = toSelect.GetChild(toSelect.ChildrenCount - 1) as TreeNode;
select = select.GetChild(select.ChildrenCount - 1) as TreeNode;
}
if (select == null || toSelect.Contains(select))
toSelect.Remove(node);
else
toSelect.Add(select);
_lastSelectedNode = select;
}
}
// Down
@@ -432,32 +466,48 @@ namespace FlaxEditor.GUI.Tree
if (node.IsExpanded && node.HasAnyVisibleChild)
{
// Select the first child
toSelect = node.GetChild(0) as TreeNode;
var select = node.GetChild(0) as TreeNode;
if (select == null || toSelect.Contains(select))
toSelect.Remove(node);
else
toSelect.Add(select);
_lastSelectedNode = select;
}
else if (myIndex == nodeParent.ChildrenCount - 1)
{
// Select next node after parent
while (parentNode != null && toSelect == null)
TreeNode select = null;
while (parentNode != null && select == null)
{
int parentIndex = parentNode.IndexInParent;
if (parentIndex != -1 && parentIndex < parentNode.Parent.ChildrenCount - 1)
{
toSelect = parentNode.Parent.GetChild(parentIndex + 1) as TreeNode;
select = parentNode.Parent.GetChild(parentIndex + 1) as TreeNode;
}
parentNode = parentNode.Parent as TreeNode;
}
if (select == null || toSelect.Contains(select))
toSelect.Remove(node);
else
toSelect.Add(select);
_lastSelectedNode = select;
}
else
{
// Select next parent child
toSelect = nodeParent.GetChild(myIndex + 1) as TreeNode;
var select = nodeParent.GetChild(myIndex + 1) as TreeNode;
if (select == null || toSelect.Contains(select))
toSelect.Remove(node);
else
toSelect.Add(select);
_lastSelectedNode = select;
}
}
if (toSelect != null && toSelect.AutoFocus)
if (toSelect.Count > 0)
{
// Select
Select(toSelect);
toSelect.Focus();
_lastSelectedNode?.Focus();
}
// Reset time

View File

@@ -11,6 +11,7 @@
#include "Engine/Scripting/ManagedCLR/MClass.h"
#include "Engine/Scripting/ManagedCLR/MException.h"
#include "Engine/Scripting/Internal/MainThreadManagedInvokeAction.h"
#include "Engine/Platform/WindowsManager.h"
#include "Engine/Content/Assets/VisualScript.h"
#include "Engine/Content/Content.h"
#include "Engine/CSG/CSGBuilder.h"
@@ -622,6 +623,14 @@ void ManagedEditor::WipeOutLeftoverSceneObjects()
ObjectsRemovalService::Flush();
}
Array<Window*> ManagedEditor::GetWindows()
{
WindowsManager::WindowsLocker.Lock();
auto result = WindowsManager::Windows;
WindowsManager::WindowsLocker.Unlock();
return result;
}
void ManagedEditor::OnEditorAssemblyLoaded(MAssembly* assembly)
{
ASSERT(!HasManagedInstance());

View File

@@ -259,6 +259,7 @@ public:
API_FUNCTION(Internal) static Array<VisualScriptLocal> GetVisualScriptLocals();
API_FUNCTION(Internal) static bool EvaluateVisualScriptLocal(VisualScript* script, API_PARAM(Ref) VisualScriptLocal& local);
API_FUNCTION(Internal) static void WipeOutLeftoverSceneObjects();
API_FUNCTION(Internal) static Array<Window*> GetWindows();
private:
void OnEditorAssemblyLoaded(MAssembly* assembly);

View File

@@ -652,43 +652,47 @@ namespace FlaxEditor.Options
#endregion
#region Node editors
#region Node Editors
[DefaultValue(typeof(InputBinding), "Shift+W")]
[EditorDisplay("Node editors"), EditorOrder(4500)]
[EditorDisplay("Node Editors"), EditorOrder(4500)]
public InputBinding NodesAlignTop = new InputBinding(KeyboardKeys.W, KeyboardKeys.Shift);
[DefaultValue(typeof(InputBinding), "Shift+A")]
[EditorDisplay("Node editors"), EditorOrder(4510)]
[EditorDisplay("Node Editors"), EditorOrder(4510)]
public InputBinding NodesAlignLeft = new InputBinding(KeyboardKeys.A, KeyboardKeys.Shift);
[DefaultValue(typeof(InputBinding), "Shift+S")]
[EditorDisplay("Node editors"), EditorOrder(4520)]
[EditorDisplay("Node Editors"), EditorOrder(4520)]
public InputBinding NodesAlignBottom = new InputBinding(KeyboardKeys.S, KeyboardKeys.Shift);
[DefaultValue(typeof(InputBinding), "Shift+D")]
[EditorDisplay("Node editors"), EditorOrder(4530)]
[EditorDisplay("Node Editors"), EditorOrder(4530)]
public InputBinding NodesAlignRight = new InputBinding(KeyboardKeys.D, KeyboardKeys.Shift);
[DefaultValue(typeof(InputBinding), "Alt+Shift+W")]
[EditorDisplay("Node editors"), EditorOrder(4540)]
[EditorDisplay("Node Editors"), EditorOrder(4540)]
public InputBinding NodesAlignMiddle = new InputBinding(KeyboardKeys.W, KeyboardKeys.Shift, KeyboardKeys.Alt);
[DefaultValue(typeof(InputBinding), "Alt+Shift+S")]
[EditorDisplay("Node editors"), EditorOrder(4550)]
[EditorDisplay("Node Editors"), EditorOrder(4550)]
public InputBinding NodesAlignCenter = new InputBinding(KeyboardKeys.S, KeyboardKeys.Shift, KeyboardKeys.Alt);
[DefaultValue(typeof(InputBinding), "Q")]
[EditorDisplay("Node editors"), EditorOrder(4560)]
[EditorDisplay("Node Editors"), EditorOrder(4560)]
public InputBinding NodesAutoFormat = new InputBinding(KeyboardKeys.Q);
[DefaultValue(typeof(InputBinding), "None")]
[EditorDisplay("Node editors"), EditorOrder(4570)]
public InputBinding NodesDistributeHorizontal = new InputBinding(KeyboardKeys.None);
[DefaultValue(typeof(InputBinding), "Shift+Q")]
[EditorDisplay("Node Editors"), EditorOrder(4560)]
public InputBinding NodesStraightenConnections = new InputBinding(KeyboardKeys.Q, KeyboardKeys.Shift);
[DefaultValue(typeof(InputBinding), "None")]
[EditorDisplay("Node editors"), EditorOrder(4580)]
public InputBinding NodesDistributeVertical = new InputBinding(KeyboardKeys.None);
[DefaultValue(typeof(InputBinding), "Alt+W")]
[EditorDisplay("Node Editors"), EditorOrder(4570)]
public InputBinding NodesDistributeHorizontal = new InputBinding(KeyboardKeys.W, KeyboardKeys.Alt);
[DefaultValue(typeof(InputBinding), "Alt+A")]
[EditorDisplay("Node Editors"), EditorOrder(4580)]
public InputBinding NodesDistributeVertical = new InputBinding(KeyboardKeys.A, KeyboardKeys.Alt);
#endregion
}

View File

@@ -446,6 +446,12 @@ namespace FlaxEditor.Options
[DefaultValue(1), Range(1, 4)]
[EditorDisplay("Cook & Run"), EditorOrder(600)]
public int NumberOfGameClientsToLaunch = 1;
/// <summary>
/// Gets or sets the build configuration to use when using Cook and Run option in the editor.
/// </summary>
[EditorDisplay("Cook & Run"), EditorOrder(601), ExpandGroups, Tooltip("The build configuration to use when using Cook and Run option in the editor.")]
public BuildConfiguration CookAndRunBuildConfiguration { get; set; } = BuildConfiguration.Development;
/// <summary>
/// Gets or sets the curvature of the line connecting to connected visject nodes.

View File

@@ -810,7 +810,7 @@ namespace FlaxEditor.Surface.Archetypes
Create = (id, context, arch, groupArch) => new StateMachineState(id, context, arch, groupArch),
Title = "State",
Description = "The animation states machine state node",
Flags = NodeFlags.AnimGraph | NodeFlags.NoSpawnViaGUI | NodeFlags.NoSpawnViaPaste,
Flags = NodeFlags.AnimGraph | NodeFlags.NoSpawnViaGUI,
Size = new Float2(100, 0),
DefaultValues = new object[]
{

View File

@@ -199,7 +199,7 @@ namespace FlaxEditor.Surface.Archetypes
private Label _labelValue;
private FloatValueBox _timeValue;
private ColorValueBox _colorValue;
private const int MaxStops = 8;
private const int MaxStops = 12;
/// <inheritdoc />
public ColorGradientNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
@@ -1386,10 +1386,11 @@ namespace FlaxEditor.Surface.Archetypes
Title = "Time",
Description = "Game time constant",
Flags = NodeFlags.MaterialGraph,
Size = new Float2(110, 20),
Size = new Float2(110, 40),
Elements = new[]
{
NodeElementArchetype.Factory.Output(0, "", typeof(float), 0),
NodeElementArchetype.Factory.Output(0, "Time", typeof(float), 0),
NodeElementArchetype.Factory.Output(1, "Unscaled Time", typeof(float), 1),
}
},
new NodeArchetype
@@ -1506,7 +1507,11 @@ namespace FlaxEditor.Surface.Archetypes
0.95f,
Color.White,
// Empty stops 2-7
// Empty stops 2-11
0.0f, Color.Black,
0.0f, Color.Black,
0.0f, Color.Black,
0.0f, Color.Black,
0.0f, Color.Black,
0.0f, Color.Black,
0.0f, Color.Black,

View File

@@ -5,39 +5,39 @@ using FlaxEngine;
namespace FlaxEditor.Surface
{
/// <summary>
/// Node Alignment type
/// Node Alignment type.
/// </summary>
[HideInEditor]
public enum NodeAlignmentType
{
/// <summary>
/// Align nodes vertically to top, matching top-most node
/// Align nodes vertically to top, matching top-most node.
/// </summary>
Top,
/// <summary>
/// Align nodes vertically to middle, using average of all nodes
/// Align nodes vertically to middle, using average of all nodes.
/// </summary>
Middle,
/// <summary>
/// Align nodes vertically to bottom, matching bottom-most node
/// Align nodes vertically to bottom, matching bottom-most node.
/// </summary>
Bottom,
/// <summary>
/// Align nodes horizontally to left, matching left-most node
/// Align nodes horizontally to left, matching left-most node.
/// </summary>
Left,
/// <summary>
/// Align nodes horizontally to center, using average of all nodes
/// Align nodes horizontally to center, using average of all nodes.
/// </summary>
Center,
/// <summary>
/// Align nodes horizontally to right, matching right-most node
/// Align nodes horizontally to right, matching right-most node.
/// </summary>
Right,
}
}
}

View File

@@ -28,7 +28,7 @@ namespace FlaxEditor.Surface
/// </summary>
/// <param name="scriptType">The input type to process.</param>
/// <param name="cache">Node groups cache that can be used for reusing groups for different nodes.</param>
/// <param name="version">The cache version number. Can be used to reject any cached data after <see cref="NodesCache"/> rebuilt.</param>
/// <param name="version">The cache version number. Can be used to reject any cached data after.<see cref="NodesCache"/> rebuilt.</param>
public delegate void IterateType(ScriptType scriptType, Dictionary<KeyValuePair<string, ushort>, GroupArchetype> cache, int version);
internal static readonly List<NodesCache> Caches = new List<NodesCache>(8);
@@ -412,6 +412,7 @@ namespace FlaxEditor.Surface
_cmFormatNodesMenu.Enabled = CanEdit && HasNodesSelection;
_cmFormatNodesConnectionButton = _cmFormatNodesMenu.ContextMenu.AddButton("Auto format", Editor.Instance.Options.Options.Input.NodesAutoFormat, () => { FormatGraph(SelectedNodes); });
_cmFormatNodesConnectionButton = _cmFormatNodesMenu.ContextMenu.AddButton("Straighten connections", Editor.Instance.Options.Options.Input.NodesStraightenConnections, () => { StraightenGraphConnections(SelectedNodes); });
_cmFormatNodesMenu.ContextMenu.AddSeparator();
_cmAlignNodesTopButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align top", Editor.Instance.Options.Options.Input.NodesAlignTop, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Top); });

View File

@@ -1,9 +1,9 @@
using FlaxEditor.Surface.Elements;
using FlaxEditor.Surface.Undo;
using FlaxEngine;
using System;
using System.Collections.Generic;
using System.Linq;
using FlaxEngine;
using FlaxEditor.Surface.Elements;
using FlaxEditor.Surface.Undo;
namespace FlaxEditor.Surface
{
@@ -14,26 +14,26 @@ namespace FlaxEditor.Surface
private class NodeFormattingData
{
/// <summary>
/// Starting from 0 at the main nodes
/// Starting from 0 at the main nodes.
/// </summary>
public int Layer;
/// <summary>
/// Position in the layer
/// Position in the layer.
/// </summary>
public int Offset;
/// <summary>
/// How far the subtree needs to be moved additionally
/// How far the subtree needs to be moved additionally.
/// </summary>
public int SubtreeOffset;
}
/// <summary>
/// Formats a graph where the nodes can be disjointed.
/// Uses the Sugiyama method
/// Uses the Sugiyama method.
/// </summary>
/// <param name="nodes">List of nodes</param>
/// <param name="nodes">List of nodes.</param>
public void FormatGraph(List<SurfaceNode> nodes)
{
if (nodes.Count <= 1)
@@ -78,9 +78,9 @@ namespace FlaxEditor.Surface
}
/// <summary>
/// Formats a graph where all nodes are connected
/// Formats a graph where all nodes are connected.
/// </summary>
/// <param name="nodes">List of connected nodes</param>
/// <param name="nodes">List of connected nodes.</param>
protected void FormatConnectedGraph(List<SurfaceNode> nodes)
{
if (nodes.Count <= 1)
@@ -160,11 +160,71 @@ namespace FlaxEditor.Surface
}
/// <summary>
/// Assigns a layer to every node
/// Straightens every connection between nodes in <paramref name="nodes"/>.
/// </summary>
/// <param name="nodeData">The exta node data</param>
/// <param name="endNodes">The end nodes</param>
/// <returns>The number of the maximum layer</returns>
/// <param name="nodes">List of nodes.</param>
public void StraightenGraphConnections(List<SurfaceNode> nodes)
{
if (nodes.Count <= 1)
return;
List<MoveNodesAction> undoActions = new List<MoveNodesAction>();
// Only process nodes that have any connection
List<SurfaceNode> connectedNodes = nodes.Where(n => n.GetBoxes().Any(b => b.HasAnyConnection)).ToList();
if (connectedNodes.Count == 0)
return;
for (int i = 0; i < connectedNodes.Count - 1; i++)
{
SurfaceNode nodeA = connectedNodes[i];
List<Box> connectedOutputBoxes = nodeA.GetBoxes().Where(b => b.IsOutput && b.HasAnyConnection).ToList();
for (int j = 0; j < connectedOutputBoxes.Count; j++)
{
Box boxA = connectedOutputBoxes[j];
for (int b = 0; b < boxA.Connections.Count; b++)
{
Box boxB = boxA.Connections[b];
// Ensure the other node is selected
if (!connectedNodes.Contains(boxB.ParentNode))
continue;
// Node with no outgoing connections reached. Advance to next node in list
if (boxA == null || boxB == null)
continue;
SurfaceNode nodeB = boxB.ParentNode;
// Calculate the Y offset needed for nodeB to align boxB's Y to boxA's Y
float boxASurfaceY = boxA.PointToParent(this, Float2.Zero).Y;
float boxBSurfaceY = boxB.PointToParent(this, Float2.Zero).Y;
float deltaY = (boxASurfaceY - boxBSurfaceY) / ViewScale;
Float2 delta = new Float2(0f, deltaY);
nodeB.Location += delta;
if (Undo != null)
undoActions.Add(new MoveNodesAction(Context, new[] { nodeB.ID }, delta));
}
}
}
if (undoActions.Count > 0)
Undo?.AddAction(new MultiUndoAction(undoActions, "Straightned "));
MarkAsEdited(false);
}
/// <summary>
/// Assigns a layer to every node.
/// </summary>
/// <param name="nodeData">The exta node data.</param>
/// <param name="endNodes">The end nodes.</param>
/// <returns>The number of the maximum layer.</returns>
private int SetLayers(Dictionary<SurfaceNode, NodeFormattingData> nodeData, List<SurfaceNode> endNodes)
{
// Longest path layering
@@ -201,12 +261,12 @@ namespace FlaxEditor.Surface
/// <summary>
/// Sets the node offsets
/// Sets the node offsets.
/// </summary>
/// <param name="nodeData">The exta node data</param>
/// <param name="endNodes">The end nodes</param>
/// <param name="maxLayer">The number of the maximum layer</param>
/// <returns>The number of the maximum offset</returns>
/// <param name="nodeData">The exta node data.</param>
/// <param name="endNodes">The end nodes.</param>
/// <param name="maxLayer">The number of the maximum layer.</param>
/// <returns>The number of the maximum offset.</returns>
private int SetOffsets(Dictionary<SurfaceNode, NodeFormattingData> nodeData, List<SurfaceNode> endNodes, int maxLayer)
{
int maxOffset = 0;
@@ -287,10 +347,10 @@ namespace FlaxEditor.Surface
/// Align given nodes on a graph using the given alignment type.
/// Ignores any potential overlap.
/// </summary>
/// <param name="nodes">List of nodes</param>
/// <param name="alignmentType">Alignemnt type</param>
/// <param name="nodes">List of nodes.</param>
/// <param name="alignmentType">Alignemnt type.</param>
public void AlignNodes(List<SurfaceNode> nodes, NodeAlignmentType alignmentType)
{
{
if(nodes.Count <= 1)
return;
@@ -328,8 +388,8 @@ namespace FlaxEditor.Surface
/// <summary>
/// Distribute the given nodes as equally as possible inside the bounding box, if no fit can be done it will use a default pad of 10 pixels between nodes.
/// </summary>
/// <param name="nodes">List of nodes</param>
/// <param name="vertically">If false will be done horizontally, if true will be done vertically</param>
/// <param name="nodes">List of nodes.</param>
/// <param name="vertically">If false will be done horizontally, if true will be done vertically.</param>
public void DistributeNodes(List<SurfaceNode> nodes, bool vertically)
{
if(nodes.Count <= 1)

View File

@@ -416,6 +416,7 @@ namespace FlaxEditor.Surface
new InputActionsContainer.Binding(options => options.Cut, Cut),
new InputActionsContainer.Binding(options => options.Duplicate, Duplicate),
new InputActionsContainer.Binding(options => options.NodesAutoFormat, () => { FormatGraph(SelectedNodes); }),
new InputActionsContainer.Binding(options => options.NodesStraightenConnections, () => { StraightenGraphConnections(SelectedNodes); }),
new InputActionsContainer.Binding(options => options.NodesAlignTop, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Top); }),
new InputActionsContainer.Binding(options => options.NodesAlignMiddle, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Middle); }),
new InputActionsContainer.Binding(options => options.NodesAlignBottom, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Bottom); }),

View File

@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Xml;
@@ -471,7 +470,7 @@ namespace FlaxEditor.Windows.Assets
private void OnOverrideMethodClicked()
{
var cm = new ContextMenu();
var cm = new ItemsListContextMenu(235);
var window = (VisualScriptWindow)Values[0];
var scriptMeta = window.Asset.Meta;
var baseType = TypeUtils.GetType(scriptMeta.BaseTypename);
@@ -499,27 +498,39 @@ namespace FlaxEditor.Windows.Assets
if (isAlreadyAdded)
continue;
var cmButton = cm.AddButton($"{name} (in {member.DeclaringType.Name})");
cmButton.TooltipText = Editor.Instance.CodeDocs.GetTooltip(member);
cmButton.Clicked += () =>
var item = new ItemsListContextMenu.Item
{
var surface = ((VisualScriptWindow)Values[0]).Surface;
var surfaceBounds = surface.AllNodesBounds;
surface.ShowArea(new Rectangle(surfaceBounds.BottomLeft, new Float2(200, 150)).MakeExpanded(400.0f));
var node = surface.Context.SpawnNode(16, 3, surfaceBounds.BottomLeft + new Float2(0, 50), new object[]
{
name,
parameters.Length,
Utils.GetEmptyArray<byte>()
});
surface.Select(node);
Name = $"{name} (in {member.DeclaringType.Name})",
TooltipText = Editor.Instance.CodeDocs.GetTooltip(member),
Tag = new object[] { name, parameters.Length, Utils.GetEmptyArray<byte>() },
// Do some basic sorting based on if the method is defined directly in the script base class
SortScore = member.DeclaringType == member.Type.ReflectedType ? 1 : 0,
};
cm.AddItem(item);
}
}
if (!cm.Items.Any())
cm.ItemClicked += (item) =>
{
cm.AddButton("Nothing to override");
var surface = ((VisualScriptWindow)Values[0]).Surface;
var surfaceBounds = surface.AllNodesBounds;
surface.ShowArea(new Rectangle(surfaceBounds.BottomLeft, new Float2(200, 150)).MakeExpanded(400.0f));
var node = surface.Context.SpawnNode(16, 3, surfaceBounds.BottomLeft + new Float2(0, 50), item.Tag as object[]);
surface.Select(node);
};
if (cm.ItemsPanel.ChildrenCount == 0)
{
var item = new ItemsListContextMenu.Item
{
Name = "Nothing to override"
};
item.Enabled = false;
cm.AddItem(item);
}
cm.SortItems();
cm.Show(_overrideButton, new Float2(0, _overrideButton.Height));
}
}

View File

@@ -335,12 +335,12 @@ namespace FlaxEditor.Windows
{
Parent = this,
};
toolstrip.AddButton("Clear", Clear).LinkTooltip("Clears all log entries");
toolstrip.AddButton("Clear", Clear).LinkTooltip("Clears all log entries.");
_clearOnPlayButton = (ToolStripButton)toolstrip.AddButton("Clear on Play", () =>
{
editor.Options.Options.Interface.DebugLogClearOnPlay = _clearOnPlayButton.Checked;
editor.Options.Apply(editor.Options.Options);
}).SetAutoCheck(true).LinkTooltip("Clears all log entries on enter playmode");
}).SetAutoCheck(true).LinkTooltip("Clears all log entries on enter playmode.");
_collapseLogsButton = (ToolStripButton)toolstrip.AddButton("Collapse", () =>
{
editor.Options.Options.Interface.DebugLogCollapse = _collapseLogsButton.Checked;
@@ -350,14 +350,14 @@ namespace FlaxEditor.Windows
{
editor.Options.Options.Interface.DebugLogPauseOnError = _pauseOnErrorButton.Checked;
editor.Options.Apply(editor.Options.Options);
}).SetAutoCheck(true).LinkTooltip("Performs auto pause on error");
}).SetAutoCheck(true).LinkTooltip("Performs auto pause on error.");
toolstrip.AddSeparator();
_groupButtons[0] = (ToolStripButton)toolstrip.AddButton(editor.Icons.Error32, () => { OnGroupButtonPressed(0); })
.SetAutoCheck(true).LinkTooltip("Shows/hides error messages");
.SetAutoCheck(true).LinkTooltip("Shows/hides error messages.");
_groupButtons[1] = (ToolStripButton)toolstrip.AddButton(editor.Icons.Warning32, () => { OnGroupButtonPressed(1); })
.SetAutoCheck(true).LinkTooltip("Shows/hides warning messages");
.SetAutoCheck(true).LinkTooltip("Shows/hides warning messages.");
_groupButtons[2] = (ToolStripButton)toolstrip.AddButton(editor.Icons.Info32, () => { OnGroupButtonPressed(2); })
.SetAutoCheck(true).LinkTooltip("Shows/hides info messages");
.SetAutoCheck(true).LinkTooltip("Shows/hides info messages.");
UpdateCount();
// Split panel
@@ -495,6 +495,7 @@ namespace FlaxEditor.Windows
// Pause on Error (we should do it as fast as possible)
if (newEntry.Group == LogGroup.Error && _pauseOnErrorButton.Checked && Editor.StateMachine.CurrentState == Editor.StateMachine.PlayingState)
{
Editor.Log("Pause Play mode on error (toggle this behaviour in the Debug Log panel)");
Editor.Simulation.RequestPausePlay();
}
}

View File

@@ -974,8 +974,9 @@ namespace FlaxEditor.Windows
public void BuildAndRun()
{
Editor.Log("Building and running");
GameCooker.GetCurrentPlatform(out var platform, out var buildPlatform, out var buildConfiguration);
GameCooker.GetCurrentPlatform(out var platform, out var buildPlatform, out _);
var numberOfClients = Editor.Options.Options.Interface.NumberOfGameClientsToLaunch;
var buildConfig = Editor.Options.Options.Interface.CookAndRunBuildConfiguration;
for (int i = 0; i < numberOfClients; i++)
{
var buildOptions = BuildOptions.AutoRun;
@@ -988,7 +989,7 @@ namespace FlaxEditor.Windows
{
Output = _buildTabProxy.PerPlatformOptions[platform].Output,
Platform = buildPlatform,
Mode = buildConfiguration,
Mode = buildConfig,
},
Options = buildOptions,
});
@@ -1001,8 +1002,9 @@ namespace FlaxEditor.Windows
public void RunCooked()
{
Editor.Log("Running cooked build");
GameCooker.GetCurrentPlatform(out var platform, out var buildPlatform, out var buildConfiguration);
GameCooker.GetCurrentPlatform(out var platform, out var buildPlatform, out _);
var numberOfClients = Editor.Options.Options.Interface.NumberOfGameClientsToLaunch;
var buildConfig = Editor.Options.Options.Interface.CookAndRunBuildConfiguration;
for (int i = 0; i < numberOfClients; i++)
{
_buildingQueue.Enqueue(new QueueItem
@@ -1011,7 +1013,7 @@ namespace FlaxEditor.Windows
{
Output = _buildTabProxy.PerPlatformOptions[platform].Output,
Platform = buildPlatform,
Mode = buildConfiguration,
Mode = buildConfig,
},
Options = BuildOptions.AutoRun | BuildOptions.NoCook,
});

View File

@@ -63,6 +63,16 @@ namespace FlaxEditor.Windows
},
};
/// <summary>
/// Fired when the game window audio is muted.
/// </summary>
public event Action MuteAudio;
/// <summary>
/// Fired when the game window master audio volume is changed.
/// </summary>
public event Action<float> MasterVolumeChanged;
/// <summary>
/// Gets the viewport.
/// </summary>
@@ -120,6 +130,7 @@ namespace FlaxEditor.Windows
{
Audio.MasterVolume = value ? 0 : AudioVolume;
_audioMuted = value;
MuteAudio?.Invoke();
}
}
@@ -134,6 +145,7 @@ namespace FlaxEditor.Windows
if (!AudioMuted)
Audio.MasterVolume = value;
_audioVolume = value;
MasterVolumeChanged?.Invoke(value);
}
}

View File

@@ -229,18 +229,12 @@ void AnimGraphExecutor::ProcessAnimEvents(AnimGraphNode* node, bool loop, float
auto& context = *Context.Get();
float eventTimeMin = animPrevPos;
float eventTimeMax = animPos;
if (loop && context.DeltaTime * speed < 0)
if (eventTimeMin > eventTimeMax)
{
// Check if animation looped (for anim events shooting during backwards playback)
//const float posNotLooped = startTimePos + oldTimePos;
//if (posNotLooped < 0.0f || posNotLooped > length)
//const int32 animPosCycle = Math::CeilToInt(animPos / anim->GetDuration());
//const int32 animPrevPosCycle = Math::CeilToInt(animPrevPos / anim->GetDuration());
//if (animPosCycle != animPrevPosCycle)
{
Swap(eventTimeMin, eventTimeMax);
}
Swap(eventTimeMin, eventTimeMax);
}
const float eventTime = (float)(animPos / anim->Data.FramesPerSecond);
const float eventDeltaTime = (float)((animPos - animPrevPos) / anim->Data.FramesPerSecond);
for (const auto& track : anim->Events)
@@ -251,7 +245,13 @@ void AnimGraphExecutor::ProcessAnimEvents(AnimGraphNode* node, bool loop, float
continue;
const float duration = k.Value.Duration > 1 ? k.Value.Duration : 0.0f;
#define ADD_OUTGOING_EVENT(type) context.Data->OutgoingEvents.Add({ k.Value.Instance, (AnimatedModel*)context.Data->Object, anim, eventTime, eventDeltaTime, AnimGraphInstanceData::OutgoingEvent::type })
if (k.Time <= eventTimeMax && eventTimeMin <= k.Time + duration)
if ((k.Time <= eventTimeMax && eventTimeMin <= k.Time + duration
&& (Math::FloorToInt(animPos) != 0 && Math::CeilToInt(animPrevPos) != Math::CeilToInt(anim->GetDuration()) && Math::FloorToInt(animPrevPos) != 0 && Math::CeilToInt(animPos) != Math::CeilToInt(anim->GetDuration())))
// Handle the edge case of an event on 0 or on max animation duration during looping
|| (loop && duration == 0.0f && Math::CeilToInt(animPos) == Math::CeilToInt(anim->GetDuration()) && k.Time == anim->GetDuration())
|| (loop && Math::FloorToInt(animPos) == 0 && Math::CeilToInt(animPrevPos) == Math::CeilToInt(anim->GetDuration()) && k.Time == 0.0f)
|| (loop && Math::FloorToInt(animPrevPos) == 0 && Math::CeilToInt(animPos) == Math::CeilToInt(anim->GetDuration()) && k.Time == 0.0f)
)
{
int32 stateIndex = -1;
if (duration > 1)

View File

@@ -531,7 +531,7 @@ bool Animation::SaveHeader(const ModelData& modelData, WriteStream& stream, int3
// Nested animations
stream.WriteInt32(0); // Empty list
return false;
return stream.HasError();
}
void Animation::GetReferences(Array<Guid>& assets, Array<String>& files) const

View File

@@ -395,7 +395,7 @@ bool Model::LoadHeader(ReadStream& stream, byte& headerVersion)
}
}
return false;
return stream.HasError();
}
#if USE_EDITOR
@@ -454,7 +454,7 @@ bool Model::SaveHeader(WriteStream& stream, const ModelData& modelData)
}
}
return false;
return stream.HasError();
}
bool Model::Save(bool withMeshDataFromGpu, Function<FlaxChunk*(int32)>& getChunk) const

View File

@@ -329,7 +329,7 @@ bool ModelBase::LoadHeader(ReadStream& stream, byte& headerVersion)
stream.Read(slot.Name, 11);
}
return false;
return stream.HasError();
}
bool ModelBase::LoadMesh(MemoryReadStream& stream, byte meshVersion, MeshBase* mesh, MeshData* dataIfReadOnly)

View File

@@ -656,7 +656,7 @@ bool SkinnedModel::LoadHeader(ReadStream& stream, byte& headerVersion)
}
}
return false;
return stream.HasError();
}
#if USE_EDITOR
@@ -686,7 +686,7 @@ bool SkinnedModel::SaveHeader(WriteStream& stream) const
const int32 blendShapes = mesh.BlendShapes.Count();
stream.Write((uint16)blendShapes);
for (const auto& blendShape : mesh.BlendShapes)
blendShape.Save(stream);
blendShape.SaveHeader(stream);
}
}

View File

@@ -4,7 +4,7 @@
#include "StringView.h"
#include "Engine/Core/Collections/Array.h"
String String::Empty;
const String String::Empty;
String::String(const StringAnsi& str)
{

View File

@@ -548,7 +548,7 @@ public:
/// <summary>
/// Instance of the empty string.
/// </summary>
static String Empty;
static const String Empty;
public:
/// <summary>

View File

@@ -9,7 +9,7 @@ StringView StringBuilder::ToStringView() const
return StringView(_data.Get(), _data.Count());
}
StringView StringView::Empty;
const StringView StringView::Empty;
StringView::StringView(const String& str)
: StringViewBase<Char>(str.Get(), str.Length())

View File

@@ -219,7 +219,7 @@ public:
/// <summary>
/// Instance of the empty string.
/// </summary>
static StringView Empty;
static const StringView Empty;
public:
/// <summary>

View File

@@ -16,7 +16,7 @@
PACK_STRUCT(struct DecalMaterialShaderData {
Matrix WorldMatrix;
Matrix InvWorld;
Matrix SVPositionToWorld;
Matrix SvPositionToWorld;
});
DrawPass DecalMaterialShader::GetDrawModes() const
@@ -47,7 +47,9 @@ void DecalMaterialShader::Bind(BindParameters& params)
MaterialParams::Bind(params.ParamsLink, bindMeta);
// Decals use depth buffer to draw on top of the objects
context->BindSR(0, GET_TEXTURE_VIEW_SAFE(params.RenderContext.Buffers->DepthBuffer));
GPUTexture* depthBuffer = params.RenderContext.Buffers->DepthBuffer;
GPUTextureView* depthBufferView = EnumHasAnyFlags(depthBuffer->Flags(), GPUTextureFlags::ReadOnlyDepthView) ? depthBuffer->ViewReadOnlyDepth() : depthBuffer->View();
context->BindSR(0, depthBufferView);
// Setup material constants
{
@@ -65,7 +67,7 @@ void DecalMaterialShader::Bind(BindParameters& params)
0, 0, 1, 0,
-1.0f, 1.0f, 0, 1);
const Matrix svPositionToWorld = offsetMatrix * view.IVP;
Matrix::Transpose(svPositionToWorld, materialData->SVPositionToWorld);
Matrix::Transpose(svPositionToWorld, materialData->SvPositionToWorld);
}
// Bind constants
@@ -90,16 +92,20 @@ void DecalMaterialShader::Unload()
bool DecalMaterialShader::Load()
{
GPUPipelineState::Description psDesc0 = GPUPipelineState::Description::DefaultNoDepth;
psDesc0.VS = _shader->GetVS("VS_Decal");
psDesc0.VS = _shader->GetVS("VS_Decal"); // TODO: move VS_Decal to be shared (eg. in GBuffer.shader)
if (psDesc0.VS == nullptr)
return true;
psDesc0.PS = _shader->GetPS("PS_Decal");
psDesc0.CullMode = CullMode::Normal;
if (GPUDevice::Instance->Limits.HasReadOnlyDepth)
{
psDesc0.DepthEnable = true;
psDesc0.DepthWriteEnable = false;
}
switch (_info.DecalBlendingMode)
{
case MaterialDecalBlendingMode::Translucent:
{
psDesc0.BlendMode.BlendEnable = true;
psDesc0.BlendMode.SrcBlend = BlendingMode::Blend::SrcAlpha;
psDesc0.BlendMode.DestBlend = BlendingMode::Blend::InvSrcAlpha;
@@ -107,9 +113,7 @@ bool DecalMaterialShader::Load()
psDesc0.BlendMode.DestBlendAlpha = BlendingMode::Blend::One;
psDesc0.BlendMode.RenderTargetWriteMask = BlendingMode::ColorWrite::RGB;
break;
}
case MaterialDecalBlendingMode::Stain:
{
psDesc0.BlendMode.BlendEnable = true;
psDesc0.BlendMode.SrcBlend = BlendingMode::Blend::DestColor;
psDesc0.BlendMode.DestBlend = BlendingMode::Blend::InvSrcAlpha;
@@ -117,9 +121,7 @@ bool DecalMaterialShader::Load()
psDesc0.BlendMode.DestBlendAlpha = BlendingMode::Blend::One;
psDesc0.BlendMode.RenderTargetWriteMask = BlendingMode::ColorWrite::RGB;
break;
}
case MaterialDecalBlendingMode::Normal:
{
psDesc0.BlendMode.BlendEnable = true;
psDesc0.BlendMode.SrcBlend = BlendingMode::Blend::SrcAlpha;
psDesc0.BlendMode.DestBlend = BlendingMode::Blend::InvSrcAlpha;
@@ -127,13 +129,10 @@ bool DecalMaterialShader::Load()
psDesc0.BlendMode.DestBlendAlpha = BlendingMode::Blend::One;
psDesc0.BlendMode.RenderTargetWriteMask = BlendingMode::ColorWrite::RGB;
break;
}
case MaterialDecalBlendingMode::Emissive:
{
psDesc0.BlendMode = BlendingMode::Additive;
break;
}
}
_cache.Outside = GPUDevice::Instance->CreatePipelineState();
if (_cache.Outside->Init(psDesc0))
@@ -143,6 +142,7 @@ bool DecalMaterialShader::Load()
}
psDesc0.CullMode = CullMode::Inverted;
psDesc0.DepthEnable = false;
_cache.Inside = GPUDevice::Instance->CreatePipelineState();
if (_cache.Inside->Init(psDesc0))
{

View File

@@ -23,6 +23,8 @@ PACK_STRUCT(struct GUIMaterialShaderData {
Float4 ViewInfo;
Float4 ScreenSize;
Float4 ViewSize;
Float3 ViewPadding0;
float UnscaledTimeParam;
});
void GUIMaterialShader::Bind(BindParameters& params)
@@ -55,7 +57,8 @@ void GUIMaterialShader::Bind(BindParameters& params)
materialData->ViewPos = Float3::Zero;
materialData->ViewFar = 0.0f;
materialData->ViewDir = Float3::Forward;
materialData->TimeParam = params.TimeParam;
materialData->TimeParam = params.Time;
materialData->UnscaledTimeParam = params.UnscaledTime;
materialData->ViewInfo = Float4::Zero;
auto& viewport = Render2D::GetViewport();
materialData->ScreenSize = Float4(viewport.Width, viewport.Height, 1.0f / viewport.Width, 1.0f / viewport.Height);

View File

@@ -148,7 +148,7 @@ public:
const ::DrawCall* DrawCall = nullptr;
MaterialParamsLink* ParamsLink = nullptr;
void* CustomData = nullptr;
float TimeParam;
float Time, UnscaledTime;
bool Instanced = false;
/// <summary>

View File

@@ -36,12 +36,15 @@ GPU_CB_STRUCT(MaterialShaderDataPerView {
Float4 TemporalAAJitter;
Float3 LargeWorldsChunkIndex;
float LargeWorldsChunkSize;
Float3 ViewPadding0;
float UnscaledTimeParam;
});
IMaterial::BindParameters::BindParameters(::GPUContext* context, const ::RenderContext& renderContext)
: GPUContext(context)
, RenderContext(renderContext)
, TimeParam(Time::Draw.UnscaledTime.GetTotalSeconds())
, Time(Time::Draw.Time.GetTotalSeconds())
, UnscaledTime(Time::Draw.UnscaledTime.GetTotalSeconds())
{
}
@@ -49,7 +52,8 @@ IMaterial::BindParameters::BindParameters(::GPUContext* context, const ::RenderC
: GPUContext(context)
, RenderContext(renderContext)
, DrawCall(&drawCall)
, TimeParam(Time::Draw.UnscaledTime.GetTotalSeconds())
, Time(Time::Draw.Time.GetTotalSeconds())
, UnscaledTime(Time::Draw.UnscaledTime.GetTotalSeconds())
, Instanced(instanced)
{
}
@@ -77,7 +81,8 @@ void IMaterial::BindParameters::BindViewData()
cb.ViewPos = view.Position;
cb.ViewFar = view.Far;
cb.ViewDir = view.Direction;
cb.TimeParam = TimeParam;
cb.TimeParam = Time;
cb.UnscaledTimeParam = UnscaledTime;
cb.ViewInfo = view.ViewInfo;
cb.ScreenSize = view.ScreenSize;
cb.TemporalAAJitter = view.TemporalAAJitter;

View File

@@ -10,7 +10,7 @@
/// <summary>
/// Current materials shader version.
/// </summary>
#define MATERIAL_GRAPH_VERSION 173
#define MATERIAL_GRAPH_VERSION 174
class Material;
class GPUShader;

View File

@@ -21,6 +21,8 @@ PACK_STRUCT(struct PostFxMaterialShaderData {
Float4 ScreenSize;
Float4 TemporalAAJitter;
Matrix InverseViewProjectionMatrix;
Float3 ViewPadding0;
float UnscaledTimeParam;
});
void PostFxMaterialShader::Bind(BindParameters& params)
@@ -51,7 +53,8 @@ void PostFxMaterialShader::Bind(BindParameters& params)
materialData->ViewPos = view.Position;
materialData->ViewFar = view.Far;
materialData->ViewDir = view.Direction;
materialData->TimeParam = params.TimeParam;
materialData->TimeParam = params.Time;
materialData->UnscaledTimeParam = params.UnscaledTime;
materialData->ViewInfo = view.ViewInfo;
materialData->ScreenSize = view.ScreenSize;
materialData->TemporalAAJitter = view.TemporalAAJitter;

View File

@@ -176,6 +176,14 @@ void AnimatedModel::GetNodeTransformation(const StringView& nodeName, Matrix& no
GetNodeTransformation(SkinnedModel ? SkinnedModel->FindNode(nodeName) : -1, nodeTransformation, worldSpace);
}
void AnimatedModel::GetNodeTransformation(Array<NodeTransformation>& nodeTransformations, bool worldSpace) const
{
for (NodeTransformation& item : nodeTransformations)
{
GetNodeTransformation(item.NodeIndex, item.NodeMatrix, worldSpace);
}
}
void AnimatedModel::SetNodeTransformation(int32 nodeIndex, const Matrix& nodeTransformation, bool worldSpace)
{
if (GraphInstance.NodesPose.IsEmpty())
@@ -193,6 +201,33 @@ void AnimatedModel::SetNodeTransformation(int32 nodeIndex, const Matrix& nodeTra
OnAnimationUpdated();
}
void AnimatedModel::SetNodeTransformation(const Array<NodeTransformation>& nodeTransformations, bool worldSpace)
{
if (GraphInstance.NodesPose.IsEmpty())
const_cast<AnimatedModel*>(this)->PreInitSkinningData(); // Ensure to have valid nodes pose to return
// Calculate it once, outside loop
Matrix invWorld;
if (worldSpace)
{
Matrix world;
GetLocalToWorldMatrix(world);
Matrix::Invert(world, invWorld);
}
for (int i = 0; i < nodeTransformations.Count(); i++)
{
int nodeIndex = nodeTransformations[i].NodeIndex;
CHECK(nodeIndex >= 0 && nodeIndex < GraphInstance.NodesPose.Count());
GraphInstance.NodesPose[nodeIndex] = nodeTransformations[i].NodeMatrix;
if (worldSpace)
{
GraphInstance.NodesPose[nodeIndex] = GraphInstance.NodesPose[nodeIndex] * invWorld;
}
}
OnAnimationUpdated();
}
void AnimatedModel::SetNodeTransformation(const StringView& nodeName, const Matrix& nodeTransformation, bool worldSpace)
{
SetNodeTransformation(SkinnedModel ? SkinnedModel->FindNode(nodeName) : -1, nodeTransformation, worldSpace);
@@ -821,7 +856,10 @@ void AnimatedModel::OnAnimationUpdated_Async()
_skinningData.OnDataChanged(!PerBoneMotionBlur);
}
UpdateBounds();
if (UpdateWhenOffscreen)
{
UpdateBounds();
}
}
void AnimatedModel::OnAnimationUpdated_Sync()

View File

@@ -18,6 +18,24 @@ class FLAXENGINE_API AnimatedModel : public ModelInstanceActor
DECLARE_SCENE_OBJECT(AnimatedModel);
friend class AnimationsSystem;
/// <summary>
/// Keeps the data of a Node and its relevant Transform Matrix together when passing it between functions.
/// </summary>
API_STRUCT() struct NodeTransformation
{
DECLARE_SCRIPTING_TYPE_MINIMAL(NodeTransformation);
/// <summary>
/// The index of the node in the node hierarchy.
/// </summary>
API_FIELD() uint32 NodeIndex;
/// <summary>
/// The transformation matrix of the node
/// </summary>
API_FIELD() Matrix NodeMatrix;
};
/// <summary>
/// Describes the animation graph updates frequency for the animated model.
/// </summary>
@@ -236,6 +254,14 @@ public:
/// <param name="worldSpace">True if convert matrices into world-space, otherwise returned values will be in local-space of the actor.</param>
API_FUNCTION() void GetNodeTransformation(const StringView& nodeName, API_PARAM(Out) Matrix& nodeTransformation, bool worldSpace = false) const;
/// <summary>
/// Gets the node final transformation for a series of nodes.
/// </summary>
/// <param name="nodeTransformations">The series of nodes that will be returned</param>
/// <param name="worldSpace">True if convert matrices into world-space, otherwise returned values will be in local-space of the actor.</param>
/// <returns></returns>
API_FUNCTION() void GetNodeTransformation(API_PARAM(Ref) Array<NodeTransformation>& nodeTransformations, bool worldSpace = false) const;
/// <summary>
/// Sets the node final transformation. If multiple nodes are to be set within a frame, do not use set worldSpace to true, and do the conversion yourself to avoid recalculation of inv matrices.
/// </summary>
@@ -252,6 +278,14 @@ public:
/// <param name="worldSpace">True if convert matrices from world-space, otherwise values will be in local-space of the actor.</param>
API_FUNCTION() void SetNodeTransformation(const StringView& nodeName, const Matrix& nodeTransformation, bool worldSpace = false);
/// <summary>
/// Sets a group of nodes final transformation.
/// </summary>
/// <param name="nodeTransformations">Array of the final node transformation matrix.</param>
/// <param name="worldSpace">True if convert matrices from world-space, otherwise values will be in local-space of the actor.</param>
/// <returns></returns>
API_FUNCTION() void SetNodeTransformation(const Array<NodeTransformation>& nodeTransformations, bool worldSpace = false);
/// <summary>
/// Finds the closest node to a given location.
/// </summary>

View File

@@ -546,6 +546,19 @@ void ParticleEffect::OnParticleSystemModified()
}
void ParticleEffect::OnParticleSystemLoaded()
{
ApplyModifiedParameters();
#if USE_EDITOR
// When one of the emitters gets edited, cached parameters need to be applied
auto& emitters = ParticleSystem.Get()->Emitters;
for (auto& emitter : emitters)
{
emitter.Loaded.BindUnique<ParticleEffect, &ParticleEffect::OnParticleEmitterLoaded>(this);
}
#endif
}
void ParticleEffect::OnParticleEmitterLoaded()
{
ApplyModifiedParameters();
}
@@ -823,6 +836,10 @@ void ParticleEffect::OnActiveInTreeChanged()
CacheModifiedParameters();
Instance.ClearState();
}
else
{
ApplyModifiedParameters();
}
}
void ParticleEffect::OnTransformChanged()

View File

@@ -394,6 +394,7 @@ private:
void ApplyModifiedParameters();
void OnParticleSystemModified();
void OnParticleSystemLoaded();
void OnParticleEmitterLoaded();
public:
// [Actor]

View File

@@ -7,10 +7,6 @@
#include "Engine/Platform/File.h"
#include "Engine/Core/Types/String.h"
#include "Engine/Core/Types/StringView.h"
#include "Engine/Core/Types/TimeSpan.h"
#include "Engine/Core/Collections/Array.h"
#include "Engine/Core/Math/Math.h"
#include "Engine/Core/Log.h"
#include "Engine/Utilities/StringConverter.h"
#include <sys/types.h>
#include <sys/stat.h>
@@ -18,281 +14,9 @@
#include <stdio.h>
#include <cerrno>
#include <dirent.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
const DateTime UnixEpoch(1970, 1, 1);
bool AppleFileSystem::CreateDirectory(const StringView& path)
{
const StringAsANSI<> pathAnsi(*path, path.Length());
// Skip if already exists
struct stat fileInfo;
if (stat(pathAnsi.Get(), &fileInfo) != -1 && S_ISDIR(fileInfo.st_mode))
{
return false;
}
// Recursively do it all again for the parent directory, if any
const int32 slashIndex = path.FindLast('/');
if (slashIndex > 1)
{
if (CreateDirectory(path.Substring(0, slashIndex)))
{
return true;
}
}
// Create the last directory on the path (the recursive calls will have taken care of the parent directories by now)
return mkdir(pathAnsi.Get(), 0755) != 0 && errno != EEXIST;
}
bool DeletePathTree(const char* path)
{
size_t pathLength;
DIR* dir;
struct stat statPath, statEntry;
struct dirent* entry;
// Stat for the path
stat(path, &statPath);
// If path does not exists or is not dir - exit with status -1
if (S_ISDIR(statPath.st_mode) == 0)
{
// Is not directory
return true;
}
// If not possible to read the directory for this user
if ((dir = opendir(path)) == NULL)
{
// Cannot open directory
return true;
}
// The length of the path
pathLength = strlen(path);
// Iteration through entries in the directory
while ((entry = readdir(dir)) != NULL)
{
// Skip entries "." and ".."
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
// Determinate a full path of an entry
char full_path[256];
ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(full_path));
strcpy(full_path, path);
strcat(full_path, "/");
strcat(full_path, entry->d_name);
// Stat for the entry
stat(full_path, &statEntry);
// Recursively remove a nested directory
if (S_ISDIR(statEntry.st_mode) != 0)
{
if (DeletePathTree(full_path))
return true;
continue;
}
// Remove a file object
if (unlink(full_path) != 0)
return true;
}
// Remove the devastated directory and close the object of it
if (rmdir(path) != 0)
return true;
closedir(dir);
return false;
}
bool AppleFileSystem::DeleteDirectory(const String& path, bool deleteContents)
{
const StringAsANSI<> pathANSI(*path, path.Length());
if (deleteContents)
return DeletePathTree(pathANSI.Get());
return rmdir(pathANSI.Get()) != 0;
}
bool AppleFileSystem::DirectoryExists(const StringView& path)
{
struct stat fileInfo;
const StringAsANSI<> pathANSI(*path, path.Length());
if (stat(pathANSI.Get(), &fileInfo) != -1)
{
return S_ISDIR(fileInfo.st_mode);
}
return false;
}
bool AppleFileSystem::DirectoryGetFiles(Array<String>& results, const String& path, const Char* searchPattern, DirectorySearchOption option)
{
const StringAsANSI<> pathANSI(*path, path.Length());
const StringAsANSI<> searchPatternANSI(searchPattern);
// Check if use only top directory
if (option == DirectorySearchOption::TopDirectoryOnly)
return getFilesFromDirectoryTop(results, pathANSI.Get(), searchPatternANSI.Get());
return getFilesFromDirectoryAll(results, pathANSI.Get(), searchPatternANSI.Get());
}
bool AppleFileSystem::GetChildDirectories(Array<String>& results, const String& path)
{
size_t pathLength;
DIR* dir;
struct stat statPath, statEntry;
struct dirent* entry;
const StringAsANSI<> pathANSI(*path, path.Length());
const char* pathStr = pathANSI.Get();
// Stat for the path
stat(pathStr, &statPath);
// If path does not exist or is not dir - exit with status -1
if (S_ISDIR(statPath.st_mode) == 0)
{
// Is not directory
return true;
}
// If not possible to read the directory for this user
if ((dir = opendir(pathStr)) == NULL)
{
// Cannot open directory
return true;
}
// The length of the path
pathLength = strlen(pathStr);
// Iteration through entries in the directory
while ((entry = readdir(dir)) != NULL)
{
// Skip entries "." and ".."
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
// Determinate a full path of an entry
char fullPath[256];
ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(fullPath));
strcpy(fullPath, pathStr);
strcat(fullPath, "/");
strcat(fullPath, entry->d_name);
// Stat for the entry
stat(fullPath, &statEntry);
// Check for directory
if (S_ISDIR(statEntry.st_mode) != 0)
{
// Add directory
results.Add(String(fullPath));
}
}
closedir(dir);
return false;
}
bool AppleFileSystem::FileExists(const StringView& path)
{
struct stat fileInfo;
const StringAsANSI<> pathANSI(*path, path.Length());
if (stat(pathANSI.Get(), &fileInfo) != -1)
{
return S_ISREG(fileInfo.st_mode);
}
return false;
}
bool AppleFileSystem::DeleteFile(const StringView& path)
{
const StringAsANSI<> pathANSI(*path, path.Length());
return unlink(pathANSI.Get()) != 0;
}
uint64 AppleFileSystem::GetFileSize(const StringView& path)
{
struct stat fileInfo;
fileInfo.st_size = 0;
const StringAsANSI<> pathANSI(*path, path.Length());
if (stat(pathANSI.Get(), &fileInfo) != -1)
{
// Check for directories
if (S_ISDIR(fileInfo.st_mode))
{
fileInfo.st_size = 0;
}
}
return fileInfo.st_size;
}
bool AppleFileSystem::IsReadOnly(const StringView& path)
{
const StringAsANSI<> pathANSI(*path, path.Length());
if (access(pathANSI.Get(), W_OK) == -1)
{
return errno == EACCES;
}
return false;
}
bool AppleFileSystem::SetReadOnly(const StringView& path, bool isReadOnly)
{
const StringAsANSI<> pathANSI(*path, path.Length());
struct stat fileInfo;
if (stat(pathANSI.Get(), &fileInfo) != -1)
{
if (isReadOnly)
{
fileInfo.st_mode &= ~S_IWUSR;
}
else
{
fileInfo.st_mode |= S_IWUSR;
}
return chmod(pathANSI.Get(), fileInfo.st_mode) == 0;
}
return false;
}
bool AppleFileSystem::MoveFile(const StringView& dst, const StringView& src, bool overwrite)
{
if (!overwrite && FileExists(dst))
{
// Already exists
return true;
}
if (overwrite)
{
unlink(StringAsANSI<>(*dst, dst.Length()).Get());
}
if (rename(StringAsANSI<>(*src, src.Length()).Get(), StringAsANSI<>(*dst, dst.Length()).Get()) != 0)
{
if (errno == EXDEV)
{
if (!CopyFile(dst, src))
{
unlink(StringAsANSI<>(*src, src.Length()).Get());
return false;
}
}
return true;
}
return false;
}
bool AppleFileSystem::CopyFile(const StringView& dst, const StringView& src)
{
const StringAsANSI<> srcANSI(*src, src.Length());
@@ -352,156 +76,6 @@ out_error:
return true;
}
bool AppleFileSystem::getFilesFromDirectoryTop(Array<String>& results, const char* path, const char* searchPattern)
{
size_t pathLength;
struct stat statPath, statEntry;
struct dirent* entry;
// Stat for the path
stat(path, &statPath);
// If path does not exists or is not dir - exit with status -1
if (S_ISDIR(statPath.st_mode) == 0)
{
// Is not directory
return true;
}
// If not possible to read the directory for this user
DIR* dir = opendir(path);
if (dir == NULL)
{
// Cannot open directory
return true;
}
// The length of the path
pathLength = strlen(path);
// Iteration through entries in the directory
while ((entry = readdir(dir)) != NULL)
{
// Skip entries "." and ".."
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
// Determinate a full path of an entry
char fullPath[256];
ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(fullPath));
strcpy(fullPath, path);
strcat(fullPath, "/");
strcat(fullPath, entry->d_name);
// Stat for the entry
stat(fullPath, &statEntry);
// Check for file
if (S_ISREG(statEntry.st_mode) != 0)
{
// Validate with filter
const int32 fullPathLength = StringUtils::Length(fullPath);
const int32 searchPatternLength = StringUtils::Length(searchPattern);
if (searchPatternLength == 0 || StringUtils::Compare(searchPattern, "*") == 0)
{
// All files
}
else if (searchPattern[0] == '*' && searchPatternLength < fullPathLength && StringUtils::Compare(fullPath + fullPathLength - searchPatternLength + 1, searchPattern + 1, searchPatternLength - 1) == 0)
{
// Path ending
}
else
{
// TODO: implement all cases in a generic way
continue;
}
// Add file
results.Add(String(fullPath));
}
}
closedir(dir);
return false;
}
bool AppleFileSystem::getFilesFromDirectoryAll(Array<String>& results, const char* path, const char* searchPattern)
{
// Find all files in this directory
getFilesFromDirectoryTop(results, path, searchPattern);
size_t pathLength;
DIR* dir;
struct stat statPath, statEntry;
struct dirent* entry;
// Stat for the path
stat(path, &statPath);
// If path does not exists or is not dir - exit with status -1
if (S_ISDIR(statPath.st_mode) == 0)
{
// Is not directory
return true;
}
// If not possible to read the directory for this user
if ((dir = opendir(path)) == NULL)
{
// Cannot open directory
return true;
}
// The length of the path
pathLength = strlen(path);
// Iteration through entries in the directory
while ((entry = readdir(dir)) != NULL)
{
// Skip entries "." and ".."
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
// Determinate a full path of an entry
char full_path[256];
ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(full_path));
strcpy(full_path, path);
strcat(full_path, "/");
strcat(full_path, entry->d_name);
// Stat for the entry
stat(full_path, &statEntry);
// Check for directory
if (S_ISDIR(statEntry.st_mode) != 0)
{
if (getFilesFromDirectoryAll(results, full_path, searchPattern))
{
closedir(dir);
return true;
}
}
}
closedir(dir);
return false;
}
DateTime AppleFileSystem::GetFileLastEditTime(const StringView& path)
{
struct stat fileInfo;
const StringAsANSI<> pathANSI(*path, path.Length());
if (stat(pathANSI.Get(), &fileInfo) == -1)
{
return DateTime::MinValue();
}
const TimeSpan timeSinceEpoch(0, 0, 0, fileInfo.st_mtime);
return UnixEpoch + timeSinceEpoch;
}
void AppleFileSystem::GetSpecialFolderPath(const SpecialFolder type, String& result)
{
String home;

View File

@@ -4,33 +4,17 @@
#if PLATFORM_MAC || PLATFORM_IOS
#include "Engine/Platform/Base/FileSystemBase.h"
#include "Engine/Platform/Unix/UnixFileSystem.h"
/// <summary>
/// Apple platform implementation of filesystem service.
/// </summary>
class FLAXENGINE_API AppleFileSystem : public FileSystemBase
class FLAXENGINE_API AppleFileSystem : public UnixFileSystem
{
public:
// [FileSystemBase]
static bool CreateDirectory(const StringView& path);
static bool DeleteDirectory(const String& path, bool deleteContents = true);
static bool DirectoryExists(const StringView& path);
static bool DirectoryGetFiles(Array<String, HeapAllocation>& results, const String& path, const Char* searchPattern = TEXT("*"), DirectorySearchOption option = DirectorySearchOption::AllDirectories);
static bool GetChildDirectories(Array<String, HeapAllocation>& results, const String& path);
static bool FileExists(const StringView& path);
static bool DeleteFile(const StringView& path);
static uint64 GetFileSize(const StringView& path);
static bool IsReadOnly(const StringView& path);
static bool SetReadOnly(const StringView& path, bool isReadOnly);
static bool MoveFile(const StringView& dst, const StringView& src, bool overwrite = false);
static bool CopyFile(const StringView& dst, const StringView& src);
static DateTime GetFileLastEditTime(const StringView& path);
static void GetSpecialFolderPath(const SpecialFolder type, String& result);
private:
static bool getFilesFromDirectoryTop(Array<String, HeapAllocation>& results, const char* path, const char* searchPattern);
static bool getFilesFromDirectoryAll(Array<String, HeapAllocation>& results, const char* path, const char* searchPattern);
};
#endif

View File

@@ -4,22 +4,16 @@
#include "LinuxFileSystem.h"
#include "Engine/Platform/File.h"
#include "Engine/Platform/StringUtils.h"
#include "Engine/Core/Types/String.h"
#include "Engine/Core/Types/StringBuilder.h"
#include "Engine/Core/Types/StringView.h"
#include "Engine/Core/Types/TimeSpan.h"
#include "Engine/Core/Collections/Array.h"
#include "Engine/Core/Math/Math.h"
#include "Engine/Core/Log.h"
#include "Engine/Utilities/StringConverter.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/sendfile.h>
#include <unistd.h>
#include <stdio.h>
#include <cerrno>
#include <dirent.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
@@ -165,280 +159,6 @@ bool LinuxFileSystem::ShowFileExplorer(const StringView& path)
return false;
}
bool LinuxFileSystem::CreateDirectory(const StringView& path)
{
const StringAsUTF8<> pathAnsi(*path, path.Length());
// Skip if already exists
struct stat fileInfo;
if (stat(pathAnsi.Get(), &fileInfo) != -1 && S_ISDIR(fileInfo.st_mode))
{
return false;
}
// Recursively do it all again for the parent directory, if any
const int32 slashIndex = path.FindLast('/');
if (slashIndex > 1)
{
if (CreateDirectory(path.Substring(0, slashIndex)))
{
return true;
}
}
// Create the last directory on the path (the recursive calls will have taken care of the parent directories by now)
return mkdir(pathAnsi.Get(), 0755) != 0 && errno != EEXIST;
}
bool DeleteUnixPathTree(const char* path)
{
size_t pathLength;
DIR* dir;
struct stat statPath, statEntry;
struct dirent* entry;
// Stat for the path
stat(path, &statPath);
// If path does not exists or is not dir - exit with status -1
if (S_ISDIR(statPath.st_mode) == 0)
{
// Is not directory
return true;
}
// If not possible to read the directory for this user
if ((dir = opendir(path)) == NULL)
{
// Cannot open directory
return true;
}
// The length of the path
pathLength = strlen(path);
// Iteration through entries in the directory
while ((entry = readdir(dir)) != NULL)
{
// Skip entries "." and ".."
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
// Determinate a full path of an entry
char full_path[256];
ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(full_path));
strcpy(full_path, path);
strcat(full_path, "/");
strcat(full_path, entry->d_name);
// Stat for the entry
stat(full_path, &statEntry);
// Recursively remove a nested directory
if (S_ISDIR(statEntry.st_mode) != 0)
{
if (DeleteUnixPathTree(full_path))
return true;
continue;
}
// Remove a file object
if (unlink(full_path) != 0)
return true;
}
// Remove the devastated directory and close the object of it
if (rmdir(path) != 0)
return true;
closedir(dir);
return false;
}
bool LinuxFileSystem::DeleteDirectory(const String& path, bool deleteContents)
{
const StringAsUTF8<> pathANSI(*path, path.Length());
if (deleteContents)
{
return DeleteUnixPathTree(pathANSI.Get());
}
else
{
return rmdir(pathANSI.Get()) != 0;
}
}
bool LinuxFileSystem::DirectoryExists(const StringView& path)
{
struct stat fileInfo;
const StringAsUTF8<> pathANSI(*path, path.Length());
if (stat(pathANSI.Get(), &fileInfo) != -1)
{
return S_ISDIR(fileInfo.st_mode);
}
return false;
}
bool LinuxFileSystem::DirectoryGetFiles(Array<String>& results, const String& path, const Char* searchPattern, DirectorySearchOption option)
{
const StringAsUTF8<> pathANSI(*path, path.Length());
const StringAsUTF8<> searchPatternANSI(searchPattern);
// Check if use only top directory
if (option == DirectorySearchOption::TopDirectoryOnly)
return getFilesFromDirectoryTop(results, pathANSI.Get(), searchPatternANSI.Get());
return getFilesFromDirectoryAll(results, pathANSI.Get(), searchPatternANSI.Get());
}
bool LinuxFileSystem::GetChildDirectories(Array<String>& results, const String& path)
{
size_t pathLength;
DIR* dir;
struct stat statPath, statEntry;
struct dirent* entry;
const StringAsUTF8<> pathANSI(*path, path.Length());
const char* pathStr = pathANSI.Get();
// Stat for the path
stat(pathStr, &statPath);
// If path does not exists or is not dir - exit with status -1
if (S_ISDIR(statPath.st_mode) == 0)
{
// Is not directory
return true;
}
// If not possible to read the directory for this user
if ((dir = opendir(pathStr)) == NULL)
{
// Cannot open directory
return true;
}
// The length of the path
pathLength = strlen(pathStr);
// Iteration through entries in the directory
while ((entry = readdir(dir)) != NULL)
{
// Skip entries "." and ".."
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
// Determinate a full path of an entry
char fullPath[256];
ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(fullPath));
strcpy(fullPath, pathStr);
strcat(fullPath, "/");
strcat(fullPath, entry->d_name);
// Stat for the entry
stat(fullPath, &statEntry);
// Check for directory
if (S_ISDIR(statEntry.st_mode) != 0)
{
// Add directory
results.Add(String(fullPath));
}
}
closedir(dir);
return false;
}
bool LinuxFileSystem::FileExists(const StringView& path)
{
struct stat fileInfo;
const StringAsUTF8<> pathANSI(*path, path.Length());
if (stat(pathANSI.Get(), &fileInfo) != -1)
{
return S_ISREG(fileInfo.st_mode);
}
return false;
}
bool LinuxFileSystem::DeleteFile(const StringView& path)
{
const StringAsUTF8<> pathANSI(*path, path.Length());
return unlink(pathANSI.Get()) != 0;
}
uint64 LinuxFileSystem::GetFileSize(const StringView& path)
{
struct stat fileInfo;
fileInfo.st_size = 0;
const StringAsUTF8<> pathANSI(*path, path.Length());
if (stat(pathANSI.Get(), &fileInfo) != -1)
{
// Check for directories
if (S_ISDIR(fileInfo.st_mode))
{
fileInfo.st_size = 0;
}
}
return fileInfo.st_size;
}
bool LinuxFileSystem::IsReadOnly(const StringView& path)
{
const StringAsUTF8<> pathANSI(*path, path.Length());
if (access(pathANSI.Get(), W_OK) == -1)
{
return errno == EACCES;
}
return false;
}
bool LinuxFileSystem::SetReadOnly(const StringView& path, bool isReadOnly)
{
const StringAsUTF8<> pathANSI(*path, path.Length());
struct stat fileInfo;
if (stat(pathANSI.Get(), &fileInfo) != -1)
{
if (isReadOnly)
{
fileInfo.st_mode &= ~S_IWUSR;
}
else
{
fileInfo.st_mode |= S_IWUSR;
}
return chmod(pathANSI.Get(), fileInfo.st_mode) == 0;
}
return false;
}
bool LinuxFileSystem::MoveFile(const StringView& dst, const StringView& src, bool overwrite)
{
if (!overwrite && FileExists(dst))
{
// Already exists
return true;
}
if (overwrite)
{
unlink(StringAsUTF8<>(*dst, dst.Length()).Get());
}
if (rename(StringAsUTF8<>(*src, src.Length()).Get(), StringAsUTF8<>(*dst, dst.Length()).Get()) != 0)
{
if (errno == EXDEV)
{
if (!CopyFile(dst, src))
{
unlink(StringAsUTF8<>(*src, src.Length()).Get());
return false;
}
}
return true;
}
return false;
}
bool LinuxFileSystem::CopyFile(const StringView& dst, const StringView& src)
{
const StringAsUTF8<> srcANSI(*src, src.Length());
@@ -612,156 +332,6 @@ bool LinuxFileSystem::UrnEncodePath(const char *path, char *result, const int ma
return true;
}
bool LinuxFileSystem::getFilesFromDirectoryTop(Array<String>& results, const char* path, const char* searchPattern)
{
size_t pathLength;
struct stat statPath, statEntry;
struct dirent* entry;
// Stat for the path
stat(path, &statPath);
// If path does not exists or is not dir - exit with status -1
if (S_ISDIR(statPath.st_mode) == 0)
{
// Is not directory
return true;
}
// If not possible to read the directory for this user
DIR* dir = opendir(path);
if (dir == NULL)
{
// Cannot open directory
return true;
}
// The length of the path
pathLength = strlen(path);
// Iteration through entries in the directory
while ((entry = readdir(dir)) != NULL)
{
// Skip entries "." and ".."
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
// Determinate a full path of an entry
char fullPath[256];
ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(fullPath));
strcpy(fullPath, path);
strcat(fullPath, "/");
strcat(fullPath, entry->d_name);
// Stat for the entry
stat(fullPath, &statEntry);
// Check for file
if (S_ISREG(statEntry.st_mode) != 0)
{
// Validate with filter
const int32 fullPathLength = StringUtils::Length(fullPath);
const int32 searchPatternLength = StringUtils::Length(searchPattern);
if (searchPatternLength == 0 || StringUtils::Compare(searchPattern, "*") == 0)
{
// All files
}
else if (searchPattern[0] == '*' && searchPatternLength < fullPathLength && StringUtils::Compare(fullPath + fullPathLength - searchPatternLength + 1, searchPattern + 1, searchPatternLength - 1) == 0)
{
// Path ending
}
else
{
// TODO: implement all cases in a generic way
continue;
}
// Add file
results.Add(String(fullPath));
}
}
closedir(dir);
return false;
}
bool LinuxFileSystem::getFilesFromDirectoryAll(Array<String>& results, const char* path, const char* searchPattern)
{
// Find all files in this directory
getFilesFromDirectoryTop(results, path, searchPattern);
size_t pathLength;
DIR* dir;
struct stat statPath, statEntry;
struct dirent* entry;
// Stat for the path
stat(path, &statPath);
// If path does not exists or is not dir - exit with status -1
if (S_ISDIR(statPath.st_mode) == 0)
{
// Is not directory
return true;
}
// If not possible to read the directory for this user
if ((dir = opendir(path)) == NULL)
{
// Cannot open directory
return true;
}
// The length of the path
pathLength = strlen(path);
// Iteration through entries in the directory
while ((entry = readdir(dir)) != NULL)
{
// Skip entries "." and ".."
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
// Determinate a full path of an entry
char full_path[256];
ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(full_path));
strcpy(full_path, path);
strcat(full_path, "/");
strcat(full_path, entry->d_name);
// Stat for the entry
stat(full_path, &statEntry);
// Check for directory
if (S_ISDIR(statEntry.st_mode) != 0)
{
if (getFilesFromDirectoryAll(results, full_path, searchPattern))
{
closedir(dir);
return true;
}
}
}
closedir(dir);
return false;
}
DateTime LinuxFileSystem::GetFileLastEditTime(const StringView& path)
{
struct stat fileInfo;
const StringAsUTF8<> pathANSI(*path, path.Length());
if (stat(pathANSI.Get(), &fileInfo) == -1)
{
return DateTime::MinValue();
}
const TimeSpan timeSinceEpoch(0, 0, 0, fileInfo.st_mtime);
return UnixEpoch + timeSinceEpoch;
}
void LinuxFileSystem::GetSpecialFolderPath(const SpecialFolder type, String& result)
{
const String& home = LinuxPlatform::GetHomeDirectory();

View File

@@ -4,38 +4,24 @@
#if PLATFORM_LINUX
#include "Engine/Platform/Base/FileSystemBase.h"
#include "Engine/Platform/Unix/UnixFileSystem.h"
/// <summary>
/// Linux platform implementation of filesystem service.
/// </summary>
class FLAXENGINE_API LinuxFileSystem : public FileSystemBase
class FLAXENGINE_API LinuxFileSystem : public UnixFileSystem
{
public:
// [FileSystemBase]
static bool ShowOpenFileDialog(Window* parentWindow, const StringView& initialDirectory, const StringView& filter, bool multiSelect, const StringView& title, Array<String, HeapAllocation>& filenames);
static bool ShowBrowseFolderDialog(Window* parentWindow, const StringView& initialDirectory, const StringView& title, String& path);
static bool ShowFileExplorer(const StringView& path);
static bool CreateDirectory(const StringView& path);
static bool DeleteDirectory(const String& path, bool deleteContents = true);
static bool DirectoryExists(const StringView& path);
static bool DirectoryGetFiles(Array<String, HeapAllocation>& results, const String& path, const Char* searchPattern = TEXT("*"), DirectorySearchOption option = DirectorySearchOption::AllDirectories);
static bool GetChildDirectories(Array<String, HeapAllocation>& results, const String& path);
static bool FileExists(const StringView& path);
static bool DeleteFile(const StringView& path);
static bool MoveFileToRecycleBin(const StringView& path);
static uint64 GetFileSize(const StringView& path);
static bool IsReadOnly(const StringView& path);
static bool SetReadOnly(const StringView& path, bool isReadOnly);
static bool MoveFile(const StringView& dst, const StringView& src, bool overwrite = false);
static bool CopyFile(const StringView& dst, const StringView& src);
static DateTime GetFileLastEditTime(const StringView& path);
static bool MoveFileToRecycleBin(const StringView& path);
static void GetSpecialFolderPath(const SpecialFolder type, String& result);
private:
static bool UrnEncodePath(const char *path, char *result, int maxLength);
static bool getFilesFromDirectoryTop(Array<String, HeapAllocation>& results, const char* path, const char* searchPattern);
static bool getFilesFromDirectoryAll(Array<String, HeapAllocation>& results, const char* path, const char* searchPattern);
static String getBaseName(const StringView& path);
static String getNameWithoutExtension(const StringView& path);
};

View File

@@ -0,0 +1,470 @@
// Copyright (c) Wojciech Figat. All rights reserved.
#if PLATFORM_UNIX
#include "UnixFileSystem.h"
#include "Engine/Platform/File.h"
#include "Engine/Core/Types/TimeSpan.h"
#include "Engine/Core/Collections/Array.h"
#include "Engine/Core/Math/Math.h"
#include "Engine/Core/Log.h"
#include "Engine/Utilities/StringConverter.h"
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <cerrno>
#include <dirent.h>
#if PLATFORM_MAC || PLATFORM_IOS
typedef StringAsANSI<> UnixString;
#else
typedef StringAsUTF8<> UnixString;
#endif
const DateTime UnixEpoch(1970, 1, 1);
bool UnixFileSystem::CreateDirectory(const StringView& path)
{
const UnixString pathAnsi(*path, path.Length());
// Skip if already exists
struct stat fileInfo;
if (stat(pathAnsi.Get(), &fileInfo) != -1 && S_ISDIR(fileInfo.st_mode))
{
return false;
}
// Recursively do it all again for the parent directory, if any
const int32 slashIndex = path.FindLast('/');
if (slashIndex > 1)
{
if (CreateDirectory(path.Substring(0, slashIndex)))
{
return true;
}
}
// Create the last directory on the path (the recursive calls will have taken care of the parent directories by now)
return mkdir(pathAnsi.Get(), 0755) != 0 && errno != EEXIST;
}
bool DeleteUnixPathTree(const char* path)
{
size_t pathLength;
DIR* dir;
struct stat statPath, statEntry;
struct dirent* entry;
// Stat for the path
stat(path, &statPath);
// If path does not exists or is not dir - exit with status -1
if (S_ISDIR(statPath.st_mode) == 0)
{
// Is not directory
return true;
}
// If not possible to read the directory for this user
if ((dir = opendir(path)) == NULL)
{
// Cannot open directory
return true;
}
// The length of the path
pathLength = strlen(path);
// Iteration through entries in the directory
while ((entry = readdir(dir)) != NULL)
{
// Skip entries "." and ".."
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
// Determinate a full path of an entry
char full_path[256];
ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(full_path));
strcpy(full_path, path);
strcat(full_path, "/");
strcat(full_path, entry->d_name);
// Stat for the entry
stat(full_path, &statEntry);
// Recursively remove a nested directory
if (S_ISDIR(statEntry.st_mode) != 0)
{
if (DeleteUnixPathTree(full_path))
return true;
continue;
}
// Remove a file object
if (unlink(full_path) != 0)
return true;
}
// Remove the devastated directory and close the object of it
if (rmdir(path) != 0)
return true;
closedir(dir);
return false;
}
bool UnixFileSystem::DeleteDirectory(const String& path, bool deleteContents)
{
const UnixString pathANSI(*path, path.Length());
if (deleteContents)
{
return DeleteUnixPathTree(pathANSI.Get());
}
else
{
return rmdir(pathANSI.Get()) != 0;
}
}
bool UnixFileSystem::DirectoryExists(const StringView& path)
{
struct stat fileInfo;
const UnixString pathANSI(*path, path.Length());
if (stat(pathANSI.Get(), &fileInfo) != -1)
{
return S_ISDIR(fileInfo.st_mode);
}
return false;
}
bool UnixFileSystem::DirectoryGetFiles(Array<String>& results, const String& path, const Char* searchPattern, DirectorySearchOption option)
{
const UnixString pathANSI(*path, path.Length());
const UnixString searchPatternANSI(searchPattern);
// Check if use only top directory
if (option == DirectorySearchOption::TopDirectoryOnly)
return getFilesFromDirectoryTop(results, pathANSI.Get(), searchPatternANSI.Get());
return getFilesFromDirectoryAll(results, pathANSI.Get(), searchPatternANSI.Get());
}
bool UnixFileSystem::GetChildDirectories(Array<String>& results, const String& path)
{
size_t pathLength;
DIR* dir;
struct stat statPath, statEntry;
struct dirent* entry;
const UnixString pathANSI(*path, path.Length());
const char* pathStr = pathANSI.Get();
// Stat for the path
stat(pathStr, &statPath);
// If path does not exists or is not dir - exit with status -1
if (S_ISDIR(statPath.st_mode) == 0)
{
// Is not directory
return true;
}
// If not possible to read the directory for this user
if ((dir = opendir(pathStr)) == NULL)
{
// Cannot open directory
return true;
}
// The length of the path
pathLength = strlen(pathStr);
// Iteration through entries in the directory
while ((entry = readdir(dir)) != NULL)
{
// Skip entries "." and ".."
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
// Determinate a full path of an entry
char fullPath[256];
ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(fullPath));
strcpy(fullPath, pathStr);
strcat(fullPath, "/");
strcat(fullPath, entry->d_name);
// Stat for the entry
stat(fullPath, &statEntry);
// Check for directory
if (S_ISDIR(statEntry.st_mode) != 0)
{
// Add directory
results.Add(String(fullPath));
}
}
closedir(dir);
return false;
}
bool UnixFileSystem::FileExists(const StringView& path)
{
struct stat fileInfo;
const UnixString pathANSI(*path, path.Length());
if (stat(pathANSI.Get(), &fileInfo) != -1)
{
return S_ISREG(fileInfo.st_mode);
}
return false;
}
bool UnixFileSystem::DeleteFile(const StringView& path)
{
const UnixString pathANSI(*path, path.Length());
return unlink(pathANSI.Get()) != 0;
}
uint64 UnixFileSystem::GetFileSize(const StringView& path)
{
struct stat fileInfo;
fileInfo.st_size = 0;
const UnixString pathANSI(*path, path.Length());
if (stat(pathANSI.Get(), &fileInfo) != -1)
{
// Check for directories
if (S_ISDIR(fileInfo.st_mode))
{
fileInfo.st_size = 0;
}
}
return fileInfo.st_size;
}
bool UnixFileSystem::IsReadOnly(const StringView& path)
{
const UnixString pathANSI(*path, path.Length());
if (access(pathANSI.Get(), W_OK) == -1)
{
return errno == EACCES;
}
return false;
}
bool UnixFileSystem::SetReadOnly(const StringView& path, bool isReadOnly)
{
const UnixString pathANSI(*path, path.Length());
struct stat fileInfo;
if (stat(pathANSI.Get(), &fileInfo) != -1)
{
if (isReadOnly)
{
fileInfo.st_mode &= ~S_IWUSR;
}
else
{
fileInfo.st_mode |= S_IWUSR;
}
return chmod(pathANSI.Get(), fileInfo.st_mode) == 0;
}
return false;
}
bool UnixFileSystem::MoveFile(const StringView& dst, const StringView& src, bool overwrite)
{
if (!overwrite && FileExists(dst))
{
// Already exists
return true;
}
if (overwrite)
{
unlink(UnixString(*dst, dst.Length()).Get());
}
if (rename(UnixString(*src, src.Length()).Get(), UnixString(*dst, dst.Length()).Get()) != 0)
{
if (errno == EXDEV)
{
if (!CopyFile(dst, src))
{
unlink(UnixString(*src, src.Length()).Get());
return false;
}
}
return true;
}
return false;
}
bool UnixFileSystem::getFilesFromDirectoryTop(Array<String>& results, const char* path, const char* searchPattern)
{
size_t pathLength;
struct stat statPath, statEntry;
struct dirent* entry;
// Stat for the path
stat(path, &statPath);
// If path does not exists or is not dir - exit with status -1
if (S_ISDIR(statPath.st_mode) == 0)
{
// Is not directory
return true;
}
// If not possible to read the directory for this user
DIR* dir = opendir(path);
if (dir == NULL)
{
// Cannot open directory
return true;
}
// The length of the path
pathLength = strlen(path);
// Iteration through entries in the directory
while ((entry = readdir(dir)) != NULL)
{
// Skip entries "." and ".."
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
// Determinate a full path of an entry
char fullPath[256];
const int32 pathLength = strlen(entry->d_name);
ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(fullPath));
strcpy(fullPath, path);
strcat(fullPath, "/");
strcat(fullPath, entry->d_name);
// Stat for the entry
stat(fullPath, &statEntry);
// Check for file
if (S_ISREG(statEntry.st_mode) != 0)
{
// Validate with filter
const int32 fullPathLength = StringUtils::Length(fullPath);
const int32 searchPatternLength = StringUtils::Length(searchPattern);
if (searchPatternLength == 0 ||
StringUtils::Compare(searchPattern, "*") == 0 ||
StringUtils::Compare(searchPattern, "*.*") == 0)
{
// All files
}
else if (searchPattern[0] == '*' && searchPatternLength < fullPathLength && StringUtils::Compare(fullPath + fullPathLength - searchPatternLength + 1, searchPattern + 1, searchPatternLength - 1) == 0)
{
// Path ending
}
else if (searchPattern[0] == '*' && searchPatternLength > 2 && searchPattern[searchPatternLength-1] == '*')
{
// Contains pattern
bool match = false;
for (int32 i = 0; i < pathLength - searchPatternLength - 1; i++)
{
int32 len = Math::Min(searchPatternLength - 2, pathLength - i);
if (StringUtils::Compare(&entry->d_name[i], &searchPattern[1], len) == 0)
{
match = true;
break;
}
}
if (!match)
continue;
}
else
{
// TODO: implement all cases in a generic way
LOG(Warning, "DirectoryGetFiles: Wildcard filter is not implemented");
continue;
}
// Add file
results.Add(String(fullPath));
}
}
closedir(dir);
return false;
}
bool UnixFileSystem::getFilesFromDirectoryAll(Array<String>& results, const char* path, const char* searchPattern)
{
// Find all files in this directory
getFilesFromDirectoryTop(results, path, searchPattern);
size_t pathLength;
DIR* dir;
struct stat statPath, statEntry;
struct dirent* entry;
// Stat for the path
stat(path, &statPath);
// If path does not exists or is not dir - exit with status -1
if (S_ISDIR(statPath.st_mode) == 0)
{
// Is not directory
return true;
}
// If not possible to read the directory for this user
if ((dir = opendir(path)) == NULL)
{
// Cannot open directory
return true;
}
// The length of the path
pathLength = strlen(path);
// Iteration through entries in the directory
while ((entry = readdir(dir)) != NULL)
{
// Skip entries "." and ".."
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
// Determinate a full path of an entry
char full_path[256];
ASSERT(pathLength + strlen(entry->d_name) < ARRAY_COUNT(full_path));
strcpy(full_path, path);
strcat(full_path, "/");
strcat(full_path, entry->d_name);
// Stat for the entry
stat(full_path, &statEntry);
// Check for directory
if (S_ISDIR(statEntry.st_mode) != 0)
{
if (getFilesFromDirectoryAll(results, full_path, searchPattern))
{
closedir(dir);
return true;
}
}
}
closedir(dir);
return false;
}
DateTime UnixFileSystem::GetFileLastEditTime(const StringView& path)
{
struct stat fileInfo;
const UnixString pathANSI(*path, path.Length());
if (stat(pathANSI.Get(), &fileInfo) == -1)
{
return DateTime::MinValue();
}
const TimeSpan timeSinceEpoch(0, 0, 0, fileInfo.st_mtime);
return UnixEpoch + timeSinceEpoch;
}
#endif

View File

@@ -0,0 +1,35 @@
// Copyright (c) Wojciech Figat. All rights reserved.
#pragma once
#if PLATFORM_UNIX
#include "Engine/Platform/Base/FileSystemBase.h"
/// <summary>
/// Unix platform implementation of filesystem service.
/// </summary>
class FLAXENGINE_API UnixFileSystem : public FileSystemBase
{
public:
// [FileSystemBase]
static bool CreateDirectory(const StringView& path);
static bool DeleteDirectory(const String& path, bool deleteContents = true);
static bool DirectoryExists(const StringView& path);
static bool DirectoryGetFiles(Array<String, HeapAllocation>& results, const String& path, const Char* searchPattern = TEXT("*"), DirectorySearchOption option = DirectorySearchOption::AllDirectories);
static bool GetChildDirectories(Array<String, HeapAllocation>& results, const String& path);
static bool FileExists(const StringView& path);
static bool DeleteFile(const StringView& path);
static bool MoveFileToRecycleBin(const StringView& path);
static uint64 GetFileSize(const StringView& path);
static bool IsReadOnly(const StringView& path);
static bool SetReadOnly(const StringView& path, bool isReadOnly);
static bool MoveFile(const StringView& dst, const StringView& src, bool overwrite = false);
static DateTime GetFileLastEditTime(const StringView& path);
private:
static bool getFilesFromDirectoryTop(Array<String, HeapAllocation>& results, const char* path, const char* searchPattern);
static bool getFilesFromDirectoryAll(Array<String, HeapAllocation>& results, const char* path, const char* searchPattern);
};
#endif

View File

@@ -434,6 +434,7 @@ void GBufferPass::DrawDecals(RenderContext& renderContext, GPUTextureView* light
PROFILE_GPU_CPU("Decals");
auto context = GPUDevice::Instance->GetMainContext();
auto buffers = renderContext.Buffers;
GPUTextureView* depthBuffer = EnumHasAnyFlags(buffers->DepthBuffer->Flags(), GPUTextureFlags::ReadOnlyDepthView) ? buffers->DepthBuffer->ViewReadOnlyDepth() : nullptr;
// Sort decals from the lowest order to the highest order
Sorting::QuickSort(decals.Get(), decals.Count(), &SortDecal);
@@ -484,22 +485,22 @@ void GBufferPass::DrawDecals(RenderContext& renderContext, GPUTextureView* light
count++;
targetBuffers[2] = buffers->GBuffer1->View();
}
context->SetRenderTarget(nullptr, ToSpan(targetBuffers, count));
context->SetRenderTarget(depthBuffer, ToSpan(targetBuffers, count));
break;
}
case MaterialDecalBlendingMode::Stain:
{
context->SetRenderTarget(buffers->GBuffer0->View());
context->SetRenderTarget(depthBuffer, buffers->GBuffer0->View());
break;
}
case MaterialDecalBlendingMode::Normal:
{
context->SetRenderTarget(buffers->GBuffer1->View());
context->SetRenderTarget(depthBuffer, buffers->GBuffer1->View());
break;
}
case MaterialDecalBlendingMode::Emissive:
{
context->SetRenderTarget(lightBuffer);
context->SetRenderTarget(depthBuffer, lightBuffer);
break;
}
}

View File

@@ -106,6 +106,7 @@ namespace
MMethod* _method_LateFixedUpdate = nullptr;
MMethod* _method_Draw = nullptr;
MMethod* _method_Exit = nullptr;
Array<Function<void()>> UpdateActions;
Dictionary<StringAnsi, BinaryModule*, InlinedAllocation<64>> _nonNativeModules;
#if USE_EDITOR
bool LastBinariesLoadTriggeredCompilation = false;
@@ -242,6 +243,27 @@ void ScriptingService::Update()
PROFILE_CPU_NAMED("Scripting::Update");
INVOKE_EVENT(Update);
// Flush update actions
_objectsLocker.Lock();
int32 count = UpdateActions.Count();
for (int32 i = 0; i < count; i++)
{
UpdateActions[i]();
}
int32 newlyAdded = UpdateActions.Count() - count;
if (newlyAdded == 0)
UpdateActions.Clear();
else
{
// Someone added another action within current callback
Array<Function<void()>> tmp;
for (int32 i = newlyAdded; i < UpdateActions.Count(); i++)
tmp.Add(UpdateActions[i]);
UpdateActions.Clear();
UpdateActions.Add(tmp);
}
_objectsLocker.Unlock();
#ifdef USE_NETCORE
// Force GC to run in background periodically to avoid large blocking collections causing hitches
if (Time::Update.TicksCount % 60 == 0)
@@ -303,6 +325,13 @@ void Scripting::ProcessBuildInfoPath(String& path, const String& projectFolderPa
path = projectFolderPath / path;
}
void Scripting::InvokeOnUpdate(const Function<void()>& action)
{
_objectsLocker.Lock();
UpdateActions.Add(action);
_objectsLocker.Unlock();
}
bool Scripting::LoadBinaryModules(const String& path, const String& projectFolderPath)
{
PROFILE_CPU_NAMED("LoadBinaryModules");

View File

@@ -230,6 +230,11 @@ public:
static void ProcessBuildInfoPath(String& path, const String& projectFolderPath);
/// <summary>
/// Calls the given action on the next scripting update.
/// </summary>
/// <param name="action">The action to invoke.</param>
static void InvokeOnUpdate(const Function<void()>& action);
private:
static bool LoadBinaryModules(const String& path, const String& projectFolderPath);

View File

@@ -61,7 +61,11 @@ void MemoryReadStream::ReadBytes(void* data, uint32 bytes)
{
if (bytes > 0)
{
ASSERT(data && GetLength() - GetPosition() >= bytes);
if (!data || GetLength() - GetPosition() < bytes)
{
_hasError = true;
return;
}
Platform::MemoryCopy(data, _position, bytes);
_position += bytes;
}

View File

@@ -34,7 +34,6 @@ MaterialValue* MaterialGenerator::sampleTextureRaw(Node* caller, Value& value, B
const bool isArray = texture->Type == MaterialParameterType::GPUTextureArray;
const bool isVolume = texture->Type == MaterialParameterType::GPUTextureVolume;
const bool isNormalMap = texture->Type == MaterialParameterType::NormalMap;
const bool canUseSample = CanUseSample(_treeType);
MaterialGraphBox* valueBox = parent->GetBox(1);
// Check if has variable assigned
@@ -63,6 +62,16 @@ MaterialValue* MaterialGenerator::sampleTextureRaw(Node* caller, Value& value, B
// Check if hasn't been sampled during that tree eating
if (valueBox->Cache.IsInvalid())
{
bool canUseSample = CanUseSample(_treeType);
String mipLevel = TEXT("0");
const auto layer = GetRootLayer();
if (layer && layer->Domain == MaterialDomain::Decal && _treeType == MaterialTreeType::PixelShader)
{
// Decals use computed mip level due to ddx/ddy being unreliable
canUseSample = false;
mipLevel = String::Format(TEXT("CalculateTextureMipmap(input, {})"), texture->ShaderName);
}
// Check if use custom UVs
String uv;
MaterialGraphBox* uvBox = parent->GetBox(0);
@@ -94,10 +103,10 @@ MaterialValue* MaterialGenerator::sampleTextureRaw(Node* caller, Value& value, B
// Sample texture
if (isNormalMap)
{
const Char* format = canUseSample ? TEXT("{0}.Sample({1}, {2}).xyz") : TEXT("{0}.SampleLevel({1}, {2}, 0).xyz");
const Char* format = canUseSample ? TEXT("{0}.Sample({1}, {2}).xyz") : TEXT("{0}.SampleLevel({1}, {2}, {3}).xyz");
// Sample encoded normal map
const String sampledValue = String::Format(format, texture->ShaderName, sampler, uv);
const String sampledValue = String::Format(format, texture->ShaderName, sampler, uv, mipLevel);
const auto normalVector = writeLocal(VariantType::Float3, sampledValue, parent);
// Decode normal vector
@@ -123,12 +132,12 @@ MaterialValue* MaterialGenerator::sampleTextureRaw(Node* caller, Value& value, B
}
else*/
{
format = canUseSample ? TEXT("{0}.Sample({1}, {2})") : TEXT("{0}.SampleLevel({1}, {2}, 0)");
format = canUseSample ? TEXT("{0}.Sample({1}, {2})") : TEXT("{0}.SampleLevel({1}, {2}, {3})");
}
}
// Sample texture
String sampledValue = String::Format(format, texture->ShaderName, sampler, uv, _ddx.Value, _ddy.Value);
String sampledValue = String::Format(format, texture->ShaderName, sampler, uv, mipLevel);
valueBox->Cache = writeLocal(VariantType::Float4, sampledValue, parent);
}
}

View File

@@ -48,10 +48,8 @@ void MaterialGenerator::ProcessGroupTools(Box* box, Node* node, Value& value)
}
// Time
case 3:
{
value = getTime;
value = box->ID == 1 ? getUnscaledTime : getTime;
break;
}
// Panner
case 6:
{

View File

@@ -107,6 +107,7 @@ bool FeatureData::Init()
MaterialValue MaterialGenerator::getUVs(VariantType::Float2, TEXT("input.TexCoord"));
MaterialValue MaterialGenerator::getTime(VariantType::Float, TEXT("TimeParam"));
MaterialValue MaterialGenerator::getUnscaledTime(VariantType::Float, TEXT("UnscaledTimeParam"));
MaterialValue MaterialGenerator::getNormal(VariantType::Float3, TEXT("input.TBN[2]"));
MaterialValue MaterialGenerator::getNormalZero(VariantType::Float3, TEXT("float3(0, 0, 1)"));
MaterialValue MaterialGenerator::getVertexColor(VariantType::Float4, TEXT("GetVertexColor(input)"));

View File

@@ -120,7 +120,6 @@ private:
MaterialValue _ddx, _ddy, _cameraVector;
public:
MaterialGenerator();
~MaterialGenerator();
@@ -211,6 +210,7 @@ public:
static MaterialValue getUVs;
static MaterialValue getTime;
static MaterialValue getUnscaledTime;
static MaterialValue getNormal;
static MaterialValue getNormalZero;
static MaterialValue getVertexColor;

View File

@@ -25,7 +25,7 @@ namespace FlaxEngine.GUI
/// Initializes a new instance of the <see cref="MaterialBrush"/> struct.
/// </summary>
/// <param name="material">The material.</param>
public MaterialBrush(Material material)
public MaterialBrush(MaterialBase material)
{
Material = material;
}

View File

@@ -134,6 +134,10 @@ namespace FlaxEngine.Utilities
if (isLeadingSlash)
Move();
// Dont process if wrong slash is used.
if (c =='\\')
return false;
// Parse tag
bool result = ParseTag(ref tag, name);
@@ -204,6 +208,10 @@ namespace FlaxEngine.Utilities
SkipWhitespace();
while (Peek() != '>')
{
// Return false if start of new html tag is detected.
if (Peek() == '<')
return false;
if (Peek() == '/')
{
// Handle trailing forward slash

View File

@@ -122,6 +122,10 @@ float4 GetLighting(float3 viewPos, LightData lightData, GBufferSample gBuffer, f
// Calculate shadow
ShadowSample shadow = GetShadow(lightData, gBuffer, shadowMask);
#if !LIGHTING_NO_DIRECTIONAL
// Directional shadowing
shadow.SurfaceShadow *= NoL;
#endif
// Calculate attenuation
if (isRadial)
@@ -135,11 +139,6 @@ float4 GetLighting(float3 viewPos, LightData lightData, GBufferSample gBuffer, f
shadow.TransmissionShadow *= attenuation;
}
#if !LIGHTING_NO_DIRECTIONAL
// Reduce shadow mapping artifacts
shadow.SurfaceShadow *= saturate(NoL * 6.0f - 0.2f) * NoL;
#endif
BRANCH
if (shadow.SurfaceShadow + shadow.TransmissionShadow > 0)
{

View File

@@ -176,6 +176,8 @@ cbuffer ViewData : register(b1)
float4 TemporalAAJitter;
float3 LargeWorldsChunkIndex;
float LargeWorldsChunkSize;
float3 ViewPadding0;
float UnscaledTimeParam;
};
#endif

View File

@@ -56,7 +56,7 @@ static const uint g_numTaps[4] = { 3, 5, 8, 12 };
#define SSAO_NORMAL_BASED_EDGES_ENABLE_AT_QUALITY_PRESET (2) // to disable simply set to 99 or similar
#define SSAO_NORMAL_BASED_EDGES_DOT_THRESHOLD (0.5) // use 0-0.1 for super-sharp normal-based edges
//
#define SSAO_DETAIL_AO_ENABLE_AT_QUALITY_PRESET (1) // whether to use DetailAOStrength; to disable simply set to 99 or similar
#define SSAO_DETAIL_AO_ENABLE_AT_QUALITY_PRESET (99) // whether to use DetailAOStrength; to disable simply set to 99 or similar
//
#define SSAO_DEPTH_MIPS_ENABLE_AT_QUALITY_PRESET (99) // !!warning!! the MIP generation on the C++ side will be enabled on quality preset 2 regardless of this value, so if changing here, change the C++ side too
#define SSAO_DEPTH_MIPS_GLOBAL_OFFSET (-4.3) // best noise/quality/performance tradeoff, found empirically

View File

@@ -513,6 +513,7 @@ namespace Flax.Build
// Combine build options from this module
project.CSharp.SystemReferences.AddRange(moduleBuildOptions.ScriptingAPI.SystemReferences);
project.CSharp.FileReferences.AddRange(moduleBuildOptions.ScriptingAPI.FileReferences);
project.CSharp.NugetPackageReferences.AddRange(moduleBuildOptions.NugetPackageReferences);
// Find references based on the modules dependencies (external or from projects)
foreach (var dependencyName in moduleBuildOptions.PublicDependencies.Concat(moduleBuildOptions.PrivateDependencies))

View File

@@ -109,6 +109,7 @@ namespace Flax.Build
// Merge module into target environment
buildData.TargetOptions.LinkEnv.InputFiles.AddRange(moduleOptions.OutputFiles);
buildData.TargetOptions.DependencyFiles.AddRange(moduleOptions.DependencyFiles);
buildData.TargetOptions.NugetPackageReferences.AddRange(moduleOptions.NugetPackageReferences);
buildData.TargetOptions.OptionalDependencyFiles.AddRange(moduleOptions.OptionalDependencyFiles);
buildData.TargetOptions.Libraries.AddRange(moduleOptions.Libraries);
buildData.TargetOptions.DelayLoadLibraries.AddRange(moduleOptions.DelayLoadLibraries);
@@ -141,6 +142,19 @@ namespace Flax.Build
var dstFile = Path.Combine(outputPath, Path.GetFileName(srcFile));
graph.AddCopyFile(dstFile, srcFile);
}
if (targetBuildOptions.NugetPackageReferences.Any())
{
var nugetPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages");
foreach (var reference in targetBuildOptions.NugetPackageReferences)
{
var path = Path.Combine(nugetPath, reference.Name, reference.Version, "lib", reference.Framework, $"{reference.Name}.dll");
if (!File.Exists(path))
Utilities.RestoreNugetPackages(graph, target);
var dstFile = Path.Combine(outputPath, Path.GetFileName(path));
graph.AddCopyFile(dstFile, path);
}
}
}
}
@@ -283,6 +297,18 @@ namespace Flax.Build
args.Add(string.Format("/reference:\"{0}{1}.dll\"", referenceAssemblies, reference));
foreach (var reference in fileReferences)
args.Add(string.Format("/reference:\"{0}\"", reference));
// Reference Nuget package
if (buildData.TargetOptions.NugetPackageReferences.Any())
{
var nugetPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages");
foreach (var reference in buildOptions.NugetPackageReferences)
{
var path = Path.Combine(nugetPath, reference.Name, reference.Version, "lib", reference.Framework, $"{reference.Name}.dll");
args.Add(string.Format("/reference:\"{0}\"", path));
}
}
#if USE_NETCORE
foreach (var systemAnalyzer in buildOptions.ScriptingAPI.SystemAnalyzers)
args.Add(string.Format("/analyzer:\"{0}{1}.dll\"", referenceAnalyzers, systemAnalyzer));

View File

@@ -220,6 +220,7 @@ namespace Flax.Build
exeBuildOptions.LinkEnv.InputLibraries.Add(Path.Combine(buildOptions.OutputFolder, buildOptions.Platform.GetLinkOutputFileName(LibraryName, engineLibraryType)));
exeBuildOptions.LinkEnv.InputFiles.AddRange(mainModuleOptions.OutputFiles);
exeBuildOptions.DependencyFiles.AddRange(mainModuleOptions.DependencyFiles);
exeBuildOptions.NugetPackageReferences.AddRange(mainModuleOptions.NugetPackageReferences);
exeBuildOptions.OptionalDependencyFiles.AddRange(mainModuleOptions.OptionalDependencyFiles);
exeBuildOptions.Libraries.AddRange(mainModuleOptions.Libraries);
exeBuildOptions.DelayLoadLibraries.AddRange(mainModuleOptions.DelayLoadLibraries);

View File

@@ -70,6 +70,40 @@ namespace Flax.Build.NativeCpp
Annotations,
}
/// <summary>
/// Defines a Nuget Package
/// </summary>
public struct NugetPackage
{
/// <summary>
/// The name of the nuget package.
/// </summary>
public string Name;
/// <summary>
/// The version of the nuget package.
/// </summary>
public string Version;
/// <summary>
/// The target framework. ex. net8.0, netstandard2.1
/// </summary>
public string Framework;
/// <summary>
/// Initialize the nuget package.
/// </summary>
/// <param name="name">The name of the package.</param>
/// <param name="version">The version of the package.</param>
/// <param name="framework">The target framework. ex. net8.0, netstandard2.1, etc.</param>
public NugetPackage(string name, string version, string framework)
{
Name = name;
Version = version;
Framework = framework;
}
}
/// <summary>
/// The native C++ module build settings container.
/// </summary>
@@ -129,6 +163,11 @@ namespace Flax.Build.NativeCpp
/// The collection of the modules that are required by this module (for linking).
/// </summary>
public List<string> PrivateDependencies = new List<string>();
/// <summary>
/// The nuget package references.
/// </summary>
public List<NugetPackage> NugetPackageReferences = new List<NugetPackage>();
/// <summary>
/// The collection of defines with preprocessing symbol for a source files of this module. Inherited by the modules that include it.

View File

@@ -425,6 +425,7 @@ namespace Flax.Build
moduleOptions.LinkEnv.InputFiles.AddRange(dependencyOptions.OutputFiles);
moduleOptions.DependencyFiles.AddRange(dependencyOptions.DependencyFiles);
moduleOptions.OptionalDependencyFiles.AddRange(dependencyOptions.OptionalDependencyFiles);
moduleOptions.NugetPackageReferences.AddRange(dependencyOptions.NugetPackageReferences);
moduleOptions.PrivateIncludePaths.AddRange(dependencyOptions.PublicIncludePaths);
moduleOptions.Libraries.AddRange(dependencyOptions.Libraries);
moduleOptions.DelayLoadLibraries.AddRange(dependencyOptions.DelayLoadLibraries);
@@ -440,6 +441,7 @@ namespace Flax.Build
moduleOptions.LinkEnv.InputFiles.AddRange(dependencyOptions.OutputFiles);
moduleOptions.DependencyFiles.AddRange(dependencyOptions.DependencyFiles);
moduleOptions.OptionalDependencyFiles.AddRange(dependencyOptions.OptionalDependencyFiles);
moduleOptions.NugetPackageReferences.AddRange(dependencyOptions.NugetPackageReferences);
moduleOptions.PublicIncludePaths.AddRange(dependencyOptions.PublicIncludePaths);
moduleOptions.Libraries.AddRange(dependencyOptions.Libraries);
moduleOptions.DelayLoadLibraries.AddRange(dependencyOptions.DelayLoadLibraries);
@@ -934,6 +936,7 @@ namespace Flax.Build
buildData.TargetOptions.LinkEnv.InputFiles.AddRange(moduleOptions.OutputFiles);
buildData.TargetOptions.DependencyFiles.AddRange(moduleOptions.DependencyFiles);
buildData.TargetOptions.OptionalDependencyFiles.AddRange(moduleOptions.OptionalDependencyFiles);
buildData.TargetOptions.NugetPackageReferences.AddRange(moduleOptions.NugetPackageReferences);
buildData.TargetOptions.Libraries.AddRange(moduleOptions.Libraries);
buildData.TargetOptions.DelayLoadLibraries.AddRange(moduleOptions.DelayLoadLibraries);
buildData.TargetOptions.ScriptingAPI.Add(moduleOptions.ScriptingAPI);
@@ -1054,6 +1057,19 @@ namespace Flax.Build
var dstFile = Path.Combine(outputPath, Path.GetFileName(srcFile));
graph.AddCopyFile(dstFile, srcFile);
}
if (targetBuildOptions.NugetPackageReferences.Any())
{
var nugetPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages");
foreach (var reference in targetBuildOptions.NugetPackageReferences)
{
var path = Path.Combine(nugetPath, reference.Name, reference.Version, "lib", reference.Framework, $"{reference.Name}.dll");
if (!File.Exists(path))
Utilities.RestoreNugetPackages(graph, target);
var dstFile = Path.Combine(outputPath, Path.GetFileName(path));
graph.AddCopyFile(dstFile, path);
}
}
}
}
@@ -1192,6 +1208,7 @@ namespace Flax.Build
buildData.TargetOptions.ExternalModules.AddRange(moduleOptions.ExternalModules);
buildData.TargetOptions.DependencyFiles.AddRange(moduleOptions.DependencyFiles);
buildData.TargetOptions.OptionalDependencyFiles.AddRange(moduleOptions.OptionalDependencyFiles);
buildData.TargetOptions.NugetPackageReferences.AddRange(moduleOptions.NugetPackageReferences);
}
}
}
@@ -1253,6 +1270,19 @@ namespace Flax.Build
var dstFile = Path.Combine(outputPath, Path.GetFileName(srcFile));
graph.AddCopyFile(dstFile, srcFile);
}
if (targetBuildOptions.NugetPackageReferences.Any())
{
var nugetPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages");
foreach (var reference in targetBuildOptions.NugetPackageReferences)
{
var path = Path.Combine(nugetPath, reference.Name, reference.Version, "lib", reference.Framework, $"{reference.Name}.dll");
if (!File.Exists(path))
Utilities.RestoreNugetPackages(graph, target);
var dstFile = Path.Combine(outputPath, Path.GetFileName(path));
graph.AddCopyFile(dstFile, path);
}
}
}
}

View File

@@ -229,6 +229,11 @@ namespace Flax.Build.Projects
/// The .Net libraries references (dll or exe files paths).
/// </summary>
public HashSet<string> FileReferences;
/// <summary>
/// The nuget references.
/// </summary>
public HashSet<NugetPackage> NugetPackageReferences;
/// <summary>
/// The output folder path (optional).
@@ -248,6 +253,7 @@ namespace Flax.Build.Projects
{
SystemReferences = new HashSet<string>(),
FileReferences = new HashSet<string>(),
NugetPackageReferences = new HashSet<NugetPackage>(),
};
/// <summary>

View File

@@ -175,6 +175,19 @@ namespace Flax.Build.Projects.VisualStudio
csProjectFileContent.AppendLine(" </PropertyGroup>");
}
// Nuget
if (project.CSharp.NugetPackageReferences.Any())
{
csProjectFileContent.AppendLine(" <ItemGroup>");
foreach (var reference in project.CSharp.NugetPackageReferences)
{
csProjectFileContent.AppendLine(string.Format(" <PackageReference Include=\"{0}\" Version=\"{1}\" />", reference.Name, reference.Version));
}
csProjectFileContent.AppendLine(" </ItemGroup>");
}
// References
csProjectFileContent.AppendLine(" <ItemGroup>");

View File

@@ -134,6 +134,20 @@ namespace Flax.Build.Projects.VisualStudio
}
// References
// Nuget
if (project.CSharp.NugetPackageReferences.Any())
{
csProjectFileContent.AppendLine(" <ItemGroup>");
foreach (var reference in project.CSharp.NugetPackageReferences)
{
csProjectFileContent.AppendLine(string.Format(" <PackageReference Include=\"{0}\" Version=\"{1}\" />", reference.Name, reference.Version));
}
csProjectFileContent.AppendLine(" </ItemGroup>");
csProjectFileContent.AppendLine("");
}
csProjectFileContent.AppendLine(" <ItemGroup>");

View File

@@ -16,6 +16,47 @@ namespace Flax.Build
/// </summary>
public static class Utilities
{
/// <summary>
/// Gets the .Net SDK path.
/// </summary>
/// <returns>The path.</returns>
public static string GetDotNetPath()
{
var buildPlatform = Platform.BuildTargetPlatform;
var dotnetSdk = DotNetSdk.Instance;
if (!dotnetSdk.IsValid)
throw new DotNetSdk.MissingException();
var dotnetPath = "dotnet";
switch (buildPlatform)
{
case TargetPlatform.Windows:
dotnetPath = Path.Combine(dotnetSdk.RootPath, "dotnet.exe");
break;
case TargetPlatform.Linux: break;
case TargetPlatform.Mac:
dotnetPath = Path.Combine(dotnetSdk.RootPath, "dotnet");
break;
default: throw new InvalidPlatformException(buildPlatform);
}
return dotnetPath;
}
/// <summary>
/// Restores a targets nuget packages.
/// </summary>
/// <param name="graph">The task graph.</param>
/// <param name="target">The target.</param>
/// <param name="dotNetPath">The dotnet path.</param>
public static void RestoreNugetPackages(Graph.TaskGraph graph, Target target)
{
var dotNetPath = GetDotNetPath();
var task = graph.Add<Graph.Task>();
task.WorkingDirectory = target.FolderPath;
task.InfoMessage = $"Restoring Nuget Packages for {target.Name}";
task.CommandPath = dotNetPath;
task.CommandArguments = $"restore";
}
/// <summary>
/// Gets the hash code for the string (the same for all platforms). Matches Engine algorithm for string hashing.
/// </summary>