From 1a8f396766211ef4345fe521ec601c53d81d5caf Mon Sep 17 00:00:00 2001 From: Syntriax Date: Fri, 23 Jan 2026 12:34:41 +0300 Subject: [PATCH 01/51] fix: cs files encoding being set as utf-8-bom --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index b314c0c..4803563 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,7 +13,7 @@ spelling_exclusion_path = SpellingExclusions.dic [*.{cs,csx,vb,vbx}] indent_size = 4 insert_final_newline = true -charset = utf-8-bom +charset = utf-8 # XML project files [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] -- 2.49.1 From ee58e60ef196564d58fa78ed28a691521bac098b Mon Sep 17 00:00:00 2001 From: Syntriax Date: Mon, 26 Jan 2026 12:15:42 +0300 Subject: [PATCH 02/51] fix: universe entrance issues caused by wrong logic --- .../Systems/UniverseEntranceManager.cs | 49 +++---------------- 1 file changed, 8 insertions(+), 41 deletions(-) diff --git a/Engine.Core/Systems/UniverseEntranceManager.cs b/Engine.Core/Systems/UniverseEntranceManager.cs index 85f579e..e2919e1 100644 --- a/Engine.Core/Systems/UniverseEntranceManager.cs +++ b/Engine.Core/Systems/UniverseEntranceManager.cs @@ -9,9 +9,7 @@ public class UniverseEntranceManager : Internal.BehaviourIndependent private static System.Func GetPriority() => (b) => b.Priority; private readonly ActiveBehaviourCollectorOrdered enterUniverses = new(GetPriority(), SortByAscendingPriority()); - - private readonly List toCallEnterUniverses = new(32); - private readonly List toCallExitUniverses = new(32); + private readonly ActiveBehaviourCollectorOrdered exitUniverses = new(GetPriority(), SortByAscendingPriority()); protected override void OnEnteredUniverse(IUniverse universe) { @@ -20,54 +18,23 @@ public class UniverseEntranceManager : Internal.BehaviourIndependent // event it tries to call OnUniverseObjectRegistered again on the same object, causing a duplicate entry error. Debug.Assert.AssertTrue(BehaviourController.Count == 1, $"{nameof(UniverseEntranceManager)} must be in it's own {nameof(IUniverseObject)} with no other {nameof(IBehaviour)}s attached at the moment. Failing to do so might cause instantiation or serialization issues."); enterUniverses.Assign(universe); - - foreach (IUniverseObject universeObject in universe) - OnUniverseObjectRegistered(universe, new(universeObject)); - - universe.OnUniverseObjectRegistered.AddListener(OnUniverseObjectRegistered); - universe.OnUniverseObjectUnRegistered.AddListener(OnUniverseObjectUnRegistered); + exitUniverses.Assign(universe); } protected override void OnExitedUniverse(IUniverse universe) { enterUniverses.Unassign(); - - foreach (IUniverseObject universeObject in universe) - OnUniverseObjectUnRegistered(universe, new(universeObject)); - - universe.OnUniverseObjectRegistered.RemoveListener(OnUniverseObjectRegistered); - universe.OnUniverseObjectUnRegistered.RemoveListener(OnUniverseObjectUnRegistered); + exitUniverses.Unassign(); } - - private void OnUniverseObjectUnRegistered(IUniverse sender, IUniverse.UniverseObjectUnRegisteredArguments args) - { - args.UniverseObjectUnregistered.BehaviourController.GetBehavioursInChildren(toCallExitUniverses); - - for (int i = toCallExitUniverses.Count - 1; i >= 0; i--) - { - IExitUniverse exitUniverse = toCallExitUniverses[i]; - toCallExitUniverses.RemoveAt(i); - exitUniverse.ExitUniverse(Universe); - } - } - - private void OnUniverseObjectRegistered(IUniverse sender, IUniverse.UniverseObjectRegisteredArguments args) - { - for (int i = toCallEnterUniverses.Count - 1; i >= 0; i--) - { - IEnterUniverse enterUniverse = toCallEnterUniverses[i]; - toCallEnterUniverses.RemoveAt(i); - enterUniverse.EnterUniverse(Universe); - } - } - private void OnEnterUniverseCollected(IBehaviourCollector sender, IBehaviourCollector.BehaviourCollectedArguments args) - { - toCallEnterUniverses.Add(args.BehaviourCollected); - } + => args.BehaviourCollected.EnterUniverse(Universe); + + private void OnExitUniverseRemoved(IBehaviourCollector sender, IBehaviourCollector.BehaviourRemovedArguments args) + => args.BehaviourRemoved.ExitUniverse(Universe); public UniverseEntranceManager() { enterUniverses.OnCollected.AddListener(OnEnterUniverseCollected); + exitUniverses.OnRemoved.AddListener(OnExitUniverseRemoved); } } -- 2.49.1 From 5ce5e4eb0b4a4a0d37290e24d02fb94767c9e120 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Mon, 26 Jan 2026 21:48:15 +0300 Subject: [PATCH 03/51] feat: added inverse & float scale methods to 4x4 matrices --- Engine.Core/Primitives/Matrix4x4.cs | 86 +++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/Engine.Core/Primitives/Matrix4x4.cs b/Engine.Core/Primitives/Matrix4x4.cs index 0288b41..9bce992 100644 --- a/Engine.Core/Primitives/Matrix4x4.cs +++ b/Engine.Core/Primitives/Matrix4x4.cs @@ -62,6 +62,11 @@ public readonly struct Matrix4x4( 0f, 0f, 0f, 1f ); + /// + /// Represents the inverted version of this . + /// + public Matrix4x4 Inverse => Invert(this); + public static Matrix4x4 operator *(Matrix4x4 a, Matrix4x4 b) => new( a.M11 * b.M11 + a.M12 * b.M21 + a.M13 * b.M31 + a.M14 * b.M41, a.M11 * b.M12 + a.M12 * b.M22 + a.M13 * b.M32 + a.M14 * b.M42, @@ -129,6 +134,74 @@ public readonly struct Matrix4x4( m.M13 * m.M21 * m.M32 * m.M44 - m.M11 * m.M23 * m.M32 * m.M44 - m.M12 * m.M21 * m.M33 * m.M44 + m.M11 * m.M22 * m.M33 * m.M44; + /// + /// Inverts the given . + /// + /// The . + /// The inverted of the given . + public static Matrix4x4 Invert(Matrix4x4 m) + { + float m1 = m.M11, m2 = m.M12, m3 = m.M13, m4 = m.M14; + float m5 = m.M21, m6 = m.M22, m7 = m.M23, m8 = m.M24; + float m9 = m.M31, m10 = m.M32, m11 = m.M33, m12 = m.M34; + float m13 = m.M41, m14 = m.M42, m15 = m.M43, m16 = m.M44; + + float num = m11 * m16 - m12 * m15; + float num2 = m10 * m16 - m12 * m14; + float num3 = m10 * m15 - m11 * m14; + float num4 = m9 * m16 - m12 * m13; + float num5 = m9 * m15 - m11 * m13; + float num6 = m9 * m14 - m10 * m13; + + float num7 = m6 * num - m7 * num2 + m8 * num3; + float num8 = -(m5 * num - m7 * num4 + m8 * num5); + float num9 = m5 * num2 - m6 * num4 + m8 * num6; + float num10 = -(m5 * num3 - m6 * num5 + m7 * num6); + + float invDet = 1f / (m1 * num7 + m2 * num8 + m3 * num9 + m4 * num10); + + float r11 = num7 * invDet; + float r21 = num8 * invDet; + float r31 = num9 * invDet; + float r41 = num10 * invDet; + + float r12 = (-(m2 * num - m3 * num2 + m4 * num3)) * invDet; + float r22 = (m1 * num - m3 * num4 + m4 * num5) * invDet; + float r32 = (-(m1 * num2 - m2 * num4 + m4 * num6)) * invDet; + float r42 = (m1 * num3 - m2 * num5 + m3 * num6) * invDet; + + float num12 = m7 * m16 - m8 * m15; + float num13 = m6 * m16 - m8 * m14; + float num14 = m6 * m15 - m7 * m14; + float num15 = m5 * m16 - m8 * m13; + float num16 = m5 * m15 - m7 * m13; + float num17 = m5 * m14 - m6 * m13; + + float r13 = (m2 * num12 - m3 * num13 + m4 * num14) * invDet; + float r23 = (-(m1 * num12 - m3 * num15 + m4 * num16)) * invDet; + float r33 = (m1 * num13 - m2 * num15 + m4 * num17) * invDet; + float r43 = (-(m1 * num14 - m2 * num16 + m3 * num17)) * invDet; + + float num18 = m7 * m12 - m8 * m11; + float num19 = m6 * m12 - m8 * m10; + float num20 = m6 * m11 - m7 * m10; + float num21 = m5 * m12 - m8 * m9; + float num22 = m5 * m11 - m7 * m9; + float num23 = m5 * m10 - m6 * m9; + + float r14 = (-(m2 * num18 - m3 * num19 + m4 * num20)) * invDet; + float r24 = (m1 * num18 - m3 * num21 + m4 * num22) * invDet; + float r34 = (-(m1 * num19 - m2 * num21 + m4 * num23)) * invDet; + float r44 = (m1 * num20 - m2 * num22 + m3 * num23) * invDet; + + return new( + r11, r12, r13, r14, + r21, r22, r23, r24, + r31, r32, r33, r34, + r41, r42, r43, r44 + ); + } + public static Matrix4x4 CreateTranslation(Vector3D position) => new( 1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, @@ -136,6 +209,13 @@ public readonly struct Matrix4x4( position.X, position.Y, position.Z, 1 ); + public static Matrix4x4 CreateScale(float scale) => new( + scale, 0f, 0f, 0f, + 0f, scale, 0f, 0f, + 0f, 0f, scale, 0f, + 0f, 0f, 0f, 1f + ); + public static Matrix4x4 CreateScale(Vector3D scale) => new( scale.X, 0f, 0f, 0f, 0f, scale.Y, 0f, 0f, @@ -261,12 +341,18 @@ public static class Matrix4x4Extensions /// public static float Determinant(this Matrix4x4 matrix) => Matrix4x4.Determinant(matrix); + /// + public static Matrix4x4 Invert(this Matrix4x4 matrix) => Matrix4x4.Invert(matrix); + /// public static Matrix4x4 ApplyTranslation(this Matrix4x4 matrix, Vector3D translation) => matrix * Matrix4x4.CreateTranslation(translation); /// public static Matrix4x4 ApplyScale(this Matrix4x4 matrix, Vector3 scale) => matrix * Matrix4x4.CreateScale(scale); + /// + public static Matrix4x4 ApplyScale(this Matrix4x4 matrix, float scale) => matrix * Matrix4x4.CreateScale(scale); + /// public static Matrix4x4 ApplyRotationX(this Matrix4x4 matrix, float radians) => matrix * Matrix4x4.CreateRotationX(radians); -- 2.49.1 From e2820670c612aafce52b0942f971447d71254af4 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Mon, 26 Jan 2026 22:01:36 +0300 Subject: [PATCH 04/51] fix: classname x filename inconsistency fixed --- Engine.Core/{BehaviourInternal.cs => BehaviourIndependent.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Engine.Core/{BehaviourInternal.cs => BehaviourIndependent.cs} (100%) diff --git a/Engine.Core/BehaviourInternal.cs b/Engine.Core/BehaviourIndependent.cs similarity index 100% rename from Engine.Core/BehaviourInternal.cs rename to Engine.Core/BehaviourIndependent.cs -- 2.49.1 From af6dde84fde1d3d9ec2246ec47becfcefb91207b Mon Sep 17 00:00:00 2001 From: Syntriax Date: Mon, 26 Jan 2026 23:23:49 +0300 Subject: [PATCH 05/51] fix: universe entrance issues where entering behaviours adding more behaviours causing duplicate/missing calls --- .../Systems/UniverseEntranceManager.cs | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/Engine.Core/Systems/UniverseEntranceManager.cs b/Engine.Core/Systems/UniverseEntranceManager.cs index e2919e1..db40ce7 100644 --- a/Engine.Core/Systems/UniverseEntranceManager.cs +++ b/Engine.Core/Systems/UniverseEntranceManager.cs @@ -11,14 +11,29 @@ public class UniverseEntranceManager : Internal.BehaviourIndependent private readonly ActiveBehaviourCollectorOrdered enterUniverses = new(GetPriority(), SortByAscendingPriority()); private readonly ActiveBehaviourCollectorOrdered exitUniverses = new(GetPriority(), SortByAscendingPriority()); + private bool isInitialCollectionDone = false; + private readonly FastListOrdered toCallEnterUniverses = new(GetPriority(), SortByAscendingPriority()); + protected override void OnEnteredUniverse(IUniverse universe) { - // FIXME: This causes an issue when the UniverseEntranceManager is already attached to a UniverseObject then registered into a Universe, - // the enter/exit universe collectors call OnUniverseObjectRegistered internally on Assign, but since the Universe calls the OnUniverseObjectRegistered - // event it tries to call OnUniverseObjectRegistered again on the same object, causing a duplicate entry error. - Debug.Assert.AssertTrue(BehaviourController.Count == 1, $"{nameof(UniverseEntranceManager)} must be in it's own {nameof(IUniverseObject)} with no other {nameof(IBehaviour)}s attached at the moment. Failing to do so might cause instantiation or serialization issues."); - enterUniverses.Assign(universe); exitUniverses.Assign(universe); + + // FIXME: the isInitialCollectionDone is for the sole reason of some behaviours + // adding more behaviours during entrance calls and the internal workings of + // behaviour collector not being able to tell which behaviour was already called + // (because it just runs a for loop with the behaviour count, and priority ordering doesn't help as well) + // so it sometimes double processes or misses behaviours. A more elegant way of + // handling this would be nice but for now it works good enough. + // + // SIDE NOTE: This same issue has the potential to occur on exitUniverses as well, but I've yet to run + // into an instance of it actually happening so... I'm not gonna touch it until the edge case happens. + isInitialCollectionDone = false; + enterUniverses.Assign(universe); + isInitialCollectionDone = true; + + for (int i = toCallEnterUniverses.Count - 1; i >= 0; i--) + toCallEnterUniverses[i].EnterUniverse(universe); + toCallEnterUniverses.Clear(); } protected override void OnExitedUniverse(IUniverse universe) @@ -26,8 +41,17 @@ public class UniverseEntranceManager : Internal.BehaviourIndependent enterUniverses.Unassign(); exitUniverses.Unassign(); } + private void OnEnterUniverseCollected(IBehaviourCollector sender, IBehaviourCollector.BehaviourCollectedArguments args) - => args.BehaviourCollected.EnterUniverse(Universe); + { + if (!isInitialCollectionDone) + { + toCallEnterUniverses.Add(args.BehaviourCollected); + return; + } + + args.BehaviourCollected.EnterUniverse(Universe); + } private void OnExitUniverseRemoved(IBehaviourCollector sender, IBehaviourCollector.BehaviourRemovedArguments args) => args.BehaviourRemoved.ExitUniverse(Universe); -- 2.49.1 From 7c9973a5e7674f0706a86570ccd4ccc035ed0bae Mon Sep 17 00:00:00 2001 From: Syntriax Date: Mon, 26 Jan 2026 23:48:16 +0300 Subject: [PATCH 06/51] refactor: ICamera interface added with view and projection matrices fields --- Engine.Core/Abstract/ICamera.cs | 17 +++++++ Engine.Core/Abstract/ICamera2D.cs | 2 +- Engine.Core/Abstract/ICamera3D.cs | 2 +- .../Behaviours/MonoGameCamera2D.cs | 49 ++++++++++--------- .../Behaviours/MonoGameCamera3D.cs | 29 ++++++----- .../EngineConverter.cs | 8 +++ 6 files changed, 69 insertions(+), 38 deletions(-) create mode 100644 Engine.Core/Abstract/ICamera.cs diff --git a/Engine.Core/Abstract/ICamera.cs b/Engine.Core/Abstract/ICamera.cs new file mode 100644 index 0000000..850bce9 --- /dev/null +++ b/Engine.Core/Abstract/ICamera.cs @@ -0,0 +1,17 @@ +namespace Engine.Core; + +/// +/// Represents a camera with view and projections matrices in the engine. +/// +public interface ICamera +{ + /// + /// View of the . + /// + Matrix4x4 ViewMatrix { get; } + + /// + /// Projection of the . + /// + Matrix4x4 ProjectionMatrix { get; } +} diff --git a/Engine.Core/Abstract/ICamera2D.cs b/Engine.Core/Abstract/ICamera2D.cs index 4c12105..941fcd5 100644 --- a/Engine.Core/Abstract/ICamera2D.cs +++ b/Engine.Core/Abstract/ICamera2D.cs @@ -3,7 +3,7 @@ namespace Engine.Core; /// /// Represents a 2D camera in the engine. /// -public interface ICamera2D : IBehaviour2D +public interface ICamera2D : ICamera, IBehaviour2D { /// /// The zoom level of the camera. diff --git a/Engine.Core/Abstract/ICamera3D.cs b/Engine.Core/Abstract/ICamera3D.cs index a0024a0..b2346bb 100644 --- a/Engine.Core/Abstract/ICamera3D.cs +++ b/Engine.Core/Abstract/ICamera3D.cs @@ -3,7 +3,7 @@ namespace Engine.Core; /// /// Represents a 3D camera in the engine. /// -public interface ICamera3D : IBehaviour3D +public interface ICamera3D : ICamera, IBehaviour3D { /// /// Event triggered when the near plane of the changes. diff --git a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera2D.cs b/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera2D.cs index 6811eb9..7b0a180 100644 --- a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera2D.cs +++ b/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera2D.cs @@ -7,31 +7,39 @@ namespace Engine.Integration.MonoGame; public class MonoGameCamera2D : Behaviour, ICamera2D, IFirstFrameUpdate, ILastFrameUpdate, IPreDraw { - public Event OnMatrixTransformChanged { get; } = new(); + public Event OnViewMatrixChanged { get; } = new(); + public Event OnProjectionMatrixChanged { get; } = new(); public Event OnViewportChanged { get; } = new(); public Event OnZoomChanged { get; } = new(); public GraphicsDeviceManager Graphics { get; private set; } = null!; public ITransform2D Transform { get; private set; } = null!; - public Matrix MatrixTransform + public Matrix4x4 ProjectionMatrix { get; - set + private set { if (field == value) return; field = value; - OnMatrixTransformChanged.Invoke(this); + OnProjectionMatrixChanged.Invoke(this); } - } = Matrix.Identity; + } = Matrix4x4.Identity; - public Vector2D Position + public Matrix4x4 ViewMatrix { - get => Transform.Position; - set => Transform.Position = value; - } + get; + private set + { + if (field == value) + return; + + field = value; + OnViewMatrixChanged.Invoke(this); + } + } = Matrix4x4.Identity; public Viewport Viewport { @@ -61,21 +69,15 @@ public class MonoGameCamera2D : Behaviour, ICamera2D, IFirstFrameUpdate, ILastFr } } = 1f; - public float Rotation - { - get => Transform.Rotation; - set => Transform.Rotation = value; - } - // TODO This causes delay since OnPreDraw calls assuming this is called in in Update public Vector2D ScreenToWorldPosition(Vector2D screenPosition) { - Vector2D worldPosition = Vector2.Transform(screenPosition.ToVector2(), Matrix.Invert(MatrixTransform)).ToVector2D(); + Vector2D worldPosition = Vector2.Transform(screenPosition.ToVector2(), ViewMatrix.Inverse.ToXnaMatrix()).ToVector2D(); return worldPosition.Scale(EngineConverterExtensions.screenScale); } public Vector2D WorldToScreenPosition(Vector2D worldPosition) { - Vector2D screenPosition = Vector2.Transform(worldPosition.ToVector2(), MatrixTransform).ToVector2D(); + Vector2D screenPosition = Vector2.Transform(worldPosition.ToVector2(), ViewMatrix.ToXnaMatrix()).ToVector2D(); return screenPosition.Scale(EngineConverterExtensions.screenScale); } @@ -89,11 +91,12 @@ public class MonoGameCamera2D : Behaviour, ICamera2D, IFirstFrameUpdate, ILastFr public void PreDraw() { - MatrixTransform = - Matrix.CreateTranslation(new Vector3(-Position.X, Position.Y, 0f)) * - Matrix.CreateRotationZ(Rotation * Math.DegreeToRadian) * - Matrix.CreateScale(Transform.Scale.X.Max(Transform.Scale.Y)) * - Matrix.CreateScale(Zoom) * - Matrix.CreateTranslation(new Vector3(Viewport.Width * .5f, Viewport.Height * .5f, 0f)); + ProjectionMatrix = Matrix.CreateOrthographicOffCenter(Viewport.X, Viewport.Width, Viewport.Height, Viewport.Y, 0, 1).FromXnaMatrix(); + ViewMatrix = + Matrix4x4.CreateTranslation(new Vector3D(-Transform.Position.X, Transform.Position.Y, 0f)) * + Matrix4x4.CreateRotationZ(Transform.Rotation * Math.DegreeToRadian) * + Matrix4x4.CreateScale(Transform.Scale.X.Max(Transform.Scale.Y)) * + Matrix4x4.CreateScale(Zoom) * + Matrix4x4.CreateTranslation(new Vector3D(Viewport.Width * .5f, Viewport.Height * .5f, 0f)); } } diff --git a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera3D.cs b/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera3D.cs index 5516551..1784c85 100644 --- a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera3D.cs +++ b/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera3D.cs @@ -21,7 +21,7 @@ public class MonoGameCamera3D : Behaviour, ICamera3D, IFirstFrameUpdate, ILastFr public GraphicsDeviceManager Graphics { get; private set; } = null!; public ITransform3D Transform { get; private set; } = null!; - public Matrix View + public Matrix4x4 ViewMatrix { get; set @@ -29,13 +29,13 @@ public class MonoGameCamera3D : Behaviour, ICamera3D, IFirstFrameUpdate, ILastFr if (field == value) return; - Matrix previousView = field; + Matrix4x4 previousView = field; field = value; OnViewChanged.Invoke(this, new(previousView)); } - } = Matrix.Identity; + } = Matrix4x4.Identity; - public Matrix Projection + public Matrix4x4 ProjectionMatrix { get; set @@ -43,11 +43,11 @@ public class MonoGameCamera3D : Behaviour, ICamera3D, IFirstFrameUpdate, ILastFr if (field == value) return; - Matrix previousProjection = field; + Matrix4x4 previousProjection = field; field = value; OnProjectionChanged.Invoke(this, new(previousProjection)); } - } = Matrix.Identity; + } = Matrix4x4.Identity; public Viewport Viewport { @@ -111,14 +111,17 @@ public class MonoGameCamera3D : Behaviour, ICamera3D, IFirstFrameUpdate, ILastFr Vector3 nearPoint = new(screenPosition.X, screenPosition.Y, 0f); Vector3 farPoint = new(screenPosition.X, screenPosition.Y, 1f); - Vector3 worldNear = Viewport.Unproject(nearPoint, Projection, View, Matrix.Identity); - Vector3 worldFar = Viewport.Unproject(farPoint, Projection, View, Matrix.Identity); + Matrix projection = ProjectionMatrix.ToXnaMatrix(); + Matrix view = ViewMatrix.ToXnaMatrix(); + + Vector3 worldNear = Viewport.Unproject(nearPoint, projection, view, Matrix.Identity); + Vector3 worldFar = Viewport.Unproject(farPoint, projection, view, Matrix.Identity); Vector3 direction = Vector3.Normalize(worldFar - worldNear); return new(worldNear.ToVector3D(), direction.ToVector3D()); } - public Vector2D WorldToScreenPosition(Vector3D worldPosition) => Viewport.Project(worldPosition.ToVector3(), Projection, View, Matrix.Identity).ToVector3D(); + public Vector2D WorldToScreenPosition(Vector3D worldPosition) => Viewport.Project(worldPosition.ToVector3(), ProjectionMatrix.ToXnaMatrix(), ViewMatrix.ToXnaMatrix(), Matrix.Identity).ToVector3D(); public void LastActiveFrame() => Transform.OnTransformUpdated.RemoveListener(SetDirtyOnTransformUpdate); public void FirstActiveFrame() @@ -150,7 +153,7 @@ public class MonoGameCamera3D : Behaviour, ICamera3D, IFirstFrameUpdate, ILastFr Vector3 right = Vector3.Normalize(Transform.Right.ToVector3()); Vector3 position = Transform.Position.ToVector3(); - View = new Matrix( + ViewMatrix = new Matrix4x4( right.X, up.X, forward.X, 0, right.Y, up.Y, forward.Y, 0, right.Z, up.Z, forward.Z, 0, @@ -166,7 +169,7 @@ public class MonoGameCamera3D : Behaviour, ICamera3D, IFirstFrameUpdate, ILastFr float yScale = 1f / (float)Math.Tan(fieldOfView / 2f); float xScale = yScale / Viewport.AspectRatio; - Projection = new Matrix( + ProjectionMatrix = new Matrix4x4( xScale, 0, 0, 0, 0, yScale, 0, 0, 0, 0, FarPlane / (FarPlane - NearPlane), 1, @@ -174,7 +177,7 @@ public class MonoGameCamera3D : Behaviour, ICamera3D, IFirstFrameUpdate, ILastFr ); } - public readonly record struct ViewChangedArguments(Matrix PreviousView); - public readonly record struct ProjectionChangedArguments(Matrix PreviousProjection); + public readonly record struct ViewChangedArguments(Matrix4x4 PreviousView); + public readonly record struct ProjectionChangedArguments(Matrix4x4 PreviousProjection); public readonly record struct ViewportChangedArguments(Viewport PreviousViewport); } diff --git a/Engine.Integration/Engine.Integration.MonoGame/EngineConverter.cs b/Engine.Integration/Engine.Integration.MonoGame/EngineConverter.cs index 354b196..f30cc0c 100644 --- a/Engine.Integration/Engine.Integration.MonoGame/EngineConverter.cs +++ b/Engine.Integration/Engine.Integration.MonoGame/EngineConverter.cs @@ -49,6 +49,14 @@ public static class EngineConverterExtensions m.M41, m.M42, m.M43, m.M44 ); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Matrix4x4 FromXnaMatrix(this Matrix m) => new( + m.M11, m.M12, m.M13, m.M14, + m.M21, m.M22, m.M23, m.M24, + m.M31, m.M32, m.M33, m.M34, + m.M41, m.M42, m.M43, m.M44 + ); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Microsoft.Xna.Framework.Quaternion ToXnaQuaternion(this Core.Quaternion quaternion) => new(quaternion.X, quaternion.Y, quaternion.Z, quaternion.W); -- 2.49.1 From 985f8983274dd3dac414f0b7904aae8747450f4f Mon Sep 17 00:00:00 2001 From: Syntriax Date: Tue, 27 Jan 2026 13:04:04 +0300 Subject: [PATCH 07/51] refactor: moved drawable triangles into systems project for platform agnosticism --- .../Abstract/ITriangleBatch.cs | 12 ------ .../Behaviours/SpriteBatcher.cs | 2 +- .../Behaviours/TriangleBatcher.cs | 40 ------------------- ...angleBatch.cs => MonoGameTriangleBatch.cs} | 18 +++++---- .../Graphics/DrawableShape2D.cs | 10 ++--- .../Graphics}/IDrawableTriangle.cs | 2 +- Engine.Systems/Graphics/ITriangleBatch.cs | 10 +++++ Engine.Systems/Graphics/TriangleBatcher.cs | 33 +++++++++++++++ 8 files changed, 60 insertions(+), 67 deletions(-) delete mode 100644 Engine.Integration/Engine.Integration.MonoGame/Abstract/ITriangleBatch.cs delete mode 100644 Engine.Integration/Engine.Integration.MonoGame/Behaviours/TriangleBatcher.cs rename Engine.Integration/Engine.Integration.MonoGame/{TriangleBatch.cs => MonoGameTriangleBatch.cs} (78%) rename Engine.Integration/Engine.Integration.MonoGame/Behaviours/DrawableShape.cs => Engine.Systems/Graphics/DrawableShape2D.cs (66%) rename {Engine.Integration/Engine.Integration.MonoGame/Abstract => Engine.Systems/Graphics}/IDrawableTriangle.cs (75%) create mode 100644 Engine.Systems/Graphics/ITriangleBatch.cs create mode 100644 Engine.Systems/Graphics/TriangleBatcher.cs diff --git a/Engine.Integration/Engine.Integration.MonoGame/Abstract/ITriangleBatch.cs b/Engine.Integration/Engine.Integration.MonoGame/Abstract/ITriangleBatch.cs deleted file mode 100644 index 1de8f80..0000000 --- a/Engine.Integration/Engine.Integration.MonoGame/Abstract/ITriangleBatch.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.Xna.Framework; - -using Engine.Core; - -namespace Engine.Integration.MonoGame; - -public interface ITriangleBatch -{ - void Begin(Matrix? view = null, Matrix? projection = null); - void Draw(Triangle triangle, ColorRGBA colorRGBA); - void End(); -} diff --git a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/SpriteBatcher.cs b/Engine.Integration/Engine.Integration.MonoGame/Behaviours/SpriteBatcher.cs index b9ca10e..7ca0b88 100644 --- a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/SpriteBatcher.cs +++ b/Engine.Integration/Engine.Integration.MonoGame/Behaviours/SpriteBatcher.cs @@ -26,7 +26,7 @@ public class SpriteBatcher : Behaviour, IFirstFrameUpdate, IDraw public void Draw() { - spriteBatch.Begin(transformMatrix: camera2D.MatrixTransform); + spriteBatch.Begin(transformMatrix: camera2D.ViewMatrix.ToXnaMatrix()); for (int i = 0; i < drawableSprites.Count; i++) drawableSprites[i].Draw(spriteBatch); spriteBatch.End(); diff --git a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/TriangleBatcher.cs b/Engine.Integration/Engine.Integration.MonoGame/Behaviours/TriangleBatcher.cs deleted file mode 100644 index b8bf5d0..0000000 --- a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/TriangleBatcher.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.Generic; - -using Microsoft.Xna.Framework; - -using Engine.Core; - -namespace Engine.Integration.MonoGame; - -public class TriangleBatcher : Behaviour, ITriangleBatch, IFirstFrameUpdate, IDraw -{ - private static Comparer SortByAscendingPriority() => Comparer.Create((x, y) => x.CompareTo(y)); - private static System.Func GetPriority() => (b) => b.Priority; - - private TriangleBatch triangleBatch = null!; - private MonoGameCamera2D camera2D = null!; - - private readonly ActiveBehaviourCollectorOrdered drawableShapes = new(GetPriority(), SortByAscendingPriority()); - - public void FirstActiveFrame() - { - MonoGameWindowContainer windowContainer = BehaviourController.UniverseObject.Universe.FindRequiredBehaviour(); - camera2D = BehaviourController.UniverseObject.Universe.FindRequiredBehaviour(); - - triangleBatch = new(windowContainer.Window.GraphicsDevice); - drawableShapes.Unassign(); - drawableShapes.Assign(BehaviourController.UniverseObject.Universe); - } - - public void Draw() - { - triangleBatch.Begin(camera2D.MatrixTransform); - for (int i = 0; i < drawableShapes.Count; i++) - drawableShapes[i].Draw(triangleBatch); - triangleBatch.End(); - } - - public void Begin(Matrix? view = null, Matrix? projection = null) => triangleBatch.Begin(view, projection); - public void Draw(Triangle triangle, ColorRGBA colorRGBA) => triangleBatch.Draw(triangle, colorRGBA); - public void End() => triangleBatch.End(); -} diff --git a/Engine.Integration/Engine.Integration.MonoGame/TriangleBatch.cs b/Engine.Integration/Engine.Integration.MonoGame/MonoGameTriangleBatch.cs similarity index 78% rename from Engine.Integration/Engine.Integration.MonoGame/TriangleBatch.cs rename to Engine.Integration/Engine.Integration.MonoGame/MonoGameTriangleBatch.cs index 499a6ed..7f8cb2c 100644 --- a/Engine.Integration/Engine.Integration.MonoGame/TriangleBatch.cs +++ b/Engine.Integration/Engine.Integration.MonoGame/MonoGameTriangleBatch.cs @@ -2,24 +2,26 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Engine.Core; +using Engine.Systems.Graphics; namespace Engine.Integration.MonoGame; -public class TriangleBatch : ITriangleBatch +public class MonoGameTriangleBatch : Behaviour, ITriangleBatch, IFirstFrameUpdate { - private readonly GraphicsDevice graphicsDevice; - private readonly VertexBuffer vertexBuffer = default!; + private GraphicsDevice graphicsDevice = null!; + private VertexBuffer vertexBuffer = default!; private readonly VertexPositionColor[] vertices = new VertexPositionColor[1024]; private int verticesIndex = 0; private Matrix view = Matrix.Identity; private Matrix projection = Matrix.Identity; - private readonly BasicEffect basicEffect = null!; + private BasicEffect basicEffect = null!; private readonly RasterizerState rasterizerState = new() { CullMode = CullMode.None }; - public TriangleBatch(GraphicsDevice graphicsDevice) + public void FirstActiveFrame() { + GraphicsDevice graphicsDevice = Universe.FindRequiredBehaviour().Window.GraphicsDevice; this.graphicsDevice = graphicsDevice; basicEffect = new(graphicsDevice); basicEffect.VertexColorEnabled = true; @@ -41,15 +43,15 @@ public class TriangleBatch : ITriangleBatch vertices[verticesIndex++] = new(new(C.X, C.Y, 0f), color); } - public void Begin(Matrix? view = null, Matrix? projection = null) + public void Begin(Matrix4x4? view = null, Matrix4x4? projection = null) { if (view != null) - this.view = view.Value; + this.view = view.Value.ToXnaMatrix(); else this.view = Matrix.Identity; if (projection != null) - this.projection = projection.Value; + this.projection = projection.Value.ToXnaMatrix(); else { Viewport viewport = graphicsDevice.Viewport; diff --git a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/DrawableShape.cs b/Engine.Systems/Graphics/DrawableShape2D.cs similarity index 66% rename from Engine.Integration/Engine.Integration.MonoGame/Behaviours/DrawableShape.cs rename to Engine.Systems/Graphics/DrawableShape2D.cs index 0078088..2a6d179 100644 --- a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/DrawableShape.cs +++ b/Engine.Systems/Graphics/DrawableShape2D.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using Engine.Core; -namespace Engine.Integration.MonoGame; +namespace Engine.Systems.Graphics; -public class DrawableShape : Behaviour2D, IDrawableTriangle, IPreDraw +public class DrawableShape2D : Behaviour2D, IDrawableTriangle, IPreDraw { private readonly Shape2D shape = new([]); private readonly List worldTriangles = []; @@ -23,7 +23,7 @@ public class DrawableShape : Behaviour2D, IDrawableTriangle, IPreDraw protected void UpdateWorldShape() => shape.Transform(Transform, worldShape); - public DrawableShape() => shape = Shape2D.Triangle; - public DrawableShape(Shape2D shape) => this.shape = shape; - public DrawableShape(Shape2D shape, ColorRGB color) { this.shape = shape; this.color = color; } + public DrawableShape2D() => shape = Shape2D.Triangle; + public DrawableShape2D(Shape2D shape) => this.shape = shape; + public DrawableShape2D(Shape2D shape, ColorRGB color) { this.shape = shape; this.color = color; } } diff --git a/Engine.Integration/Engine.Integration.MonoGame/Abstract/IDrawableTriangle.cs b/Engine.Systems/Graphics/IDrawableTriangle.cs similarity index 75% rename from Engine.Integration/Engine.Integration.MonoGame/Abstract/IDrawableTriangle.cs rename to Engine.Systems/Graphics/IDrawableTriangle.cs index 7d3372d..2606b18 100644 --- a/Engine.Integration/Engine.Integration.MonoGame/Abstract/IDrawableTriangle.cs +++ b/Engine.Systems/Graphics/IDrawableTriangle.cs @@ -1,6 +1,6 @@ using Engine.Core; -namespace Engine.Integration.MonoGame; +namespace Engine.Systems.Graphics; public interface IDrawableTriangle : IBehaviour { diff --git a/Engine.Systems/Graphics/ITriangleBatch.cs b/Engine.Systems/Graphics/ITriangleBatch.cs new file mode 100644 index 0000000..c25d6aa --- /dev/null +++ b/Engine.Systems/Graphics/ITriangleBatch.cs @@ -0,0 +1,10 @@ +using Engine.Core; + +namespace Engine.Systems.Graphics; + +public interface ITriangleBatch +{ + void Begin(Matrix4x4? view = null, Matrix4x4? projection = null); + void Draw(Triangle triangle, ColorRGBA colorRGBA); + void End(); +} diff --git a/Engine.Systems/Graphics/TriangleBatcher.cs b/Engine.Systems/Graphics/TriangleBatcher.cs new file mode 100644 index 0000000..8373833 --- /dev/null +++ b/Engine.Systems/Graphics/TriangleBatcher.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; + +using Engine.Core; + +namespace Engine.Systems.Graphics; + +public class TriangleBatcher : Behaviour, IFirstFrameUpdate, IDraw +{ + private static Comparer SortByAscendingPriority() => Comparer.Create((x, y) => x.CompareTo(y)); + private static System.Func GetPriority() => (b) => b.Priority; + + private ITriangleBatch triangleBatch = null!; + private ICamera camera = null!; + + private readonly ActiveBehaviourCollectorOrdered drawableShapes = new(GetPriority(), SortByAscendingPriority()); + + public void FirstActiveFrame() + { + camera = Universe.FindRequiredBehaviour(); + + triangleBatch = Universe.FindRequiredBehaviour(); + drawableShapes.Unassign(); + drawableShapes.Assign(Universe); + } + + public void Draw() + { + triangleBatch.Begin(camera.ViewMatrix, camera.ProjectionMatrix); + for (int i = 0; i < drawableShapes.Count; i++) + drawableShapes[i].Draw(triangleBatch); + triangleBatch.End(); + } +} -- 2.49.1 From 50a02697987fb737051d8ff5eb924e1509a54b5c Mon Sep 17 00:00:00 2001 From: Syntriax Date: Tue, 27 Jan 2026 20:48:39 +0300 Subject: [PATCH 08/51] feat: added matrix 4x4 transpose & orthographic view matrix methods --- Engine.Core/Primitives/Matrix4x4.cs | 52 +++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/Engine.Core/Primitives/Matrix4x4.cs b/Engine.Core/Primitives/Matrix4x4.cs index 9bce992..2c2ab80 100644 --- a/Engine.Core/Primitives/Matrix4x4.cs +++ b/Engine.Core/Primitives/Matrix4x4.cs @@ -67,6 +67,11 @@ public readonly struct Matrix4x4( /// public Matrix4x4 Inverse => Invert(this); + /// + /// Represents the transposed version of this . + /// + public Matrix4x4 Transposed => Transpose(this); + public static Matrix4x4 operator *(Matrix4x4 a, Matrix4x4 b) => new( a.M11 * b.M11 + a.M12 * b.M21 + a.M13 * b.M31 + a.M14 * b.M41, a.M11 * b.M12 + a.M12 * b.M22 + a.M13 * b.M32 + a.M14 * b.M42, @@ -202,6 +207,18 @@ public readonly struct Matrix4x4( ); } + /// + /// Transposes the given . + /// + /// The . + /// The transposed of the given . + public static Matrix4x4 Transpose(Matrix4x4 m) => new( + m.M11, m.M21, m.M31, m.M41, + m.M12, m.M22, m.M32, m.M42, + m.M13, m.M23, m.M33, m.M43, + m.M14, m.M24, m.M34, m.M44 + ); + public static Matrix4x4 CreateTranslation(Vector3D position) => new( 1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, @@ -297,6 +314,30 @@ public readonly struct Matrix4x4( ); } + public static Matrix4x4 CreateOrthographicView(float width, float height, float nearPlane = -1f, float farPlane = 1f) + { + float invDepth = 1f / (farPlane - nearPlane); + + return new Matrix4x4( + 2f / width, 0f, 0f, 0f, + 0f, -2f / height, 0f, 0f, + 0f, 0f, invDepth, 0f, + -1f, 1f, -nearPlane * invDepth, 1f + ); + } + + public static Matrix4x4 CreateOrthographicViewCentered(float width, float height, float nearPlane = -1f, float farPlane = 1f) + { + float invDepth = 1f / (farPlane - nearPlane); + + return new Matrix4x4( + 2f / width, 0f, 0f, 0f, + 0f, 2f / height, 0f, 0f, + 0f, 0f, invDepth, 0f, + 0f, 0f, -nearPlane * invDepth, 1f + ); + } + public static Matrix4x4 CreatePerspectiveFieldOfView(float fieldOfViewInRadians, float aspectRatio, float nearPlane, float farPlane) { float yScale = 1f / Math.Tan(fieldOfViewInRadians / 2f); @@ -344,6 +385,9 @@ public static class Matrix4x4Extensions /// public static Matrix4x4 Invert(this Matrix4x4 matrix) => Matrix4x4.Invert(matrix); + /// + public static Matrix4x4 Transpose(this Matrix4x4 matrix) => Matrix4x4.Transpose(matrix); + /// public static Matrix4x4 ApplyTranslation(this Matrix4x4 matrix, Vector3D translation) => matrix * Matrix4x4.CreateTranslation(translation); @@ -375,6 +419,14 @@ public static class Matrix4x4Extensions public static Matrix4x4 ApplyPerspectiveFieldOfView(this Matrix4x4 matrix, float fieldOfViewInRadians, float aspectRatio, float nearPlane, float farPlane) => matrix * Matrix4x4.CreatePerspectiveFieldOfView(fieldOfViewInRadians, aspectRatio, nearPlane, farPlane); + /// + public static Matrix4x4 ApplyOrthographicView(this Matrix4x4 matrix, float width, float height, float nearPlane = -1f, float farPlane = 1f) + => matrix * Matrix4x4.CreateOrthographicView(width, height, nearPlane, farPlane); + + /// + public static Matrix4x4 ApplyOrthographicViewCentered(this Matrix4x4 matrix, float width, float height, float nearPlane = -1f, float farPlane = 1f) + => matrix * Matrix4x4.CreateOrthographicViewCentered(width, height, nearPlane, farPlane); + /// struct with the specified values. @@ -220,10 +220,10 @@ public readonly struct Matrix4x4( ); public static Matrix4x4 CreateTranslation(Vector3D position) => new( - 1f, 0f, 0f, 0f, - 0f, 1f, 0f, 0f, - 0f, 0f, 1f, 0f, - position.X, position.Y, position.Z, 1 + 1f, 0f, 0f, position.X, + 0f, 1f, 0f, position.Y, + 0f, 0f, 1f, position.Z, + 0f, 0f, 0f, 1f ); public static Matrix4x4 CreateScale(float scale) => new( @@ -245,11 +245,11 @@ public readonly struct Matrix4x4( float c = Math.Cos(radians); float s = Math.Sin(radians); - return new Matrix4x4( - 1f, 0f, 0f, 0f, - 0f, c, s, 0f, - 0f, -s, c, 0f, - 0f, 0f, 0f, 1f + return new( + 1f, 0f, 0f, 0f, + 0f, c, -s, 0f, + 0f, s, c, 0f, + 0f, 0f, 0f, 1f ); } @@ -258,10 +258,10 @@ public readonly struct Matrix4x4( float c = Math.Cos(radians); float s = Math.Sin(radians); - return new Matrix4x4( - c, 0f, -s, 0f, + return new( + c, 0f, s, 0f, 0f, 1f, 0f, 0f, - s, 0f, c, 0f, + -s, 0f, c, 0f, 0f, 0f, 0f, 1f ); } @@ -271,9 +271,9 @@ public readonly struct Matrix4x4( float c = Math.Cos(radians); float s = Math.Sin(radians); - return new Matrix4x4( - c, s, 0f, 0f, - -s, c, 0f, 0f, + return new( + c, -s, 0f, 0f, + s, c, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f, 1f ); @@ -292,7 +292,7 @@ public readonly struct Matrix4x4( Vector3D x = up.Cross(z).Normalized; Vector3D y = z.Cross(x); - return new Matrix4x4( + return new( x.X, y.X, z.X, 0f, x.Y, y.Y, z.Y, 0f, x.Z, y.Z, z.Z, 0f, @@ -302,15 +302,15 @@ public readonly struct Matrix4x4( public static Matrix4x4 CreateLookMatrix(Vector3D position, Vector3D target, Vector3D up) { - Vector3D z = position.FromTo(target).Normalized; - Vector3D x = up.Cross(z).Normalized; - Vector3D y = z.Cross(x); + Vector3D f = (target - position).Normalized; + Vector3D s = f.Cross(up).Normalized; + Vector3D u = s.Cross(f); - return new Matrix4x4( - x.X, y.X, z.X, 0f, - x.Y, y.Y, z.Y, 0f, - x.Z, y.Z, z.Z, 0f, - -x.Dot(position), -y.Dot(position), -z.Dot(position), 1f + return new( + s.X, u.X, -f.X, 0f, + s.Y, u.Y, -f.Y, 0f, + s.Z, u.Z, -f.Z, 0f, + -s.Dot(position), -u.Dot(position), f.Dot(position), 1f ); } @@ -318,10 +318,10 @@ public readonly struct Matrix4x4( { float invDepth = 1f / (farPlane - nearPlane); - return new Matrix4x4( + return new( 2f / width, 0f, 0f, 0f, 0f, -2f / height, 0f, 0f, - 0f, 0f, invDepth, 0f, + 0f, 0f, 1f * invDepth, 0f, -1f, 1f, -nearPlane * invDepth, 1f ); } @@ -330,24 +330,25 @@ public readonly struct Matrix4x4( { float invDepth = 1f / (farPlane - nearPlane); - return new Matrix4x4( + return new( 2f / width, 0f, 0f, 0f, 0f, 2f / height, 0f, 0f, - 0f, 0f, invDepth, 0f, + 0f, 0f, 1f * invDepth, 0f, 0f, 0f, -nearPlane * invDepth, 1f ); } - public static Matrix4x4 CreatePerspectiveFieldOfView(float fieldOfViewInRadians, float aspectRatio, float nearPlane, float farPlane) + public static Matrix4x4 CreatePerspectiveFieldOfView(float fovRadians, float aspectRatio, float nearPlane, float farPlane) { - float yScale = 1f / Math.Tan(fieldOfViewInRadians / 2f); + float yScale = 1f / Math.Tan(fovRadians / 2f); float xScale = yScale / aspectRatio; + float zRange = farPlane - nearPlane; - return new Matrix4x4( + return new( xScale, 0f, 0f, 0f, 0f, yScale, 0f, 0f, - 0f, 0f, farPlane / (farPlane - nearPlane), 1f, - 0f, 0f, -nearPlane * farPlane / (farPlane - nearPlane), 0f + 0f, 0f, -(farPlane + nearPlane) / zRange, -1f, + 0f, 0f, -(2f * nearPlane * farPlane) / zRange, 0f ); } diff --git a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera2D.cs b/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera2D.cs index 7b0a180..1dcd651 100644 --- a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera2D.cs +++ b/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera2D.cs @@ -1,11 +1,12 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; using Engine.Core; namespace Engine.Integration.MonoGame; -public class MonoGameCamera2D : Behaviour, ICamera2D, IFirstFrameUpdate, ILastFrameUpdate, IPreDraw +public class MonoGameCamera2D : Behaviour, ICamera2D, IFirstFrameUpdate, ILastFrameUpdate, IPreDraw, IUpdate { public Event OnViewMatrixChanged { get; } = new(); public Event OnProjectionMatrixChanged { get; } = new(); @@ -91,12 +92,20 @@ public class MonoGameCamera2D : Behaviour, ICamera2D, IFirstFrameUpdate, ILastFr public void PreDraw() { - ProjectionMatrix = Matrix.CreateOrthographicOffCenter(Viewport.X, Viewport.Width, Viewport.Height, Viewport.Y, 0, 1).FromXnaMatrix(); + ProjectionMatrix = Matrix4x4.CreateOrthographicViewCentered(Viewport.Width, -Viewport.Height); ViewMatrix = - Matrix4x4.CreateTranslation(new Vector3D(-Transform.Position.X, Transform.Position.Y, 0f)) * - Matrix4x4.CreateRotationZ(Transform.Rotation * Math.DegreeToRadian) * - Matrix4x4.CreateScale(Transform.Scale.X.Max(Transform.Scale.Y)) * - Matrix4x4.CreateScale(Zoom) * - Matrix4x4.CreateTranslation(new Vector3D(Viewport.Width * .5f, Viewport.Height * .5f, 0f)); + Matrix4x4.CreateTranslation(new Vector3D(-Transform.Position.X, Transform.Position.Y, 0f)) + .ApplyRotationZ(Transform.Rotation * Math.DegreeToRadian) + .ApplyScale(Transform.Scale.X.Max(Transform.Scale.Y)) + .ApplyScale(Zoom); + } + + public void Update() + { + KeyboardInputs keyboardInputs = Universe.FindRequiredBehaviour(); + if (keyboardInputs.IsPressed(Keys.Right)) Transform.Position += Transform.Right * Universe.Time.DeltaTime * 100; + if (keyboardInputs.IsPressed(Keys.Up)) Transform.Position += Transform.Up * Universe.Time.DeltaTime * 100; + if (keyboardInputs.IsPressed(Keys.Down)) Transform.Position += Transform.Down * Universe.Time.DeltaTime * 100; + if (keyboardInputs.IsPressed(Keys.Left)) Transform.Position += Transform.Left * Universe.Time.DeltaTime * 100; } } diff --git a/Engine.Integration/Engine.Integration.MonoGame/MonoGameTriangleBatch.cs b/Engine.Integration/Engine.Integration.MonoGame/MonoGameTriangleBatch.cs index 7f8cb2c..a6f4c4e 100644 --- a/Engine.Integration/Engine.Integration.MonoGame/MonoGameTriangleBatch.cs +++ b/Engine.Integration/Engine.Integration.MonoGame/MonoGameTriangleBatch.cs @@ -45,18 +45,10 @@ public class MonoGameTriangleBatch : Behaviour, ITriangleBatch, IFirstFrameUpdat public void Begin(Matrix4x4? view = null, Matrix4x4? projection = null) { - if (view != null) - this.view = view.Value.ToXnaMatrix(); - else - this.view = Matrix.Identity; + Viewport viewport = graphicsDevice.Viewport; - if (projection != null) - this.projection = projection.Value.ToXnaMatrix(); - else - { - Viewport viewport = graphicsDevice.Viewport; - this.projection = Matrix.CreateOrthographicOffCenter(viewport.X, viewport.Width, viewport.Height, viewport.Y, 0, 1); - } + this.view = (view ?? Matrix4x4.Identity).Transposed.ToXnaMatrix(); + this.projection = (projection ?? Matrix4x4.CreateOrthographicViewCentered(viewport.Width, viewport.Height)).Transposed.ToXnaMatrix(); } public void End() => Flush(); -- 2.49.1 From d4437edfbf73c969e523680c585719e030f354fe Mon Sep 17 00:00:00 2001 From: Syntriax Date: Wed, 28 Jan 2026 10:22:36 +0300 Subject: [PATCH 11/51] feat: added Matrix4x4 x Vector4D multiplication --- Engine.Core/Primitives/Matrix4x4.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Engine.Core/Primitives/Matrix4x4.cs b/Engine.Core/Primitives/Matrix4x4.cs index fbbb8c3..e01e91a 100644 --- a/Engine.Core/Primitives/Matrix4x4.cs +++ b/Engine.Core/Primitives/Matrix4x4.cs @@ -94,6 +94,13 @@ public readonly struct Matrix4x4( a.M41 * b.M14 + a.M42 * b.M24 + a.M43 * b.M34 + a.M44 * b.M44 ); + public static Vector4D operator *(Matrix4x4 m, Vector4D v) => new( + m.M11 * v.X + m.M12 * v.Y + m.M13 * v.Z + m.M14 * v.W, + m.M21 * v.X + m.M22 * v.Y + m.M23 * v.Z + m.M24 * v.W, + m.M31 * v.X + m.M32 * v.Y + m.M33 * v.Z + m.M34 * v.W, + m.M41 * v.X + m.M42 * v.Y + m.M43 * v.Z + m.M44 * v.W + ); + public static bool operator ==(Matrix4x4 left, Matrix4x4 right) => left.M11 == right.M11 && left.M12 == right.M12 && left.M13 == right.M13 && left.M14 == right.M14 && left.M21 == right.M21 && left.M22 == right.M22 && left.M23 == right.M23 && left.M24 == right.M24 && -- 2.49.1 From e30280f1f8a96eb853285db24fc4379c852682a3 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Wed, 28 Jan 2026 11:02:07 +0300 Subject: [PATCH 12/51] fix: missing implicit conversion operators on Vectors --- Engine.Core/Primitives/Vector2D.cs | 14 +++++++++++--- Engine.Core/Primitives/Vector2DInt.cs | 5 ++++- Engine.Core/Primitives/Vector3D.cs | 14 +++++++++++--- Engine.Core/Primitives/Vector3DInt.cs | 5 ++++- Engine.Core/Primitives/Vector4D.cs | 11 +++++++++++ 5 files changed, 41 insertions(+), 8 deletions(-) diff --git a/Engine.Core/Primitives/Vector2D.cs b/Engine.Core/Primitives/Vector2D.cs index 6009875..fbe4291 100644 --- a/Engine.Core/Primitives/Vector2D.cs +++ b/Engine.Core/Primitives/Vector2D.cs @@ -82,11 +82,19 @@ public readonly struct Vector2D(float x, float y) : IEquatable public static bool operator ==(Vector2D left, Vector2D right) => left.X == right.X && left.Y == right.Y; public static bool operator !=(Vector2D left, Vector2D right) => left.X != right.X || left.Y != right.Y; - public static implicit operator System.Numerics.Vector2(Vector2D vector) => new(vector.X, vector.Y); - public static implicit operator Vector2D(System.Numerics.Vector2 vector) => new(vector.X, vector.Y); public static implicit operator Vector2D(Vector2DInt vector) => new(vector.X, vector.Y); - public static implicit operator Vector2D(Vector3D vector) => new(vector.X, vector.Y); + public static implicit operator Vector2D(Vector3DInt vector) => new(vector.X, vector.Y); + + public static implicit operator System.Numerics.Vector2(Vector2D vector) => new(vector.X, vector.Y); + public static implicit operator System.Numerics.Vector3(Vector2D vector) => new(vector.X, vector.Y, 0f); + public static implicit operator System.Numerics.Vector4(Vector2D vector) => new(vector.X, vector.Y, 0f, 0f); + + public static implicit operator Vector2D(System.Numerics.Vector2 vector) => new(vector.X, vector.Y); public static implicit operator Vector2D(System.Numerics.Vector3 vector) => new(vector.X, vector.Y); + public static implicit operator Vector2D(System.Numerics.Vector4 vector) => new(vector.X, vector.Y); + + public static implicit operator Vector2D(Vector3D vector) => new(vector.X, vector.Y); + public static implicit operator Vector2D(Vector4D vector) => new(vector.X, vector.Y); /// /// Calculates the length of the . diff --git a/Engine.Core/Primitives/Vector2DInt.cs b/Engine.Core/Primitives/Vector2DInt.cs index 31b9f37..ba9a58e 100644 --- a/Engine.Core/Primitives/Vector2DInt.cs +++ b/Engine.Core/Primitives/Vector2DInt.cs @@ -74,9 +74,12 @@ public readonly struct Vector2DInt(int x, int y) : IEquatable public static bool operator ==(Vector2DInt left, Vector2DInt right) => left.X == right.X && left.Y == right.Y; public static bool operator !=(Vector2DInt left, Vector2DInt right) => left.X != right.X || left.Y != right.Y; - public static implicit operator Vector2DInt(Vector2D vector) => new(vector.X.RoundToInt(), vector.Y.RoundToInt()); public static implicit operator Vector2DInt(Vector3DInt vector) => new(vector.X, vector.Y); + public static implicit operator Vector2DInt(Vector2D vector) => new(vector.X.RoundToInt(), vector.Y.RoundToInt()); + public static implicit operator Vector2DInt(Vector3D vector) => new(vector.X.RoundToInt(), vector.Y.RoundToInt()); + public static implicit operator Vector2DInt(Vector4D vector) => new(vector.X.RoundToInt(), vector.Y.RoundToInt()); + /// /// Calculates the length of the . /// diff --git a/Engine.Core/Primitives/Vector3D.cs b/Engine.Core/Primitives/Vector3D.cs index 3551f35..8b0598b 100644 --- a/Engine.Core/Primitives/Vector3D.cs +++ b/Engine.Core/Primitives/Vector3D.cs @@ -92,11 +92,19 @@ public readonly struct Vector3D(float x, float y, float z) : IEquatable left.X == right.X && left.Y == right.Y && left.Z == right.Z; public static bool operator !=(Vector3D left, Vector3D right) => left.X != right.X || left.Y != right.Y || left.Z != right.Z; - public static implicit operator System.Numerics.Vector3(Vector3D vector) => new(vector.X, vector.Y, vector.Z); - public static implicit operator Vector3D(System.Numerics.Vector3 vector) => new(vector.X, vector.Y, vector.Z); + public static implicit operator Vector3D(Vector2DInt vector) => new(vector.X, vector.Y, 0f); public static implicit operator Vector3D(Vector3DInt vector) => new(vector.X, vector.Y, vector.Z); - public static implicit operator Vector3D(Vector2D vector) => new(vector.X, vector.Y, 0f); + + public static implicit operator System.Numerics.Vector2(Vector3D vector) => new(vector.X, vector.Y); + public static implicit operator System.Numerics.Vector3(Vector3D vector) => new(vector.X, vector.Y, vector.Z); + public static implicit operator System.Numerics.Vector4(Vector3D vector) => new(vector.X, vector.Y, vector.Z, 0f); + public static implicit operator Vector3D(System.Numerics.Vector2 vector) => new(vector.X, vector.Y, 0f); + public static implicit operator Vector3D(System.Numerics.Vector3 vector) => new(vector.X, vector.Y, vector.Z); + public static implicit operator Vector3D(System.Numerics.Vector4 vector) => new(vector.X, vector.Y, vector.Z); + + public static implicit operator Vector3D(Vector2D vector) => new(vector.X, vector.Y, 0f); + public static implicit operator Vector3D(Vector4D vector) => new(vector.X, vector.Y, vector.Z); /// /// Calculates the length of the . diff --git a/Engine.Core/Primitives/Vector3DInt.cs b/Engine.Core/Primitives/Vector3DInt.cs index 817351f..e35c195 100644 --- a/Engine.Core/Primitives/Vector3DInt.cs +++ b/Engine.Core/Primitives/Vector3DInt.cs @@ -84,9 +84,12 @@ public readonly struct Vector3DInt(int x, int y, int z) : IEquatable left.X == right.X && left.Y == right.Y && left.Z == right.Z; public static bool operator !=(Vector3DInt left, Vector3DInt right) => left.X != right.X || left.Y != right.Y || left.Z != right.Z; - public static implicit operator Vector3DInt(Vector3D vector) => new(vector.X.RoundToInt(), vector.Y.RoundToInt(), vector.Z.RoundToInt()); public static implicit operator Vector3DInt(Vector2DInt vector) => new(vector.X, vector.Y, 0); + public static implicit operator Vector3DInt(Vector2D vector) => new(vector.X.RoundToInt(), vector.Y.RoundToInt(), 0); + public static implicit operator Vector3DInt(Vector3D vector) => new(vector.X.RoundToInt(), vector.Y.RoundToInt(), vector.Z.RoundToInt()); + public static implicit operator Vector3DInt(Vector4D vector) => new(vector.X.RoundToInt(), vector.Y.RoundToInt(), vector.Z.RoundToInt()); + /// /// Calculates the length of the . /// diff --git a/Engine.Core/Primitives/Vector4D.cs b/Engine.Core/Primitives/Vector4D.cs index 4e38feb..b613933 100644 --- a/Engine.Core/Primitives/Vector4D.cs +++ b/Engine.Core/Primitives/Vector4D.cs @@ -90,9 +90,20 @@ public readonly struct Vector4D(float x, float y, float z, float w) : IEquatable public static bool operator ==(Vector4D left, Vector4D right) => left.X == right.X && left.Y == right.Y && left.Z == right.Z && left.W == right.W; public static bool operator !=(Vector4D left, Vector4D right) => left.X != right.X || left.Y != right.Y || left.Z != right.Z || left.W != right.W; + public static implicit operator Vector4D(Vector2DInt vector) => new(vector.X, vector.Y, 0f, 0f); + public static implicit operator Vector4D(Vector3DInt vector) => new(vector.X, vector.Y, vector.Z, 0f); + + public static implicit operator System.Numerics.Vector2(Vector4D vector) => new(vector.X, vector.Y); + public static implicit operator System.Numerics.Vector3(Vector4D vector) => new(vector.X, vector.Y, vector.Z); public static implicit operator System.Numerics.Vector4(Vector4D vector) => new(vector.X, vector.Y, vector.Z, vector.W); + + public static implicit operator Vector4D(System.Numerics.Vector2 vector) => new(vector.X, vector.Y, 0f, 0f); + public static implicit operator Vector4D(System.Numerics.Vector3 vector) => new(vector.X, vector.Y, vector.Z, 0f); public static implicit operator Vector4D(System.Numerics.Vector4 vector) => new(vector.X, vector.Y, vector.Z, vector.W); + public static implicit operator Vector4D(Vector2D vector) => new(vector.X, vector.Y, 0f, 0f); + public static implicit operator Vector4D(Vector3D vector) => new(vector.X, vector.Y, vector.Z, 0f); + /// /// Calculates the length of the . /// -- 2.49.1 From efa4da43988c8ab3eda2b9da5347ef200331d391 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Wed, 28 Jan 2026 11:06:15 +0300 Subject: [PATCH 13/51] fix: ScreenToWorldPosition & vice versa methods on MonoGameCamera2D fixed --- .../Behaviours/MonoGameCamera2D.cs | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera2D.cs b/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera2D.cs index 1dcd651..f28deb5 100644 --- a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera2D.cs +++ b/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera2D.cs @@ -1,12 +1,11 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; -using Microsoft.Xna.Framework.Input; using Engine.Core; namespace Engine.Integration.MonoGame; -public class MonoGameCamera2D : Behaviour, ICamera2D, IFirstFrameUpdate, ILastFrameUpdate, IPreDraw, IUpdate +public class MonoGameCamera2D : Behaviour, ICamera2D, IFirstFrameUpdate, ILastFrameUpdate, IPreDraw { public Event OnViewMatrixChanged { get; } = new(); public Event OnProjectionMatrixChanged { get; } = new(); @@ -73,13 +72,34 @@ public class MonoGameCamera2D : Behaviour, ICamera2D, IFirstFrameUpdate, ILastFr // TODO This causes delay since OnPreDraw calls assuming this is called in in Update public Vector2D ScreenToWorldPosition(Vector2D screenPosition) { - Vector2D worldPosition = Vector2.Transform(screenPosition.ToVector2(), ViewMatrix.Inverse.ToXnaMatrix()).ToVector2D(); - return worldPosition.Scale(EngineConverterExtensions.screenScale); + float x = 2f * screenPosition.X / Viewport.Width - 1f; + float y = 2f * screenPosition.Y / Viewport.Height - 1f; + Vector4D normalizedCoordinates = new(x, y, 0f, 1f); + + Matrix4x4 invertedViewProjectionMatrix = (ProjectionMatrix * ViewMatrix).Inverse; + + Vector4D worldPosition = invertedViewProjectionMatrix * normalizedCoordinates; + + if (worldPosition.W != 0f) + worldPosition /= worldPosition.W; + + return new(worldPosition.X, worldPosition.Y); } + public Vector2D WorldToScreenPosition(Vector2D worldPosition) { - Vector2D screenPosition = Vector2.Transform(worldPosition.ToVector2(), ViewMatrix.ToXnaMatrix()).ToVector2D(); - return screenPosition.Scale(EngineConverterExtensions.screenScale); + Vector4D worldPosition4D = new(worldPosition.X, worldPosition.Y, 0f, 1f); + + Matrix4x4 viewProjection = ProjectionMatrix * ViewMatrix; + Vector4D clip = viewProjection * worldPosition4D; + + if (clip.W != 0f) + clip /= clip.W; + + float screenX = (clip.X + 1f) * .5f * Viewport.Width; + float screenY = (clip.Y + 1f) * .5f * Viewport.Height; + + return new(screenX, screenY); } public void LastActiveFrame() => Transform = null!; @@ -99,13 +119,4 @@ public class MonoGameCamera2D : Behaviour, ICamera2D, IFirstFrameUpdate, ILastFr .ApplyScale(Transform.Scale.X.Max(Transform.Scale.Y)) .ApplyScale(Zoom); } - - public void Update() - { - KeyboardInputs keyboardInputs = Universe.FindRequiredBehaviour(); - if (keyboardInputs.IsPressed(Keys.Right)) Transform.Position += Transform.Right * Universe.Time.DeltaTime * 100; - if (keyboardInputs.IsPressed(Keys.Up)) Transform.Position += Transform.Up * Universe.Time.DeltaTime * 100; - if (keyboardInputs.IsPressed(Keys.Down)) Transform.Position += Transform.Down * Universe.Time.DeltaTime * 100; - if (keyboardInputs.IsPressed(Keys.Left)) Transform.Position += Transform.Left * Universe.Time.DeltaTime * 100; - } } -- 2.49.1 From 297e0eb790989b22586dfbdd78eb0421f4796fea Mon Sep 17 00:00:00 2001 From: Syntriax Date: Wed, 28 Jan 2026 11:14:31 +0300 Subject: [PATCH 14/51] fix: typos in Matrix4x4 methods --- Engine.Core/Primitives/Matrix4x4.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Engine.Core/Primitives/Matrix4x4.cs b/Engine.Core/Primitives/Matrix4x4.cs index e01e91a..364fcfd 100644 --- a/Engine.Core/Primitives/Matrix4x4.cs +++ b/Engine.Core/Primitives/Matrix4x4.cs @@ -1,5 +1,4 @@ using System; -using System.Numerics; namespace Engine.Core; @@ -400,7 +399,7 @@ public static class Matrix4x4Extensions public static Matrix4x4 ApplyTranslation(this Matrix4x4 matrix, Vector3D translation) => matrix * Matrix4x4.CreateTranslation(translation); /// - public static Matrix4x4 ApplyScale(this Matrix4x4 matrix, Vector3 scale) => matrix * Matrix4x4.CreateScale(scale); + public static Matrix4x4 ApplyScale(this Matrix4x4 matrix, Vector3D scale) => matrix * Matrix4x4.CreateScale(scale); /// public static Matrix4x4 ApplyScale(this Matrix4x4 matrix, float scale) => matrix * Matrix4x4.CreateScale(scale); @@ -418,10 +417,10 @@ public static class Matrix4x4Extensions public static Matrix4x4 ApplyRotation(this Matrix4x4 matrix, Quaternion quaternion) => matrix * Matrix4x4.CreateRotation(quaternion); /// - public static Matrix4x4 ApplyLookRotationTo(this Matrix4x4 matrix, Vector3D forward, Vector3 up) => matrix * Matrix4x4.CreateLookMatrix(forward, up); + public static Matrix4x4 ApplyLookRotationTo(this Matrix4x4 matrix, Vector3D forward, Vector3D up) => matrix * Matrix4x4.CreateLookMatrix(forward, up); /// - public static Matrix4x4 CreateLookMatrixTo(this Vector3D from, Vector3D to, Vector3 up) => Matrix4x4.CreateLookMatrix(from, to, up); + public static Matrix4x4 CreateLookMatrixTo(this Vector3D from, Vector3D to, Vector3D up) => Matrix4x4.CreateLookMatrix(from, to, up); /// public static Matrix4x4 ApplyPerspectiveFieldOfView(this Matrix4x4 matrix, float fieldOfViewInRadians, float aspectRatio, float nearPlane, float farPlane) -- 2.49.1 From ad444decbb7fad331278415d2437c9a01df694ab Mon Sep 17 00:00:00 2001 From: Syntriax Date: Wed, 28 Jan 2026 11:16:47 +0300 Subject: [PATCH 15/51] perf: removed old position flipping on MonoGame --- .../Behaviours/MonoGameCamera2D.cs | 8 +++--- .../Behaviours/SpriteBatchWrapper.cs | 26 +++++++++---------- .../EngineConverter.cs | 12 ++------- .../MonoGameTriangleBatch.cs | 6 ++--- 4 files changed, 22 insertions(+), 30 deletions(-) diff --git a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera2D.cs b/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera2D.cs index f28deb5..0d6f811 100644 --- a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera2D.cs +++ b/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera2D.cs @@ -73,7 +73,7 @@ public class MonoGameCamera2D : Behaviour, ICamera2D, IFirstFrameUpdate, ILastFr public Vector2D ScreenToWorldPosition(Vector2D screenPosition) { float x = 2f * screenPosition.X / Viewport.Width - 1f; - float y = 2f * screenPosition.Y / Viewport.Height - 1f; + float y = 1f - 2f * screenPosition.Y / Viewport.Height; Vector4D normalizedCoordinates = new(x, y, 0f, 1f); Matrix4x4 invertedViewProjectionMatrix = (ProjectionMatrix * ViewMatrix).Inverse; @@ -97,7 +97,7 @@ public class MonoGameCamera2D : Behaviour, ICamera2D, IFirstFrameUpdate, ILastFr clip /= clip.W; float screenX = (clip.X + 1f) * .5f * Viewport.Width; - float screenY = (clip.Y + 1f) * .5f * Viewport.Height; + float screenY = (1f - clip.Y) * .5f * Viewport.Height; return new(screenX, screenY); } @@ -112,9 +112,9 @@ public class MonoGameCamera2D : Behaviour, ICamera2D, IFirstFrameUpdate, ILastFr public void PreDraw() { - ProjectionMatrix = Matrix4x4.CreateOrthographicViewCentered(Viewport.Width, -Viewport.Height); + ProjectionMatrix = Matrix4x4.CreateOrthographicViewCentered(Viewport.Width, Viewport.Height); ViewMatrix = - Matrix4x4.CreateTranslation(new Vector3D(-Transform.Position.X, Transform.Position.Y, 0f)) + Matrix4x4.CreateTranslation(new Vector3D(-Transform.Position.X, -Transform.Position.Y, 0f)) .ApplyRotationZ(Transform.Rotation * Math.DegreeToRadian) .ApplyScale(Transform.Scale.X.Max(Transform.Scale.Y)) .ApplyScale(Zoom); diff --git a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/SpriteBatchWrapper.cs b/Engine.Integration/Engine.Integration.MonoGame/Behaviours/SpriteBatchWrapper.cs index 8940127..57610d9 100644 --- a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/SpriteBatchWrapper.cs +++ b/Engine.Integration/Engine.Integration.MonoGame/Behaviours/SpriteBatchWrapper.cs @@ -15,49 +15,49 @@ public class SpriteBatchWrapper(GraphicsDevice graphicsDevice) : ISpriteBatch => spriteBatch.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, transformMatrix); public void Draw(Texture2D texture, Vector2D position, AABB2D? sourceAABB, Color color, float rotation, Vector2D origin, Vector2D scale, SpriteEffects effects, float layerDepth) - => spriteBatch.Draw(texture, position.ToDisplayVector2(), sourceAABB?.ToRectangle(), color, rotation, origin.ToDisplayVector2(), scale.ToDisplayVector2(), effects, layerDepth); + => spriteBatch.Draw(texture, position.ToVector2(), sourceAABB?.ToRectangle(), color, rotation, origin.ToVector2(), scale.ToVector2(), effects, layerDepth); public void Draw(Texture2D texture, Vector2D position, AABB2D? sourceAABB, Color color, float rotation, Vector2D origin, float scale, SpriteEffects effects, float layerDepth) - => spriteBatch.Draw(texture, position.ToDisplayVector2(), sourceAABB?.ToRectangle(), color, rotation, origin.ToDisplayVector2(), scale, effects, layerDepth); + => spriteBatch.Draw(texture, position.ToVector2(), sourceAABB?.ToRectangle(), color, rotation, origin.ToVector2(), scale, effects, layerDepth); public void Draw(Texture2D texture, AABB2D destinationAABB, AABB2D? sourceAABB, Color color, float rotation, Vector2D origin, SpriteEffects effects, float layerDepth) - => spriteBatch.Draw(texture, destinationAABB.ToRectangle(), sourceAABB?.ToRectangle(), color, rotation, origin.ToDisplayVector2(), effects, layerDepth); + => spriteBatch.Draw(texture, destinationAABB.ToRectangle(), sourceAABB?.ToRectangle(), color, rotation, origin.ToVector2(), effects, layerDepth); public void Draw(Texture2D texture, Vector2D position, AABB2D? sourceAABB, Color color) - => spriteBatch.Draw(texture, position.ToDisplayVector2(), sourceAABB?.ToRectangle(), color); + => spriteBatch.Draw(texture, position.ToVector2(), sourceAABB?.ToRectangle(), color); public void Draw(Texture2D texture, AABB2D destinationAABB, AABB2D? sourceAABB, Color color) => spriteBatch.Draw(texture, destinationAABB.ToRectangle(), sourceAABB?.ToRectangle(), color); public void Draw(Texture2D texture, Vector2D position, Color color) - => spriteBatch.Draw(texture, position.ToDisplayVector2(), color); + => spriteBatch.Draw(texture, position.ToVector2(), color); public void Draw(Texture2D texture, AABB2D destinationAABB, Color color) => spriteBatch.Draw(texture, destinationAABB.ToRectangle(), color); public void DrawString(SpriteFont spriteFont, string text, Vector2D position, Color color) - => spriteBatch.DrawString(spriteFont, text, position.ToDisplayVector2(), color); + => spriteBatch.DrawString(spriteFont, text, position.ToVector2(), color); public void DrawString(SpriteFont spriteFont, string text, Vector2D position, Color color, float rotation, Vector2D origin, float scale, SpriteEffects effects, float layerDepth) - => spriteBatch.DrawString(spriteFont, text, position.ToDisplayVector2(), color, rotation, origin.ToDisplayVector2(), scale, effects, layerDepth); + => spriteBatch.DrawString(spriteFont, text, position.ToVector2(), color, rotation, origin.ToVector2(), scale, effects, layerDepth); public void DrawString(SpriteFont spriteFont, string text, Vector2D position, Color color, float rotation, Vector2D origin, Vector2D scale, SpriteEffects effects, float layerDepth) - => spriteBatch.DrawString(spriteFont, text, position.ToDisplayVector2(), color, rotation, origin.ToDisplayVector2(), scale.ToDisplayVector2(), effects, layerDepth); + => spriteBatch.DrawString(spriteFont, text, position.ToVector2(), color, rotation, origin.ToVector2(), scale.ToVector2(), effects, layerDepth); public void DrawString(SpriteFont spriteFont, string text, Vector2D position, Color color, float rotation, Vector2D origin, Vector2D scale, SpriteEffects effects, float layerDepth, bool rtl) - => spriteBatch.DrawString(spriteFont, text, position.ToDisplayVector2(), color, rotation, origin.ToDisplayVector2(), scale.ToDisplayVector2(), effects, layerDepth, rtl); + => spriteBatch.DrawString(spriteFont, text, position.ToVector2(), color, rotation, origin.ToVector2(), scale.ToVector2(), effects, layerDepth, rtl); public void DrawString(SpriteFont spriteFont, StringBuilder text, Vector2D position, Color color) - => spriteBatch.DrawString(spriteFont, text, position.ToDisplayVector2(), color); + => spriteBatch.DrawString(spriteFont, text, position.ToVector2(), color); public void DrawString(SpriteFont spriteFont, StringBuilder text, Vector2D position, Color color, float rotation, Vector2D origin, float scale, SpriteEffects effects, float layerDepth) - => spriteBatch.DrawString(spriteFont, text, position.ToDisplayVector2(), color, rotation, origin.ToDisplayVector2(), scale, effects, layerDepth); + => spriteBatch.DrawString(spriteFont, text, position.ToVector2(), color, rotation, origin.ToVector2(), scale, effects, layerDepth); public void DrawString(SpriteFont spriteFont, StringBuilder text, Vector2D position, Color color, float rotation, Vector2D origin, Vector2D scale, SpriteEffects effects, float layerDepth) - => spriteBatch.DrawString(spriteFont, text, position.ToDisplayVector2(), color, rotation, origin.ToDisplayVector2(), scale.ToDisplayVector2(), effects, layerDepth); + => spriteBatch.DrawString(spriteFont, text, position.ToVector2(), color, rotation, origin.ToVector2(), scale.ToVector2(), effects, layerDepth); public void DrawString(SpriteFont spriteFont, StringBuilder text, Vector2D position, Color color, float rotation, Vector2D origin, Vector2D scale, SpriteEffects effects, float layerDepth, bool rtl) - => spriteBatch.DrawString(spriteFont, text, position.ToDisplayVector2(), color, rotation, origin.ToDisplayVector2(), scale.ToDisplayVector2(), effects, layerDepth, rtl); + => spriteBatch.DrawString(spriteFont, text, position.ToVector2(), color, rotation, origin.ToVector2(), scale.ToVector2(), effects, layerDepth, rtl); public void End() => spriteBatch.End(); diff --git a/Engine.Integration/Engine.Integration.MonoGame/EngineConverter.cs b/Engine.Integration/Engine.Integration.MonoGame/EngineConverter.cs index f30cc0c..42d5d49 100644 --- a/Engine.Integration/Engine.Integration.MonoGame/EngineConverter.cs +++ b/Engine.Integration/Engine.Integration.MonoGame/EngineConverter.cs @@ -9,8 +9,6 @@ namespace Engine.Integration.MonoGame; public static class EngineConverterExtensions { - public readonly static Vector2D screenScale = Vector2D.Down + Vector2D.Right; - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static UniverseTime ToEngineTime(this GameTime gameTime) => new(gameTime.TotalGameTime, gameTime.ElapsedGameTime); @@ -60,12 +58,6 @@ public static class EngineConverterExtensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Microsoft.Xna.Framework.Quaternion ToXnaQuaternion(this Core.Quaternion quaternion) => new(quaternion.X, quaternion.Y, quaternion.Z, quaternion.W); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 ToDisplayVector2(this Vector2D vector) => vector.Scale(screenScale).ToVector2(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2D ApplyDisplayScale(this Vector2D vector) => vector.Scale(screenScale); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Rectangle ToDisplayRectangle(this Rectangle rectangle, DisplayMode displayMode) => new() { @@ -78,8 +70,8 @@ public static class EngineConverterExtensions [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Rectangle ToRectangle(this AABB2D aabb) => new() { - X = (int)(aabb.LowerBoundary.X * screenScale.X), - Y = (int)(aabb.LowerBoundary.Y * screenScale.Y), + X = (int)aabb.LowerBoundary.X, + Y = (int)aabb.LowerBoundary.Y, Width = (int)(aabb.UpperBoundary.X - aabb.LowerBoundary.X), Height = (int)(aabb.UpperBoundary.Y - aabb.LowerBoundary.Y) }; diff --git a/Engine.Integration/Engine.Integration.MonoGame/MonoGameTriangleBatch.cs b/Engine.Integration/Engine.Integration.MonoGame/MonoGameTriangleBatch.cs index a6f4c4e..f782028 100644 --- a/Engine.Integration/Engine.Integration.MonoGame/MonoGameTriangleBatch.cs +++ b/Engine.Integration/Engine.Integration.MonoGame/MonoGameTriangleBatch.cs @@ -33,9 +33,9 @@ public class MonoGameTriangleBatch : Behaviour, ITriangleBatch, IFirstFrameUpdat if (verticesIndex + 3 >= vertices.Length) Flush(); - Vector2 A = triangle.A.ToDisplayVector2(); - Vector2 B = triangle.B.ToDisplayVector2(); - Vector2 C = triangle.C.ToDisplayVector2(); + Vector2 A = triangle.A.ToVector2(); + Vector2 B = triangle.B.ToVector2(); + Vector2 C = triangle.C.ToVector2(); Color color = colorRGBA.ToColor(); vertices[verticesIndex++] = new(new(A.X, A.Y, 0f), color); -- 2.49.1 From c68de39c83944944a115f1b6d6a430521644d614 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Wed, 28 Jan 2026 12:58:25 +0300 Subject: [PATCH 16/51] fix: MonoGame view matrix calculation issues --- .../Behaviours/MonoGameCamera2D.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera2D.cs b/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera2D.cs index 0d6f811..eafd9db 100644 --- a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera2D.cs +++ b/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera2D.cs @@ -113,10 +113,10 @@ public class MonoGameCamera2D : Behaviour, ICamera2D, IFirstFrameUpdate, ILastFr public void PreDraw() { ProjectionMatrix = Matrix4x4.CreateOrthographicViewCentered(Viewport.Width, Viewport.Height); - ViewMatrix = - Matrix4x4.CreateTranslation(new Vector3D(-Transform.Position.X, -Transform.Position.Y, 0f)) - .ApplyRotationZ(Transform.Rotation * Math.DegreeToRadian) - .ApplyScale(Transform.Scale.X.Max(Transform.Scale.Y)) - .ApplyScale(Zoom); + ViewMatrix = Matrix4x4.Identity + .ApplyScale(Transform.Scale.X.Max(Transform.Scale.Y)) + .ApplyScale(Zoom) + .ApplyRotationZ(-Transform.Rotation * Math.DegreeToRadian) + .ApplyTranslation(new Vector3D(-Transform.Position.X, -Transform.Position.Y, 0f)); } } -- 2.49.1 From b9f3227f737a61be2860b998ca9ed99e268dcbf5 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Wed, 28 Jan 2026 22:19:04 +0300 Subject: [PATCH 17/51] refactor: removed parameters on triangle batch calls on TriangleBatcher for multi window support --- .../Engine.Integration.MonoGame/MonoGameTriangleBatch.cs | 7 +++++-- Engine.Systems/Graphics/TriangleBatcher.cs | 6 +----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Engine.Integration/Engine.Integration.MonoGame/MonoGameTriangleBatch.cs b/Engine.Integration/Engine.Integration.MonoGame/MonoGameTriangleBatch.cs index f782028..5e0a5a7 100644 --- a/Engine.Integration/Engine.Integration.MonoGame/MonoGameTriangleBatch.cs +++ b/Engine.Integration/Engine.Integration.MonoGame/MonoGameTriangleBatch.cs @@ -19,8 +19,11 @@ public class MonoGameTriangleBatch : Behaviour, ITriangleBatch, IFirstFrameUpdat private BasicEffect basicEffect = null!; private readonly RasterizerState rasterizerState = new() { CullMode = CullMode.None }; + private ICamera camera = null!; + public void FirstActiveFrame() { + camera = Universe.FindRequiredBehaviour(); GraphicsDevice graphicsDevice = Universe.FindRequiredBehaviour().Window.GraphicsDevice; this.graphicsDevice = graphicsDevice; basicEffect = new(graphicsDevice); @@ -47,8 +50,8 @@ public class MonoGameTriangleBatch : Behaviour, ITriangleBatch, IFirstFrameUpdat { Viewport viewport = graphicsDevice.Viewport; - this.view = (view ?? Matrix4x4.Identity).Transposed.ToXnaMatrix(); - this.projection = (projection ?? Matrix4x4.CreateOrthographicViewCentered(viewport.Width, viewport.Height)).Transposed.ToXnaMatrix(); + this.view = (view ?? camera.ViewMatrix).Transposed.ToXnaMatrix(); + this.projection = (projection ?? camera.ProjectionMatrix).Transposed.ToXnaMatrix(); } public void End() => Flush(); diff --git a/Engine.Systems/Graphics/TriangleBatcher.cs b/Engine.Systems/Graphics/TriangleBatcher.cs index d1c78fd..872f2be 100644 --- a/Engine.Systems/Graphics/TriangleBatcher.cs +++ b/Engine.Systems/Graphics/TriangleBatcher.cs @@ -10,14 +10,10 @@ public class TriangleBatcher : Behaviour, IFirstFrameUpdate, ILastFrameUpdate, I private static System.Func GetPriority() => (b) => b.Priority; private readonly BehaviourCollector triangleBatches = new(); - private ICamera camera = null!; - private readonly ActiveBehaviourCollectorOrdered drawableShapes = new(GetPriority(), SortByAscendingPriority()); public void FirstActiveFrame() { - camera = Universe.FindRequiredBehaviour(); - drawableShapes.Assign(Universe); triangleBatches.Assign(Universe); } @@ -27,7 +23,7 @@ public class TriangleBatcher : Behaviour, IFirstFrameUpdate, ILastFrameUpdate, I for (int i = 0; i < triangleBatches.Count; i++) { ITriangleBatch triangleBatch = triangleBatches[i]; - triangleBatch.Begin(camera.ViewMatrix, camera.ProjectionMatrix); + triangleBatch.Begin(); for (int j = 0; j < drawableShapes.Count; j++) drawableShapes[j].Draw(triangleBatch); triangleBatch.End(); -- 2.49.1 From c355c666e006003d5df8bdc0f0f3df87ce36715e Mon Sep 17 00:00:00 2001 From: Syntriax Date: Thu, 29 Jan 2026 22:29:19 +0300 Subject: [PATCH 18/51] refactor: fixed LiteNetLibServer using events to subscribe to PostUpdate instead of the interface --- .../LiteNetLibServer.cs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/Engine.Integration/Engine.Integration.LiteNetLib/LiteNetLibServer.cs b/Engine.Integration/Engine.Integration.LiteNetLib/LiteNetLibServer.cs index 9bcee1a..3b0f9cc 100644 --- a/Engine.Integration/Engine.Integration.LiteNetLib/LiteNetLibServer.cs +++ b/Engine.Integration/Engine.Integration.LiteNetLib/LiteNetLibServer.cs @@ -6,7 +6,7 @@ using Engine.Core.Debug; namespace Engine.Systems.Network; -public class LiteNetLibServer : LiteNetLibCommunicatorBase, INetworkCommunicatorServer +public class LiteNetLibServer : LiteNetLibCommunicatorBase, INetworkCommunicatorServer, IPostUpdate { public string Password { get; private set; } = string.Empty; public int MaxConnectionCount { get; private set; } = 2; @@ -104,17 +104,8 @@ public class LiteNetLibServer : LiteNetLibCommunicatorBase, INetworkCommunicator } } - private void PollEvents(IUniverse sender, IUniverse.UpdateArguments args) => Manager.PollEvents(); - - public override void EnterUniverse(IUniverse universe) + public void PostUpdate() { - base.EnterUniverse(universe); - universe.OnPostUpdate.AddListener(PollEvents); - } - - public override void ExitUniverse(IUniverse universe) - { - base.ExitUniverse(universe); - universe.OnPostUpdate.RemoveListener(PollEvents); + Manager.PollEvents(); } } -- 2.49.1 From 64e7321f0f79386d6a284c77000ff53560648b6c Mon Sep 17 00:00:00 2001 From: Syntriax Date: Fri, 30 Jan 2026 10:54:56 +0300 Subject: [PATCH 19/51] fix: rotating file logger deleting from the wrong order --- Engine.Core/Debug/RotatingFileLogger.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Engine.Core/Debug/RotatingFileLogger.cs b/Engine.Core/Debug/RotatingFileLogger.cs index ec3e007..fa4911d 100644 --- a/Engine.Core/Debug/RotatingFileLogger.cs +++ b/Engine.Core/Debug/RotatingFileLogger.cs @@ -55,7 +55,7 @@ public class RotatingFileLogger : ILogger private static void RotateLastLogs(string directory, string prefix, int rotateLength) { IOrderedEnumerable logs = System.IO.Directory.GetFiles(directory, $"{prefix}*.log") - .OrderBy(File.GetCreationTime); + .OrderByDescending(File.GetCreationTime); foreach (string file in logs.Skip(rotateLength)) try -- 2.49.1 From 72f86478f2791e410863727fbd7c67c28f0d1fe5 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Fri, 30 Jan 2026 13:43:48 +0300 Subject: [PATCH 20/51] feat!: added broadcast & routing support for NetworkManager It used to only route or broadcast, now the same packet can be both routed to it's own behaviour AND at the same time the broadcast listeners can get alerted of the same package such as universe-wide managers --- .../Abstract/IPacketListenerClientEntity.cs | 6 + .../Abstract/IPacketListenerServerEntity.cs | 6 + Engine.Systems/Network/NetworkManager.cs | 159 ++++++++++-------- 3 files changed, 104 insertions(+), 67 deletions(-) create mode 100644 Engine.Systems/Network/Abstract/IPacketListenerClientEntity.cs create mode 100644 Engine.Systems/Network/Abstract/IPacketListenerServerEntity.cs diff --git a/Engine.Systems/Network/Abstract/IPacketListenerClientEntity.cs b/Engine.Systems/Network/Abstract/IPacketListenerClientEntity.cs new file mode 100644 index 0000000..46f0ed7 --- /dev/null +++ b/Engine.Systems/Network/Abstract/IPacketListenerClientEntity.cs @@ -0,0 +1,6 @@ +namespace Engine.Systems.Network; + +public interface IPacketListenerClientEntity : INetworkEntity where T : IEntityNetworkPacket +{ + void OnEntityClientPacketArrived(IConnection sender, T packet); +} diff --git a/Engine.Systems/Network/Abstract/IPacketListenerServerEntity.cs b/Engine.Systems/Network/Abstract/IPacketListenerServerEntity.cs new file mode 100644 index 0000000..90e0051 --- /dev/null +++ b/Engine.Systems/Network/Abstract/IPacketListenerServerEntity.cs @@ -0,0 +1,6 @@ +namespace Engine.Systems.Network; + +public interface IPacketListenerServerEntity : INetworkEntity where T : IEntityNetworkPacket +{ + void OnEntityServerPacketArrived(IConnection sender, T packet); +} diff --git a/Engine.Systems/Network/NetworkManager.cs b/Engine.Systems/Network/NetworkManager.cs index 0082b2a..4474997 100644 --- a/Engine.Systems/Network/NetworkManager.cs +++ b/Engine.Systems/Network/NetworkManager.cs @@ -10,18 +10,28 @@ namespace Engine.Systems.Network; /// /// Intermediary manager that looks up in it's hierarchy for a to route/broadcast it's received packets to their destinations. /// +/// TODO: I urgently need to add proper comments on this manager, I don't exactly remember the state I was in when I was writing it. +/// It's a fairly complex manager that relies heavily on Reflection and lots of generic method delegation which is making it very hard to read back. public class NetworkManager : Behaviour, IEnterUniverse, IExitUniverse, INetworkManager { - private readonly Dictionary>> clientPacketArrivalMethods = []; - private readonly Dictionary>> serverPacketArrivalMethods = []; + private readonly Dictionary>> clientBroadcastPacketArrivalMethods = []; + private readonly Dictionary>> serverBroadcastPacketArrivalMethods = []; - private readonly Dictionary> clientPacketRouters = []; - private readonly Dictionary> serverPacketRouters = []; + private readonly Dictionary> clientBroadcastPacketRouters = []; + private readonly Dictionary> serverBroadcastPacketRouters = []; - private readonly List<(Type packetType, Delegate @delegate)> packetRetrievalDelegates = []; + private readonly Dictionary>> clientEntityPacketArrivalMethods = []; + private readonly Dictionary>> serverEntityPacketArrivalMethods = []; + + private readonly Dictionary> clientEntityPacketRouters = []; + private readonly Dictionary> serverEntityPacketRouters = []; + + private readonly List<(Type packetType, Delegate @delegate)> broadcastPacketRetrievalDelegates = []; + private readonly List<(Type packetType, Delegate @delegate)> entityPacketRetrievalDelegates = []; private readonly Dictionary clearRoutesMethods = []; - private readonly Dictionary registerPacketListenersMethods = []; + private readonly Dictionary registerBroadcastPacketListenersMethods = []; + private readonly Dictionary registerEntityPacketListenersMethods = []; private readonly Dictionary _networkEntities = []; public IReadOnlyDictionary NetworkEntities => _networkEntities; @@ -40,63 +50,55 @@ public class NetworkManager : Behaviour, IEnterUniverse, IExitUniverse, INetwork INetworkCommunicator? previousCommunicator = field; field = value; - if (previousCommunicator is not null) UnsubscribeCommunicatorMethods(previousCommunicator); - if (field is not null) SubscribeCommunicatorMethods(field); + if (previousCommunicator is not null) InvokeCommunicatorMethods(previousCommunicator, nameof(INetworkCommunicator.UnsubscribeFromPackets)); + if (field is not null) InvokeCommunicatorMethods(field, nameof(INetworkCommunicator.SubscribeToPackets)); } } = null!; - #region Communicator Subscriptions - private void SubscribeCommunicatorMethods(INetworkCommunicator networkCommunicator) - { - MethodInfo subscribeToPacketsMethod = typeof(INetworkCommunicator) - .GetMethod(nameof(INetworkCommunicator.SubscribeToPackets), BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)!; - - foreach ((Type packetType, Delegate @delegate) in packetRetrievalDelegates) - { - MethodInfo genericSubscribeMethod = subscribeToPacketsMethod.MakeGenericMethod(packetType); - genericSubscribeMethod.Invoke(networkCommunicator, [@delegate]); - } - } - - private void UnsubscribeCommunicatorMethods(INetworkCommunicator networkCommunicator) + /// Used to delegate subscription and unsubscription methods on the to . + private void InvokeCommunicatorMethods(INetworkCommunicator networkCommunicator, string name) { MethodInfo unsubscribeFromPacketsMethod = typeof(INetworkCommunicator) - .GetMethod(nameof(INetworkCommunicator.UnsubscribeFromPackets), BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)!; + .GetMethod(name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)!; - foreach ((Type packetType, Delegate @delegate) in packetRetrievalDelegates) + /// Get unique entries by the packetType so we don't get duplicate calls to + /// Because a class might have both and + /// or and together. + IEnumerable<(Type packetType, Delegate @delegate)> distinctRetrievalSubscriptionDelegates = + broadcastPacketRetrievalDelegates.Concat(entityPacketRetrievalDelegates).DistinctBy(pair => pair.packetType); + + foreach ((Type packetType, Delegate @delegate) in distinctRetrievalSubscriptionDelegates) { - MethodInfo genericUnsubscribeMethod = unsubscribeFromPacketsMethod.MakeGenericMethod(packetType); - genericUnsubscribeMethod.Invoke(networkCommunicator, [@delegate]); + MethodInfo genericMethod = unsubscribeFromPacketsMethod.MakeGenericMethod(packetType); + genericMethod.Invoke(networkCommunicator, [@delegate]); } } - #endregion - #region Packet Routing + #region Packet Routing/Broadcasting private void OnPacketReceived(IConnection sender, T entityDataPacket) { + BroadcastPacket(sender, entityDataPacket); if (entityDataPacket is IEntityNetworkPacket entityPacket) RoutePacket(sender, entityDataPacket, entityPacket); - else - BroadcastPacket(sender, entityDataPacket); } private void RoutePacket(IConnection sender, T entityDataPacket, IEntityNetworkPacket entityPacket) { if (NetworkCommunicator is INetworkCommunicatorClient) - RoutePacket(clientPacketRouters, entityPacket.EntityId, sender, entityDataPacket); + RoutePacket(clientEntityPacketRouters, entityPacket.EntityId, sender, entityDataPacket); if (NetworkCommunicator is INetworkCommunicatorServer) - RoutePacket(serverPacketRouters, entityPacket.EntityId, sender, entityDataPacket); + RoutePacket(serverEntityPacketRouters, entityPacket.EntityId, sender, entityDataPacket); } private void BroadcastPacket(IConnection sender, T entityDataPacket) { if (NetworkCommunicator is INetworkCommunicatorClient) - BroadcastPacket(clientPacketRouters, sender, entityDataPacket); + BroadcastPacket(clientBroadcastPacketRouters, sender, entityDataPacket); if (NetworkCommunicator is INetworkCommunicatorServer) - BroadcastPacket(serverPacketRouters, sender, entityDataPacket); + BroadcastPacket(serverBroadcastPacketRouters, sender, entityDataPacket); } - private static void BroadcastPacket( + private void BroadcastPacket( Dictionary> packetRouters, IConnection sender, T entityDataPacket) @@ -111,7 +113,7 @@ public class NetworkManager : Behaviour, IEnterUniverse, IExitUniverse, INetwork } } - private static void RoutePacket( + private void RoutePacket( Dictionary> packetRouters, string entityId, IConnection sender, @@ -133,32 +135,33 @@ public class NetworkManager : Behaviour, IEnterUniverse, IExitUniverse, INetwork INetworkEntity behaviour, Dictionary> packetRouters, Dictionary>> packetArrivalMethods, - NetworkType networkType) + NetworkType networkType, Dictionary registerPacketListenersMethods) { if (!packetArrivalMethods.TryGetValue(behaviour.GetType(), out Dictionary>? arrivalMethods)) return; - foreach ((Type packetType, List methods) in arrivalMethods) - foreach (MethodInfo receiveMethod in methods) + foreach (Type packetType in arrivalMethods.Keys) + { + if (!packetRouters.TryGetValue(packetType, out Dictionary? routers)) { - if (!packetRouters.TryGetValue(packetType, out Dictionary? routers)) - { - routers = []; - packetRouters.Add(packetType, routers); - } - - object packetListenerEvent = CreateEventAndRegister(packetType, behaviour, networkType); - routers.Add(behaviour.Id, packetListenerEvent); + routers = []; + packetRouters.Add(packetType, routers); } + + object packetListenerEvent = + CreateEventAndRegister(packetType, behaviour, networkType, registerPacketListenersMethods); + + routers.Add(behaviour.Id, packetListenerEvent); + } } - private object CreateEventAndRegister(Type packetType, INetworkEntity behaviour, NetworkType networkType) + private object CreateEventAndRegister(Type packetType, INetworkEntity behaviour, NetworkType networkType, Dictionary registerPacketListenersMethods) { Type genericEventType = typeof(Event<,>).MakeGenericType(typeof(IConnection), packetType); object packetListenerEvent = Activator.CreateInstance(genericEventType)!; if (!registerPacketListenersMethods.TryGetValue(packetType, out MethodInfo? registerPacketListenerMethod)) - throw new($"{nameof(RegisterPacketListenerEvent)} for {packetType.Name} has not been cached."); + throw new($"Packet Listener Events for {packetType.Name} has not been cached."); registerPacketListenerMethod.Invoke(this, [behaviour, packetListenerEvent, networkType]); return packetListenerEvent; @@ -176,6 +179,19 @@ public class NetworkManager : Behaviour, IEnterUniverse, IExitUniverse, INetwork } } + private static void RegisterEntityPacketListenerEvent( + INetworkEntity behaviour, + Event packetListenerEvent, + NetworkType networkType + ) where T : IEntityNetworkPacket + { + switch (networkType) + { + case NetworkType.Client: packetListenerEvent.AddListener((sender, packet) => ((IPacketListenerClientEntity)behaviour).OnEntityClientPacketArrived(sender, packet)); break; + case NetworkType.Server: packetListenerEvent.AddListener((sender, packet) => ((IPacketListenerServerEntity)behaviour).OnEntityServerPacketArrived(sender, packet)); break; + } + } + private void UnregisterPacketRoutersFor( INetworkEntity behaviour, Dictionary> packetRouters, @@ -215,8 +231,11 @@ public class NetworkManager : Behaviour, IEnterUniverse, IExitUniverse, INetwork if (!_networkEntities.TryAdd(collectedBehaviour.Id, collectedBehaviour)) throw new($"Unable to add {collectedBehaviour.Id} to {nameof(NetworkManager)}"); - RegisterPacketRoutersFor(collectedBehaviour, clientPacketRouters, clientPacketArrivalMethods, NetworkType.Client); - RegisterPacketRoutersFor(collectedBehaviour, serverPacketRouters, serverPacketArrivalMethods, NetworkType.Server); + RegisterPacketRoutersFor(collectedBehaviour, clientBroadcastPacketRouters, clientBroadcastPacketArrivalMethods, NetworkType.Client, registerBroadcastPacketListenersMethods); + RegisterPacketRoutersFor(collectedBehaviour, clientEntityPacketRouters, clientEntityPacketArrivalMethods, NetworkType.Client, registerEntityPacketListenersMethods); + + RegisterPacketRoutersFor(collectedBehaviour, serverBroadcastPacketRouters, serverBroadcastPacketArrivalMethods, NetworkType.Server, registerBroadcastPacketListenersMethods); + RegisterPacketRoutersFor(collectedBehaviour, serverEntityPacketRouters, serverEntityPacketArrivalMethods, NetworkType.Server, registerEntityPacketListenersMethods); } private void OnRemoved(IBehaviourCollector sender, IBehaviourCollector.BehaviourRemovedArguments args) @@ -225,8 +244,11 @@ public class NetworkManager : Behaviour, IEnterUniverse, IExitUniverse, INetwork if (!_networkEntities.Remove(args.BehaviourRemoved.Id)) return; - UnregisterPacketRoutersFor(removedBehaviour, clientPacketRouters, clientPacketArrivalMethods); - UnregisterPacketRoutersFor(removedBehaviour, serverPacketRouters, serverPacketArrivalMethods); + UnregisterPacketRoutersFor(removedBehaviour, clientBroadcastPacketRouters, clientBroadcastPacketArrivalMethods); + UnregisterPacketRoutersFor(removedBehaviour, clientEntityPacketRouters, clientEntityPacketArrivalMethods); + + UnregisterPacketRoutersFor(removedBehaviour, serverBroadcastPacketRouters, serverBroadcastPacketArrivalMethods); + UnregisterPacketRoutersFor(removedBehaviour, serverEntityPacketRouters, serverEntityPacketArrivalMethods); } public void ExitUniverse(IUniverse universe) => _networkEntityCollector.Unassign(); @@ -240,7 +262,8 @@ public class NetworkManager : Behaviour, IEnterUniverse, IExitUniverse, INetwork #region Initialization public NetworkManager() { - CachePacketRetrievalDelegates(); + CachePacketRetrievalDelegates(typeof(INetworkPacket), broadcastPacketRetrievalDelegates); + CachePacketRetrievalDelegates(typeof(IEntityNetworkPacket), entityPacketRetrievalDelegates); CacheRegistrationMethods(); CachePacketArrivalMethods(); @@ -248,45 +271,47 @@ public class NetworkManager : Behaviour, IEnterUniverse, IExitUniverse, INetwork _networkEntityCollector.OnRemoved.AddListener(OnRemoved); } - private void CachePacketRetrievalDelegates() + private void CachePacketRetrievalDelegates(Type packetType, List<(Type packetType, Delegate @delegate)> retrievalDelegates) { IEnumerable packetTypes = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()) - .Where(t => typeof(INetworkPacket).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract && !t.IsGenericType); + .Where(t => packetType.IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract && !t.IsGenericType); MethodInfo onPacketArrivedMethod = GetType() .GetMethod(nameof(OnPacketReceived), BindingFlags.NonPublic | BindingFlags.Instance)!; - foreach (Type packetType in packetTypes) + foreach (Type pType in packetTypes) { - MethodInfo genericOnPacketArrivedMethod = onPacketArrivedMethod.MakeGenericMethod(packetType); + MethodInfo genericOnPacketArrivedMethod = onPacketArrivedMethod.MakeGenericMethod(pType); - Type genericDelegateType = typeof(Event<,>.EventHandler).MakeGenericType(typeof(IConnection), packetType); + Type genericDelegateType = typeof(Event<,>.EventHandler).MakeGenericType(typeof(IConnection), pType); Delegate genericPacketReceivedDelegate = Delegate.CreateDelegate(genericDelegateType, this, genericOnPacketArrivedMethod); - - packetRetrievalDelegates.Add((packetType, genericPacketReceivedDelegate)); + retrievalDelegates.Add((pType, genericPacketReceivedDelegate)); } } private void CacheRegistrationMethods() { - CacheRegistrationMethods(registerPacketListenersMethods, nameof(RegisterPacketListenerEvent)); - CacheRegistrationMethods(clearRoutesMethods, nameof(ClearRouter)); + CacheRegistrationMethods(registerBroadcastPacketListenersMethods, nameof(RegisterPacketListenerEvent), broadcastPacketRetrievalDelegates); + CacheRegistrationMethods(registerEntityPacketListenersMethods, nameof(RegisterEntityPacketListenerEvent), entityPacketRetrievalDelegates); + CacheRegistrationMethods(clearRoutesMethods, nameof(ClearRouter), broadcastPacketRetrievalDelegates); } - private void CacheRegistrationMethods(Dictionary registrationMethods, string methodName) + private void CacheRegistrationMethods(Dictionary registrationMethods, string methodName, List<(Type packetType, Delegate @delegate)> packetRetrievalDelegates) { MethodInfo registerPacketMethod = typeof(NetworkManager).GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static)!; foreach ((Type packetType, Delegate @delegate) in packetRetrievalDelegates) { MethodInfo genericMethod = registerPacketMethod.MakeGenericMethod(packetType); - registrationMethods.Add(packetType, genericMethod); + registrationMethods.TryAdd(packetType, genericMethod); } } private void CachePacketArrivalMethods() { - CachePacketArrivalMethods(clientPacketArrivalMethods, typeof(IPacketListenerClient<>), nameof(IPacketListenerClient.OnClientPacketArrived)); - CachePacketArrivalMethods(serverPacketArrivalMethods, typeof(IPacketListenerServer<>), nameof(IPacketListenerServer.OnServerPacketArrived)); + CachePacketArrivalMethods(clientBroadcastPacketArrivalMethods, typeof(IPacketListenerClient<>), nameof(IPacketListenerClient<>.OnClientPacketArrived)); + CachePacketArrivalMethods(serverBroadcastPacketArrivalMethods, typeof(IPacketListenerServer<>), nameof(IPacketListenerServer<>.OnServerPacketArrived)); + CachePacketArrivalMethods(clientEntityPacketArrivalMethods, typeof(IPacketListenerClientEntity<>), nameof(IPacketListenerClientEntity<>.OnEntityClientPacketArrived)); + CachePacketArrivalMethods(serverEntityPacketArrivalMethods, typeof(IPacketListenerServerEntity<>), nameof(IPacketListenerServerEntity<>.OnEntityServerPacketArrived)); } private static void CachePacketArrivalMethods(Dictionary>> packetArrivalMethods, Type listenerType, string packetArrivalMethodName) -- 2.49.1 From 7675f9acaca4347fc87957f2779a6c53a2ad10e8 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Fri, 30 Jan 2026 18:02:17 +0300 Subject: [PATCH 21/51] chore: added missing Matrix4x4NetPacker --- .../LiteNetLibCommunicatorBase.cs | 1 + .../Packers/Matrix4x4NetPacker.cs | 61 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 Engine.Integration/Engine.Integration.LiteNetLib/Packers/Matrix4x4NetPacker.cs diff --git a/Engine.Integration/Engine.Integration.LiteNetLib/LiteNetLibCommunicatorBase.cs b/Engine.Integration/Engine.Integration.LiteNetLib/LiteNetLibCommunicatorBase.cs index daa7a0d..78eb3b3 100644 --- a/Engine.Integration/Engine.Integration.LiteNetLib/LiteNetLibCommunicatorBase.cs +++ b/Engine.Integration/Engine.Integration.LiteNetLib/LiteNetLibCommunicatorBase.cs @@ -178,6 +178,7 @@ public abstract class LiteNetLibCommunicatorBase : Behaviour, IEnterUniverse, IE netPacketProcessor.RegisterNestedType(Vector3DNetPacker.Write, Vector3DNetPacker.Read); netPacketProcessor.RegisterNestedType(Vector3DIntNetPacker.Write, Vector3DIntNetPacker.Read); netPacketProcessor.RegisterNestedType(Vector4DNetPacker.Write, Vector4DNetPacker.Read); + netPacketProcessor.RegisterNestedType(Matrix4x4NetPacker.Write, Matrix4x4NetPacker.Read); } public INetworkCommunicator SubscribeToPackets(Event.EventHandler callback) diff --git a/Engine.Integration/Engine.Integration.LiteNetLib/Packers/Matrix4x4NetPacker.cs b/Engine.Integration/Engine.Integration.LiteNetLib/Packers/Matrix4x4NetPacker.cs new file mode 100644 index 0000000..a72536b --- /dev/null +++ b/Engine.Integration/Engine.Integration.LiteNetLib/Packers/Matrix4x4NetPacker.cs @@ -0,0 +1,61 @@ +using LiteNetLib.Utils; + +using Engine.Core; + +namespace Engine.Systems.Network.Packers; + +internal static class Matrix4x4NetPacker +{ + internal static void Write(NetDataWriter writer, Matrix4x4 data) + { + writer.Put(data.M11); + writer.Put(data.M12); + writer.Put(data.M13); + writer.Put(data.M14); + + writer.Put(data.M21); + writer.Put(data.M22); + writer.Put(data.M23); + writer.Put(data.M24); + + writer.Put(data.M31); + writer.Put(data.M32); + writer.Put(data.M33); + writer.Put(data.M34); + + writer.Put(data.M41); + writer.Put(data.M42); + writer.Put(data.M43); + writer.Put(data.M44); + } + + internal static Matrix4x4 Read(NetDataReader reader) + { + float m11 = reader.GetFloat(); + float m12 = reader.GetFloat(); + float m13 = reader.GetFloat(); + float m14 = reader.GetFloat(); + + float m21 = reader.GetFloat(); + float m22 = reader.GetFloat(); + float m23 = reader.GetFloat(); + float m24 = reader.GetFloat(); + + float m31 = reader.GetFloat(); + float m32 = reader.GetFloat(); + float m33 = reader.GetFloat(); + float m34 = reader.GetFloat(); + + float m41 = reader.GetFloat(); + float m42 = reader.GetFloat(); + float m43 = reader.GetFloat(); + float m44 = reader.GetFloat(); + + return new Matrix4x4( + m11, m12, m13, m14, + m21, m22, m23, m24, + m31, m32, m33, m34, + m41, m42, m43, m44 + ); + } +} -- 2.49.1 From 4e9fda3d7c510044bd905d0a83b0983f2f06b6f7 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Fri, 30 Jan 2026 18:33:07 +0300 Subject: [PATCH 22/51] refactor: auto property on FastListOrdered --- Engine.Core/Helpers/FastListOrdered.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Engine.Core/Helpers/FastListOrdered.cs b/Engine.Core/Helpers/FastListOrdered.cs index d99c826..cd72982 100644 --- a/Engine.Core/Helpers/FastListOrdered.cs +++ b/Engine.Core/Helpers/FastListOrdered.cs @@ -16,8 +16,7 @@ public class FastListOrdered : IList, IReadOnlyList private readonly Func getIndexFunc = null!; private readonly IComparer sortBy = null!; - private int count = 0; - public int Count => count; + public int Count { get; private set; } = 0; public bool IsReadOnly { get; set; } = false; @@ -35,10 +34,10 @@ public class FastListOrdered : IList, IReadOnlyList private (TIndex TIndex, int i) GetAt(Index index) { int actualIndex = index.IsFromEnd - ? count - index.Value + ? Count - index.Value : index.Value; - if (actualIndex < 0 || actualIndex >= count) + if (actualIndex < 0 || actualIndex >= Count) throw new IndexOutOfRangeException(); int leftIndex = actualIndex; @@ -75,7 +74,7 @@ public class FastListOrdered : IList, IReadOnlyList items[key] = list = []; list.Add(item); - count++; + Count++; } public void Insert(int index, TItem item) @@ -88,7 +87,7 @@ public class FastListOrdered : IList, IReadOnlyList items[tIndex] = list = []; list.Insert(index, item); - count++; + Count++; } public bool Remove(TItem item) @@ -103,7 +102,7 @@ public class FastListOrdered : IList, IReadOnlyList if (!list.Remove(item)) return false; - count--; + Count--; return true; } @@ -114,7 +113,7 @@ public class FastListOrdered : IList, IReadOnlyList (TIndex tIndex, int i) = GetAt(index); items[tIndex].RemoveAt(i); - count--; + Count--; } public void Clear() @@ -125,7 +124,7 @@ public class FastListOrdered : IList, IReadOnlyList foreach ((TIndex index, FastList list) in items) list.Clear(); - count = 0; + Count = 0; } public bool Contains(TItem item) -- 2.49.1 From 913af2a4a4cb01908e9d46507a42892e6adf3741 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Sat, 31 Jan 2026 00:49:00 +0300 Subject: [PATCH 23/51] fix: added dynamic index updates during event invocation so there are no missing/duplicate invocations This also makes the events very not ideal for multithreaded applications at the moment. --- Engine.Core/Helpers/Event.cs | 93 +++++++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 27 deletions(-) diff --git a/Engine.Core/Helpers/Event.cs b/Engine.Core/Helpers/Event.cs index c008c52..042a29b 100644 --- a/Engine.Core/Helpers/Event.cs +++ b/Engine.Core/Helpers/Event.cs @@ -58,6 +58,9 @@ public class Event public ILogger Logger { get; set => field = value ?? ILogger.Shared; } = ILogger.Shared; + private int currentOnceCallIndex = -1; // These are for the purpose of if a listener is added/removed during invocation the index is dynamically updated so no missing/duplicate invocations happen. + private int currentCallIndex = -1; // These are for the purpose of if a listener is added/removed during invocation the index is dynamically updated so no missing/duplicate invocations happen. + private readonly List listeners = null!; private readonly List onceListeners = null!; @@ -74,6 +77,9 @@ public class Event if (insertIndex < 0) insertIndex = ~insertIndex; + if (insertIndex < currentCallIndex) + currentCallIndex++; + listeners.Insert(insertIndex, listenerData); } @@ -90,6 +96,9 @@ public class Event if (insertIndex < 0) insertIndex = ~insertIndex; + if (insertIndex < currentOnceCallIndex) + currentOnceCallIndex++; + onceListeners.Insert(insertIndex, listenerData); } @@ -103,6 +112,8 @@ public class Event if (listeners[i].Callback == listener) { listeners.RemoveAt(i); + if (i < currentCallIndex) + currentCallIndex--; return; } } @@ -117,6 +128,8 @@ public class Event if (onceListeners[i].Callback == listener) { onceListeners.RemoveAt(i); + if (i < currentOnceCallIndex) + currentOnceCallIndex--; return; } } @@ -131,23 +144,23 @@ public class Event /// public void Invoke() { - for (int i = listeners.Count - 1; i >= 0; i--) - try { listeners[i].Callback.Invoke(); } + for (currentCallIndex = listeners.Count - 1; currentCallIndex >= 0; currentCallIndex--) + try { listeners[currentCallIndex].Callback.Invoke(); } catch (Exception exception) { - string methodCallRepresentation = $"{listeners[i].Callback.Method.DeclaringType?.FullName}.{listeners[i].Callback.Method.Name}()"; - EventHelpers.LogInvocationException(listeners[i].Callback.Target ?? this, Logger, exception, methodCallRepresentation); + string methodCallRepresentation = $"{listeners[currentCallIndex].Callback.Method.DeclaringType?.FullName}.{listeners[currentCallIndex].Callback.Method.Name}()"; + EventHelpers.LogInvocationException(listeners[currentCallIndex].Callback.Target ?? this, Logger, exception, methodCallRepresentation); } - for (int i = onceListeners.Count - 1; i >= 0; i--) + for (currentOnceCallIndex = onceListeners.Count - 1; currentOnceCallIndex >= 0; currentOnceCallIndex--) { - try { onceListeners[i].Callback.Invoke(); } + try { onceListeners[currentOnceCallIndex].Callback.Invoke(); } catch (Exception exception) { - string methodCallRepresentation = $"{onceListeners[i].Callback.Method.DeclaringType?.FullName}.{onceListeners[i].Callback.Method.Name}()"; - EventHelpers.LogInvocationException(onceListeners[i].Callback.Target ?? this, Logger, exception, methodCallRepresentation); + string methodCallRepresentation = $"{onceListeners[currentOnceCallIndex].Callback.Method.DeclaringType?.FullName}.{onceListeners[currentOnceCallIndex].Callback.Method.Name}()"; + EventHelpers.LogInvocationException(onceListeners[currentOnceCallIndex].Callback.Target ?? this, Logger, exception, methodCallRepresentation); } - onceListeners.RemoveAt(i); + onceListeners.RemoveAt(currentOnceCallIndex); } } @@ -216,6 +229,9 @@ public class Event where TSender : class public ILogger Logger { get; set => field = value ?? ILogger.Shared; } = ILogger.Shared; + private int currentOnceCallIndex = -1; // These are for the purpose of if a listener is added/removed during invocation the index is dynamically updated so no missing/duplicate invocations happen. + private int currentCallIndex = -1; // These are for the purpose of if a listener is added/removed during invocation the index is dynamically updated so no missing/duplicate invocations happen. + private readonly List listeners = null!; private readonly List onceListeners = null!; @@ -232,6 +248,9 @@ public class Event where TSender : class if (insertIndex < 0) insertIndex = ~insertIndex; + if (insertIndex < currentCallIndex) + currentCallIndex++; + listeners.Insert(insertIndex, listenerData); } @@ -248,6 +267,9 @@ public class Event where TSender : class if (insertIndex < 0) insertIndex = ~insertIndex; + if (insertIndex < currentOnceCallIndex) + currentOnceCallIndex++; + onceListeners.Insert(insertIndex, listenerData); } @@ -261,6 +283,8 @@ public class Event where TSender : class if (listeners[i].Callback == listener) { listeners.RemoveAt(i); + if (i < currentCallIndex) + currentCallIndex--; return; } } @@ -275,6 +299,8 @@ public class Event where TSender : class if (onceListeners[i].Callback == listener) { onceListeners.RemoveAt(i); + if (i < currentOnceCallIndex) + currentOnceCallIndex--; return; } } @@ -290,23 +316,23 @@ public class Event where TSender : class /// The caller that's triggering this event. public void Invoke(TSender sender) { - for (int i = listeners.Count - 1; i >= 0; i--) - try { listeners[i].Callback.Invoke(sender); } + for (currentCallIndex = listeners.Count - 1; currentCallIndex >= 0; currentCallIndex--) + try { listeners[currentCallIndex].Callback.Invoke(sender); } catch (Exception exception) { - string methodCallRepresentation = $"{listeners[i].Callback.Method.DeclaringType?.FullName}.{listeners[i].Callback.Method.Name}({sender})"; - EventHelpers.LogInvocationException(listeners[i].Callback.Target ?? sender, Logger, exception, methodCallRepresentation); + string methodCallRepresentation = $"{listeners[currentCallIndex].Callback.Method.DeclaringType?.FullName}.{listeners[currentCallIndex].Callback.Method.Name}({sender})"; + EventHelpers.LogInvocationException(listeners[currentCallIndex].Callback.Target ?? sender, Logger, exception, methodCallRepresentation); } - for (int i = onceListeners.Count - 1; i >= 0; i--) + for (currentOnceCallIndex = onceListeners.Count - 1; currentOnceCallIndex >= 0; currentOnceCallIndex--) { - try { onceListeners[i].Callback.Invoke(sender); } + try { onceListeners[currentOnceCallIndex].Callback.Invoke(sender); } catch (Exception exception) { - string methodCallRepresentation = $"{onceListeners[i].Callback.Method.DeclaringType?.FullName}.{onceListeners[i].Callback.Method.Name}({sender})"; - EventHelpers.LogInvocationException(onceListeners[i].Callback.Target ?? sender, Logger, exception, methodCallRepresentation); + string methodCallRepresentation = $"{onceListeners[currentOnceCallIndex].Callback.Method.DeclaringType?.FullName}.{onceListeners[currentOnceCallIndex].Callback.Method.Name}({sender})"; + EventHelpers.LogInvocationException(onceListeners[currentOnceCallIndex].Callback.Target ?? sender, Logger, exception, methodCallRepresentation); } - onceListeners.RemoveAt(i); + onceListeners.RemoveAt(currentOnceCallIndex); } } @@ -382,6 +408,9 @@ public class Event where TSender : class public ILogger Logger { get; set => field = value ?? ILogger.Shared; } = ILogger.Shared; + private int currentOnceCallIndex = -1; // These are for the purpose of if a listener is added/removed during invocation the index is dynamically updated so no missing/duplicate invocations happen. + private int currentCallIndex = -1; // These are for the purpose of if a listener is added/removed during invocation the index is dynamically updated so no missing/duplicate invocations happen. + private readonly List listeners = null!; private readonly List onceListeners = null!; @@ -398,6 +427,9 @@ public class Event where TSender : class if (insertIndex < 0) insertIndex = ~insertIndex; + if (insertIndex < currentCallIndex) + currentCallIndex++; + listeners.Insert(insertIndex, listenerData); } @@ -414,6 +446,9 @@ public class Event where TSender : class if (insertIndex < 0) insertIndex = ~insertIndex; + if (insertIndex < currentOnceCallIndex) + currentOnceCallIndex++; + onceListeners.Insert(insertIndex, listenerData); } @@ -427,6 +462,8 @@ public class Event where TSender : class if (listeners[i].Callback == listener) { listeners.RemoveAt(i); + if (i < currentCallIndex) + currentCallIndex--; return; } } @@ -441,6 +478,8 @@ public class Event where TSender : class if (onceListeners[i].Callback == listener) { onceListeners.RemoveAt(i); + if (i < currentOnceCallIndex) + currentOnceCallIndex--; return; } } @@ -457,23 +496,23 @@ public class Event where TSender : class /// The arguments provided for this event. public void Invoke(TSender sender, TArguments args) { - for (int i = listeners.Count - 1; i >= 0; i--) - try { listeners[i].Callback.Invoke(sender, args); } + for (currentCallIndex = listeners.Count - 1; currentCallIndex >= 0; currentCallIndex--) + try { listeners[currentCallIndex].Callback.Invoke(sender, args); } catch (Exception exception) { - string methodCallRepresentation = $"{listeners[i].Callback.Method.DeclaringType?.FullName}.{listeners[i].Callback.Method.Name}({sender}, {args})"; - EventHelpers.LogInvocationException(listeners[i].Callback.Target ?? sender, Logger, exception, methodCallRepresentation); + string methodCallRepresentation = $"{listeners[currentCallIndex].Callback.Method.DeclaringType?.FullName}.{listeners[currentCallIndex].Callback.Method.Name}({sender}, {args})"; + EventHelpers.LogInvocationException(listeners[currentCallIndex].Callback.Target ?? sender, Logger, exception, methodCallRepresentation); } - for (int i = onceListeners.Count - 1; i >= 0; i--) + for (currentOnceCallIndex = onceListeners.Count - 1; currentOnceCallIndex >= 0; currentOnceCallIndex--) { - try { onceListeners[i].Callback.Invoke(sender, args); } + try { onceListeners[currentOnceCallIndex].Callback.Invoke(sender, args); } catch (Exception exception) { - string methodCallRepresentation = $"{onceListeners[i].Callback.Method.DeclaringType?.FullName}.{onceListeners[i].Callback.Method.Name}({sender}, {args})"; - EventHelpers.LogInvocationException(onceListeners[i].Callback.Target ?? sender, Logger, exception, methodCallRepresentation); + string methodCallRepresentation = $"{onceListeners[currentOnceCallIndex].Callback.Method.DeclaringType?.FullName}.{onceListeners[currentOnceCallIndex].Callback.Method.Name}({sender}, {args})"; + EventHelpers.LogInvocationException(onceListeners[currentOnceCallIndex].Callback.Target ?? sender, Logger, exception, methodCallRepresentation); } - onceListeners.RemoveAt(i); + onceListeners.RemoveAt(currentOnceCallIndex); } } -- 2.49.1 From 882f9e8b29e3f0496fe99059f97056d9f4869449 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Sat, 31 Jan 2026 13:06:57 +0300 Subject: [PATCH 24/51] feat: added new yields --- Engine.Core/Systems/CoroutineYield.cs | 10 ---------- Engine.Core/Systems/Yields/WaitUntilYield.cs | 10 ++++++++++ .../Tween/Yields/WaitForTweenCompleteCoroutineYield.cs | 2 +- .../Tween/Yields/WaitForTweenDoneCoroutineYield.cs | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 Engine.Core/Systems/CoroutineYield.cs create mode 100644 Engine.Core/Systems/Yields/WaitUntilYield.cs diff --git a/Engine.Core/Systems/CoroutineYield.cs b/Engine.Core/Systems/CoroutineYield.cs deleted file mode 100644 index 44e3f5b..0000000 --- a/Engine.Core/Systems/CoroutineYield.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Engine.Core; - -public class CoroutineYield(Func condition) : ICoroutineYield -{ - private readonly Func condition = condition; - - public bool Yield() => condition.Invoke(); -} diff --git a/Engine.Core/Systems/Yields/WaitUntilYield.cs b/Engine.Core/Systems/Yields/WaitUntilYield.cs new file mode 100644 index 0000000..18d29f9 --- /dev/null +++ b/Engine.Core/Systems/Yields/WaitUntilYield.cs @@ -0,0 +1,10 @@ +using System; + +namespace Engine.Core; + +public class WaitUntilYield(Func condition) : ICoroutineYield +{ + private readonly Func condition = condition; + + public bool Yield() => !condition.Invoke(); +} diff --git a/Engine.Systems/Tween/Yields/WaitForTweenCompleteCoroutineYield.cs b/Engine.Systems/Tween/Yields/WaitForTweenCompleteCoroutineYield.cs index b574530..db4702e 100644 --- a/Engine.Systems/Tween/Yields/WaitForTweenCompleteCoroutineYield.cs +++ b/Engine.Systems/Tween/Yields/WaitForTweenCompleteCoroutineYield.cs @@ -2,4 +2,4 @@ using Engine.Core; namespace Engine.Systems.Tween; -public class WaitForTweenCompleteCoroutineYield(ITween tween) : CoroutineYield(() => tween.State == TweenState.Completed); +public class WaitForTweenCompleteCoroutineYield(ITween tween) : WaitUntilYield(() => tween.State == TweenState.Completed); diff --git a/Engine.Systems/Tween/Yields/WaitForTweenDoneCoroutineYield.cs b/Engine.Systems/Tween/Yields/WaitForTweenDoneCoroutineYield.cs index 3ab3d78..c57853e 100644 --- a/Engine.Systems/Tween/Yields/WaitForTweenDoneCoroutineYield.cs +++ b/Engine.Systems/Tween/Yields/WaitForTweenDoneCoroutineYield.cs @@ -2,4 +2,4 @@ using Engine.Core; namespace Engine.Systems.Tween; -public class WaitWhileTweenActiveCoroutineYield(ITween tween) : CoroutineYield(() => tween.State.CheckFlag(TweenState.Completed | TweenState.Cancelled)); +public class WaitWhileTweenActiveCoroutineYield(ITween tween) : WaitUntilYield(() => tween.State.CheckFlag(TweenState.Completed | TweenState.Cancelled)); -- 2.49.1 From 1d6b9d242107c55f7a8848eede7ea55e21ce8da0 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Sat, 31 Jan 2026 13:08:59 +0300 Subject: [PATCH 25/51] feat: added WaitForSeconds and WaitWhile yields --- Engine.Core/Systems/Yields/WaitForSecondsYield.cs | 10 ++++++++++ Engine.Core/Systems/Yields/WaitWhileYield.cs | 10 ++++++++++ 2 files changed, 20 insertions(+) create mode 100644 Engine.Core/Systems/Yields/WaitForSecondsYield.cs create mode 100644 Engine.Core/Systems/Yields/WaitWhileYield.cs diff --git a/Engine.Core/Systems/Yields/WaitForSecondsYield.cs b/Engine.Core/Systems/Yields/WaitForSecondsYield.cs new file mode 100644 index 0000000..da82083 --- /dev/null +++ b/Engine.Core/Systems/Yields/WaitForSecondsYield.cs @@ -0,0 +1,10 @@ +using System; + +namespace Engine.Core; + +public class WaitForSecondsYield(float seconds) : ICoroutineYield +{ + private readonly DateTime triggerTime = DateTime.UtcNow.AddSeconds(seconds); + + public bool Yield() => DateTime.UtcNow < triggerTime; +} diff --git a/Engine.Core/Systems/Yields/WaitWhileYield.cs b/Engine.Core/Systems/Yields/WaitWhileYield.cs new file mode 100644 index 0000000..0c1bb4f --- /dev/null +++ b/Engine.Core/Systems/Yields/WaitWhileYield.cs @@ -0,0 +1,10 @@ +using System; + +namespace Engine.Core; + +public class WaitWhileYield(Func condition) : ICoroutineYield +{ + private readonly Func condition = condition; + + public bool Yield() => condition.Invoke(); +} -- 2.49.1 From b2cfb2a5904a0817a926c2eae609fd5980cc0a2b Mon Sep 17 00:00:00 2001 From: Syntriax Date: Sun, 1 Feb 2026 13:34:02 +0300 Subject: [PATCH 26/51] docs: added NetworkManager comments --- Engine.Systems/Network/NetworkManager.cs | 385 +++++++++++++++++------ 1 file changed, 294 insertions(+), 91 deletions(-) diff --git a/Engine.Systems/Network/NetworkManager.cs b/Engine.Systems/Network/NetworkManager.cs index 4474997..2847efe 100644 --- a/Engine.Systems/Network/NetworkManager.cs +++ b/Engine.Systems/Network/NetworkManager.cs @@ -10,35 +10,127 @@ namespace Engine.Systems.Network; /// /// Intermediary manager that looks up in it's hierarchy for a to route/broadcast it's received packets to their destinations. /// -/// TODO: I urgently need to add proper comments on this manager, I don't exactly remember the state I was in when I was writing it. +/// TODO: I need to peer check this class, I don't exactly remember the state I was in when I was originally writing it and left it uncommented and the current comments are added later on. /// It's a fairly complex manager that relies heavily on Reflection and lots of generic method delegation which is making it very hard to read back. public class NetworkManager : Behaviour, IEnterUniverse, IExitUniverse, INetworkManager { - private readonly Dictionary>> clientBroadcastPacketArrivalMethods = []; - private readonly Dictionary>> serverBroadcastPacketArrivalMethods = []; + #region Packet Router/Broadcaster to Listener Delegates - private readonly Dictionary> clientBroadcastPacketRouters = []; - private readonly Dictionary> serverBroadcastPacketRouters = []; + /// + /// Behaviour Type → Packet Type → List of listener methods (broadcast packets, client-side) + /// + private readonly Dictionary>> clientBroadcastPacketListenerMethods = []; - private readonly Dictionary>> clientEntityPacketArrivalMethods = []; - private readonly Dictionary>> serverEntityPacketArrivalMethods = []; + /// + /// Behaviour Type → Packet Type → List of listener methods (broadcast packets, server-side) + /// + private readonly Dictionary>> serverBroadcastPacketListenerMethods = []; - private readonly Dictionary> clientEntityPacketRouters = []; - private readonly Dictionary> serverEntityPacketRouters = []; + /// + /// Behaviour Type → Packet Type → List of listener methods (entity packets, client-side) + /// + private readonly Dictionary>> clientEntityPacketListenerMethods = []; - private readonly List<(Type packetType, Delegate @delegate)> broadcastPacketRetrievalDelegates = []; - private readonly List<(Type packetType, Delegate @delegate)> entityPacketRetrievalDelegates = []; + /// + /// Behaviour Type → Packet Type → List of listener methods (entity packets, server-side) + /// + private readonly Dictionary>> serverEntityPacketListenerMethods = []; + #endregion + + #region Packet Router/Broadcaster Events + + /// + /// Packet Type → Behaviour.Id → Broadcaster Event (broadcast, client-side) + /// + private readonly Dictionary> clientPacketBroadcastEvents = []; + + /// + /// Packet Type → Behaviour.Id → Broadcaster Event (broadcast, server-side) + /// + private readonly Dictionary> serverPacketBroadcastEvents = []; + + /// + /// Maps an type to a set of routing events, + /// keyed by , for CLIENT entity listeners. + /// The packet is routed to the correct instance + /// by matching . + /// + private readonly Dictionary> clientEntityPacketRouterEvents = []; + + /// + /// Maps an type to a set of routing events, + /// keyed by , for SERVER entity listeners. + /// The packet is routed to the correct instance + /// by matching . + /// + private readonly Dictionary> serverEntityPacketRouterEvents = []; + + #endregion + + #region Packet Retrieval Delegates + + /// + /// Stores delegates that connect incoming broadcast packets from + /// to . + /// These are used to subscribe/unsubscribe from events. + /// + private readonly List broadcastPacketRetrievalSubscriptionDelegates = []; + + /// + /// Stores delegates that connect incoming entity packets from + /// to . + /// These are used to subscribe/unsubscribe from events. + /// + private readonly List entityPacketRetrievalSubscriptionDelegates = []; + + /// + /// Stores delegates that connect incoming all packets from to + /// . This is a combination of all subscription + /// delegates filtered so there are no duplicates packet entries. + /// + private readonly List uniqueRetrievalSubscriptionDelegates = []; + + #endregion + + #region Method Caches + + /// + /// Packet type → method. + /// private readonly Dictionary clearRoutesMethods = []; + + /// + /// Packet type → method. + /// private readonly Dictionary registerBroadcastPacketListenersMethods = []; + + /// + /// Packet type → method. + /// private readonly Dictionary registerEntityPacketListenersMethods = []; + #endregion + + #region Network Entity Collector + + /// + /// All active network , keyed by . + /// private readonly Dictionary _networkEntities = []; public IReadOnlyDictionary NetworkEntities => _networkEntities; + /// + /// Collector responsible for detecting s entering/leaving the universe. + /// private readonly BehaviourCollector _networkEntityCollector = new(); + public IBehaviourCollector NetworkEntityCollector => _networkEntityCollector; + #endregion + + #region Network Communicator + public INetworkCommunicator NetworkCommunicator { get; @@ -50,34 +142,46 @@ public class NetworkManager : Behaviour, IEnterUniverse, IExitUniverse, INetwork INetworkCommunicator? previousCommunicator = field; field = value; - if (previousCommunicator is not null) InvokeCommunicatorMethods(previousCommunicator, nameof(INetworkCommunicator.UnsubscribeFromPackets)); - if (field is not null) InvokeCommunicatorMethods(field, nameof(INetworkCommunicator.SubscribeToPackets)); + // Unsubscribe packet delegates from old communicator + if (previousCommunicator is not null) + InvokeCommunicatorMethods(nameof(INetworkCommunicator.UnsubscribeFromPackets), previousCommunicator); + + // Subscribe packet delegates to new communicator + if (field is not null) + InvokeCommunicatorMethods(nameof(INetworkCommunicator.SubscribeToPackets), field); } } = null!; - /// Used to delegate subscription and unsubscription methods on the to . - private void InvokeCommunicatorMethods(INetworkCommunicator networkCommunicator, string name) + /// + /// Dynamically invokes + /// or + /// on the provided for all known packet types. + /// + private void InvokeCommunicatorMethods(string methodName, INetworkCommunicator networkCommunicator) { MethodInfo unsubscribeFromPacketsMethod = typeof(INetworkCommunicator) - .GetMethod(name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)!; + .GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)!; - /// Get unique entries by the packetType so we don't get duplicate calls to - /// Because a class might have both and - /// or and together. - IEnumerable<(Type packetType, Delegate @delegate)> distinctRetrievalSubscriptionDelegates = - broadcastPacketRetrievalDelegates.Concat(entityPacketRetrievalDelegates).DistinctBy(pair => pair.packetType); - - foreach ((Type packetType, Delegate @delegate) in distinctRetrievalSubscriptionDelegates) + foreach ((Type packetType, Delegate @delegate) in uniqueRetrievalSubscriptionDelegates) { MethodInfo genericMethod = unsubscribeFromPacketsMethod.MakeGenericMethod(packetType); genericMethod.Invoke(networkCommunicator, [@delegate]); } } + #endregion + + //////////////////////////////////////////////////////////////// + #region Packet Routing/Broadcasting + + /// + /// Entry point for ALL incoming packets from the . + /// private void OnPacketReceived(IConnection sender, T entityDataPacket) { BroadcastPacket(sender, entityDataPacket); + if (entityDataPacket is IEntityNetworkPacket entityPacket) RoutePacket(sender, entityDataPacket, entityPacket); } @@ -85,25 +189,27 @@ public class NetworkManager : Behaviour, IEnterUniverse, IExitUniverse, INetwork private void RoutePacket(IConnection sender, T entityDataPacket, IEntityNetworkPacket entityPacket) { if (NetworkCommunicator is INetworkCommunicatorClient) - RoutePacket(clientEntityPacketRouters, entityPacket.EntityId, sender, entityDataPacket); + RoutePacket(clientEntityPacketRouterEvents, entityPacket.EntityId, sender, entityDataPacket); + if (NetworkCommunicator is INetworkCommunicatorServer) - RoutePacket(serverEntityPacketRouters, entityPacket.EntityId, sender, entityDataPacket); + RoutePacket(serverEntityPacketRouterEvents, entityPacket.EntityId, sender, entityDataPacket); } private void BroadcastPacket(IConnection sender, T entityDataPacket) { if (NetworkCommunicator is INetworkCommunicatorClient) - BroadcastPacket(clientBroadcastPacketRouters, sender, entityDataPacket); + BroadcastPacket(clientPacketBroadcastEvents, sender, entityDataPacket); + if (NetworkCommunicator is INetworkCommunicatorServer) - BroadcastPacket(serverBroadcastPacketRouters, sender, entityDataPacket); + BroadcastPacket(serverPacketBroadcastEvents, sender, entityDataPacket); } private void BroadcastPacket( - Dictionary> packetRouters, + Dictionary> packetBroadcasters, IConnection sender, T entityDataPacket) { - if (!packetRouters.TryGetValue(entityDataPacket!.GetType(), out Dictionary? routers)) + if (!packetBroadcasters.TryGetValue(entityDataPacket!.GetType(), out Dictionary? routers)) return; foreach ((string behaviourId, object routerEventReference) in routers) @@ -128,19 +234,25 @@ public class NetworkManager : Behaviour, IEnterUniverse, IExitUniverse, INetwork Event routerEvent = (Event)routerEventReference; routerEvent.Invoke(sender, entityDataPacket!); } + #endregion #region Packet Routers + + /// + /// Registers routing events for the behaviour based on cached packet listener methods. + /// private void RegisterPacketRoutersFor( INetworkEntity behaviour, Dictionary> packetRouters, - Dictionary>> packetArrivalMethods, - NetworkType networkType, Dictionary registerPacketListenersMethods) + Dictionary>> packetListenerMethods, + NetworkType networkType, + Dictionary registerPacketListenerListenersMethods) { - if (!packetArrivalMethods.TryGetValue(behaviour.GetType(), out Dictionary>? arrivalMethods)) + if (!packetListenerMethods.TryGetValue(behaviour.GetType(), out Dictionary>? listenerMethods)) return; - foreach (Type packetType in arrivalMethods.Keys) + foreach (Type packetType in listenerMethods.Keys) { if (!packetRouters.TryGetValue(packetType, out Dictionary? routers)) { @@ -148,14 +260,20 @@ public class NetworkManager : Behaviour, IEnterUniverse, IExitUniverse, INetwork packetRouters.Add(packetType, routers); } - object packetListenerEvent = - CreateEventAndRegister(packetType, behaviour, networkType, registerPacketListenersMethods); + object packetListenerEvent = CreateEventAndRegister(packetType, behaviour, networkType, registerPacketListenerListenersMethods); routers.Add(behaviour.Id, packetListenerEvent); } } - private object CreateEventAndRegister(Type packetType, INetworkEntity behaviour, NetworkType networkType, Dictionary registerPacketListenersMethods) + /// + /// Creates an Event and attaches listener callbacks. + /// + private object CreateEventAndRegister( + Type packetType, + INetworkEntity behaviour, + NetworkType networkType, + Dictionary registerPacketListenersMethods) { Type genericEventType = typeof(Event<,>).MakeGenericType(typeof(IConnection), packetType); object packetListenerEvent = Activator.CreateInstance(genericEventType)!; @@ -167,18 +285,31 @@ public class NetworkManager : Behaviour, IEnterUniverse, IExitUniverse, INetwork return packetListenerEvent; } - private static void RegisterPacketListenerEvent( + /// + /// Registers broadcast packet listeners on the behaviour. + /// + private static void RegisterBroadcastPacketListenerEvent( INetworkEntity behaviour, Event packetListenerEvent, NetworkType networkType) { switch (networkType) { - case NetworkType.Client: packetListenerEvent.AddListener((sender, packet) => ((IPacketListenerClient)behaviour).OnClientPacketArrived(sender, packet)); break; - case NetworkType.Server: packetListenerEvent.AddListener((sender, packet) => ((IPacketListenerServer)behaviour).OnServerPacketArrived(sender, packet)); break; + case NetworkType.Client: + packetListenerEvent.AddListener( + (sender, packet) => ((IPacketListenerClient)behaviour).OnClientPacketArrived(sender, packet)); + break; + + case NetworkType.Server: + packetListenerEvent.AddListener( + (sender, packet) => ((IPacketListenerServer)behaviour).OnServerPacketArrived(sender, packet)); + break; } } + /// + /// Registers entity-specific packet listeners on a network behaviour. + /// private static void RegisterEntityPacketListenerEvent( INetworkEntity behaviour, Event packetListenerEvent, @@ -187,20 +318,30 @@ public class NetworkManager : Behaviour, IEnterUniverse, IExitUniverse, INetwork { switch (networkType) { - case NetworkType.Client: packetListenerEvent.AddListener((sender, packet) => ((IPacketListenerClientEntity)behaviour).OnEntityClientPacketArrived(sender, packet)); break; - case NetworkType.Server: packetListenerEvent.AddListener((sender, packet) => ((IPacketListenerServerEntity)behaviour).OnEntityServerPacketArrived(sender, packet)); break; + case NetworkType.Client: + packetListenerEvent.AddListener( + (sender, packet) => ((IPacketListenerClientEntity)behaviour).OnEntityClientPacketArrived(sender, packet)); + break; + + case NetworkType.Server: + packetListenerEvent.AddListener( + (sender, packet) => ((IPacketListenerServerEntity)behaviour).OnEntityServerPacketArrived(sender, packet)); + break; } } + /// + /// Unregisters all routing events associated with the behaviour. + /// private void UnregisterPacketRoutersFor( INetworkEntity behaviour, Dictionary> packetRouters, - Dictionary>> packetArrivalMethods) + Dictionary>> packetListenerMethods) { - if (!packetArrivalMethods.TryGetValue(behaviour.GetType(), out Dictionary>? arrivalMethods)) + if (!packetListenerMethods.TryGetValue(behaviour.GetType(), out Dictionary>? listenerMethods)) return; - foreach ((Type packetType, List methods) in arrivalMethods) + foreach ((Type packetType, List methods) in listenerMethods) { if (!packetRouters.TryGetValue(packetType, out Dictionary? routers)) continue; @@ -215,6 +356,9 @@ public class NetworkManager : Behaviour, IEnterUniverse, IExitUniverse, INetwork } } + /// + /// Clears all listeners from a router event. + /// private static void ClearRouter(object routerEventReference) { Event routerEvent = (Event)routerEventReference; @@ -224,133 +368,192 @@ public class NetworkManager : Behaviour, IEnterUniverse, IExitUniverse, INetwork #endregion #region Engine Callbacks - private void OnCollected(IBehaviourCollector sender, IBehaviourCollector.BehaviourCollectedArguments args) + + /// + /// Called when an enters the universe. + /// Registers all packet routing for that entity. + /// + private void OnCollected( + IBehaviourCollector sender, + IBehaviourCollector.BehaviourCollectedArguments args) { INetworkEntity collectedBehaviour = args.BehaviourCollected; if (!_networkEntities.TryAdd(collectedBehaviour.Id, collectedBehaviour)) throw new($"Unable to add {collectedBehaviour.Id} to {nameof(NetworkManager)}"); - RegisterPacketRoutersFor(collectedBehaviour, clientBroadcastPacketRouters, clientBroadcastPacketArrivalMethods, NetworkType.Client, registerBroadcastPacketListenersMethods); - RegisterPacketRoutersFor(collectedBehaviour, clientEntityPacketRouters, clientEntityPacketArrivalMethods, NetworkType.Client, registerEntityPacketListenersMethods); + RegisterPacketRoutersFor(collectedBehaviour, clientPacketBroadcastEvents, clientBroadcastPacketListenerMethods, NetworkType.Client, registerBroadcastPacketListenersMethods); + RegisterPacketRoutersFor(collectedBehaviour, clientEntityPacketRouterEvents, clientEntityPacketListenerMethods, NetworkType.Client, registerEntityPacketListenersMethods); - RegisterPacketRoutersFor(collectedBehaviour, serverBroadcastPacketRouters, serverBroadcastPacketArrivalMethods, NetworkType.Server, registerBroadcastPacketListenersMethods); - RegisterPacketRoutersFor(collectedBehaviour, serverEntityPacketRouters, serverEntityPacketArrivalMethods, NetworkType.Server, registerEntityPacketListenersMethods); + RegisterPacketRoutersFor(collectedBehaviour, serverPacketBroadcastEvents, serverBroadcastPacketListenerMethods, NetworkType.Server, registerBroadcastPacketListenersMethods); + RegisterPacketRoutersFor(collectedBehaviour, serverEntityPacketRouterEvents, serverEntityPacketListenerMethods, NetworkType.Server, registerEntityPacketListenersMethods); } + /// + /// Called when an is removed from the universe. + /// Cleans up all routing. + /// private void OnRemoved(IBehaviourCollector sender, IBehaviourCollector.BehaviourRemovedArguments args) { INetworkEntity removedBehaviour = args.BehaviourRemoved; - if (!_networkEntities.Remove(args.BehaviourRemoved.Id)) + + if (!_networkEntities.Remove(removedBehaviour.Id)) return; - UnregisterPacketRoutersFor(removedBehaviour, clientBroadcastPacketRouters, clientBroadcastPacketArrivalMethods); - UnregisterPacketRoutersFor(removedBehaviour, clientEntityPacketRouters, clientEntityPacketArrivalMethods); + UnregisterPacketRoutersFor(removedBehaviour, clientPacketBroadcastEvents, clientBroadcastPacketListenerMethods); + UnregisterPacketRoutersFor(removedBehaviour, clientEntityPacketRouterEvents, clientEntityPacketListenerMethods); - UnregisterPacketRoutersFor(removedBehaviour, serverBroadcastPacketRouters, serverBroadcastPacketArrivalMethods); - UnregisterPacketRoutersFor(removedBehaviour, serverEntityPacketRouters, serverEntityPacketArrivalMethods); + UnregisterPacketRoutersFor(removedBehaviour, serverPacketBroadcastEvents, serverBroadcastPacketListenerMethods); + UnregisterPacketRoutersFor(removedBehaviour, serverEntityPacketRouterEvents, serverEntityPacketListenerMethods); } public void ExitUniverse(IUniverse universe) => _networkEntityCollector.Unassign(); + public void EnterUniverse(IUniverse universe) { _networkEntityCollector.Assign(universe); NetworkCommunicator = BehaviourController.GetRequiredBehaviourInParent(); } + #endregion #region Initialization + public NetworkManager() { - CachePacketRetrievalDelegates(typeof(INetworkPacket), broadcastPacketRetrievalDelegates); - CachePacketRetrievalDelegates(typeof(IEntityNetworkPacket), entityPacketRetrievalDelegates); + CacheRetrievalSubscriptionDelegates(); CacheRegistrationMethods(); - CachePacketArrivalMethods(); + CachePacketListenerMethods(); _networkEntityCollector.OnCollected.AddListener(OnCollected); _networkEntityCollector.OnRemoved.AddListener(OnRemoved); } - private void CachePacketRetrievalDelegates(Type packetType, List<(Type packetType, Delegate @delegate)> retrievalDelegates) + /// + /// Caches all retrieval subscription delegates for packets. + /// + private void CacheRetrievalSubscriptionDelegates() { - IEnumerable packetTypes = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()) - .Where(t => packetType.IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract && !t.IsGenericType); + CachePacketRetrievalDelegates(typeof(INetworkPacket), broadcastPacketRetrievalSubscriptionDelegates); + CachePacketRetrievalDelegates(typeof(IEntityNetworkPacket), entityPacketRetrievalSubscriptionDelegates); - MethodInfo onPacketArrivedMethod = GetType() - .GetMethod(nameof(OnPacketReceived), BindingFlags.NonPublic | BindingFlags.Instance)!; + uniqueRetrievalSubscriptionDelegates.AddRange(broadcastPacketRetrievalSubscriptionDelegates.Concat(entityPacketRetrievalSubscriptionDelegates).DistinctBy(pair => pair.PacketType)); + } - foreach (Type pType in packetTypes) + /// + /// Creates delegates for all concrete packet types that forward packets to . + /// + private void CachePacketRetrievalDelegates(Type packetType, List retrievalDelegates) + { + IEnumerable packetTypes = + AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(a => a.GetTypes()) + .Where(t => + packetType.IsAssignableFrom(t) && + !t.IsInterface && + !t.IsAbstract && + !t.IsGenericType + ); + + MethodInfo onPacketArrivedMethod = GetType().GetMethod(nameof(OnPacketReceived), BindingFlags.NonPublic | BindingFlags.Instance)!; + + foreach (Type type in packetTypes) { - MethodInfo genericOnPacketArrivedMethod = onPacketArrivedMethod.MakeGenericMethod(pType); + MethodInfo genericOnPacketArrivedMethod = onPacketArrivedMethod.MakeGenericMethod(type); + + Type genericDelegateType = typeof(Event<,>.EventHandler).MakeGenericType(typeof(IConnection), type); - Type genericDelegateType = typeof(Event<,>.EventHandler).MakeGenericType(typeof(IConnection), pType); Delegate genericPacketReceivedDelegate = Delegate.CreateDelegate(genericDelegateType, this, genericOnPacketArrivedMethod); - retrievalDelegates.Add((pType, genericPacketReceivedDelegate)); + + retrievalDelegates.Add((type, genericPacketReceivedDelegate)); } } + /// + /// Caches all registration and cleanup methods for packets. + /// private void CacheRegistrationMethods() { - CacheRegistrationMethods(registerBroadcastPacketListenersMethods, nameof(RegisterPacketListenerEvent), broadcastPacketRetrievalDelegates); - CacheRegistrationMethods(registerEntityPacketListenersMethods, nameof(RegisterEntityPacketListenerEvent), entityPacketRetrievalDelegates); - CacheRegistrationMethods(clearRoutesMethods, nameof(ClearRouter), broadcastPacketRetrievalDelegates); + CacheRegistrationMethods(registerBroadcastPacketListenersMethods, nameof(RegisterBroadcastPacketListenerEvent), broadcastPacketRetrievalSubscriptionDelegates); + CacheRegistrationMethods(registerEntityPacketListenersMethods, nameof(RegisterEntityPacketListenerEvent), entityPacketRetrievalSubscriptionDelegates); + CacheRegistrationMethods(clearRoutesMethods, nameof(ClearRouter), uniqueRetrievalSubscriptionDelegates); } - private void CacheRegistrationMethods(Dictionary registrationMethods, string methodName, List<(Type packetType, Delegate @delegate)> packetRetrievalDelegates) + /// + /// Creates generic method instances for each packet type listener to be registered into the . + /// + private void CacheRegistrationMethods( + Dictionary listenerRegistrationMethods, + string methodName, + List packetRetrievalDelegates) { MethodInfo registerPacketMethod = typeof(NetworkManager).GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static)!; + foreach ((Type packetType, Delegate @delegate) in packetRetrievalDelegates) { MethodInfo genericMethod = registerPacketMethod.MakeGenericMethod(packetType); - registrationMethods.TryAdd(packetType, genericMethod); + listenerRegistrationMethods.TryAdd(packetType, genericMethod); } } - private void CachePacketArrivalMethods() + /// + /// Caches packet listener methods for all packet listener interfaces. + /// + private void CachePacketListenerMethods() { - CachePacketArrivalMethods(clientBroadcastPacketArrivalMethods, typeof(IPacketListenerClient<>), nameof(IPacketListenerClient<>.OnClientPacketArrived)); - CachePacketArrivalMethods(serverBroadcastPacketArrivalMethods, typeof(IPacketListenerServer<>), nameof(IPacketListenerServer<>.OnServerPacketArrived)); - CachePacketArrivalMethods(clientEntityPacketArrivalMethods, typeof(IPacketListenerClientEntity<>), nameof(IPacketListenerClientEntity<>.OnEntityClientPacketArrived)); - CachePacketArrivalMethods(serverEntityPacketArrivalMethods, typeof(IPacketListenerServerEntity<>), nameof(IPacketListenerServerEntity<>.OnEntityServerPacketArrived)); + CachePacketListenerMethods(clientBroadcastPacketListenerMethods, typeof(IPacketListenerClient<>), nameof(IPacketListenerClient<>.OnClientPacketArrived)); + CachePacketListenerMethods(serverBroadcastPacketListenerMethods, typeof(IPacketListenerServer<>), nameof(IPacketListenerServer<>.OnServerPacketArrived)); + CachePacketListenerMethods(clientEntityPacketListenerMethods, typeof(IPacketListenerClientEntity<>), nameof(IPacketListenerClientEntity<>.OnEntityClientPacketArrived)); + CachePacketListenerMethods(serverEntityPacketListenerMethods, typeof(IPacketListenerServerEntity<>), nameof(IPacketListenerServerEntity<>.OnEntityServerPacketArrived)); } - private static void CachePacketArrivalMethods(Dictionary>> packetArrivalMethods, Type listenerType, string packetArrivalMethodName) + /// + /// Discovers all types implementing a given packet listener interface and caches their methods. + /// + private static void CachePacketListenerMethods( + Dictionary>> packetListenerMethods, + Type listenerType, + string packetListenerMethodName) { foreach (Type listenerClass in GetGenericsWith(listenerType)) { - Dictionary> packetRouters = []; - packetArrivalMethods.Add(listenerClass, packetRouters); + Dictionary> listenerMethodDictionary = []; + packetListenerMethods.Add(listenerClass, listenerMethodDictionary); foreach (Type packetListener in GetGenericInterfacesWith(listenerType, listenerClass)) { Type packetType = packetListener.GetGenericArguments().First(); - List arrivalMethods = packetListener - .GetMethods() - .Where(m => m.Name == packetArrivalMethodName) - .ToList(); + List listenerMethods = [.. packetListener.GetMethods().Where(m => m.Name == packetListenerMethodName)]; - packetRouters.Add(packetType, arrivalMethods); + listenerMethodDictionary.Add(packetType, listenerMethods); } } } + /// + /// Finds all types that implement a generic interface. + /// private static IEnumerable GetGenericsWith(Type type) => AppDomain.CurrentDomain .GetAssemblies() .SelectMany(a => a.GetTypes().Where( t => t.GetInterfaces().Any( - i => i.IsGenericType && i.GetGenericTypeDefinition() == type - ) - ) - ); + i => i.IsGenericType && i.GetGenericTypeDefinition() == type))); + // Gets all generic interfaces of a specific definition on a type private static IEnumerable GetGenericInterfacesWith(Type interfaceType, Type type) - => type.GetInterfaces().Where( - i => i.IsGenericType && i.GetGenericTypeDefinition() == interfaceType - ); + => type.GetInterfaces().Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == interfaceType); + #endregion + // Identifies whether packet routing is client-side or server-side private enum NetworkType { Client, Server } + + private readonly record struct PacketRetrievalDelegatePair(Type PacketType, Delegate Delegate) + { + public static implicit operator (Type packetType, Delegate @delegate)(PacketRetrievalDelegatePair value) => (value.PacketType, value.Delegate); + public static implicit operator PacketRetrievalDelegatePair((Type packetType, Delegate @delegate) value) => new(value.packetType, value.@delegate); + } } -- 2.49.1 From 499f87590335238a0c8b04a29ccd825dcfd55735 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Mon, 2 Feb 2026 14:42:34 +0300 Subject: [PATCH 27/51] chore: removed unused piece of code --- .../Engine.Integration.MonoGame/MonoGameTriangleBatch.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Engine.Integration/Engine.Integration.MonoGame/MonoGameTriangleBatch.cs b/Engine.Integration/Engine.Integration.MonoGame/MonoGameTriangleBatch.cs index 5e0a5a7..da575f6 100644 --- a/Engine.Integration/Engine.Integration.MonoGame/MonoGameTriangleBatch.cs +++ b/Engine.Integration/Engine.Integration.MonoGame/MonoGameTriangleBatch.cs @@ -48,8 +48,6 @@ public class MonoGameTriangleBatch : Behaviour, ITriangleBatch, IFirstFrameUpdat public void Begin(Matrix4x4? view = null, Matrix4x4? projection = null) { - Viewport viewport = graphicsDevice.Viewport; - this.view = (view ?? camera.ViewMatrix).Transposed.ToXnaMatrix(); this.projection = (projection ?? camera.ProjectionMatrix).Transposed.ToXnaMatrix(); } -- 2.49.1 From 32a7e9be24fb7106a6466b30ce72816fccdc2989 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Mon, 9 Feb 2026 13:14:45 +0300 Subject: [PATCH 28/51] fix: forgotten yaml converter for Matrix4x4 --- .../Primitives/Matrix4x4Converter.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Matrix4x4Converter.cs diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Matrix4x4Converter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Matrix4x4Converter.cs new file mode 100644 index 0000000..030e5ad --- /dev/null +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Matrix4x4Converter.cs @@ -0,0 +1,33 @@ +using System; + +using Engine.Core; + +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace Engine.Serializers.Yaml; + +public class Matrix4x4Converter : EngineTypeYamlSerializerBase +{ + private static readonly int SUBSTRING_START_LENGTH = nameof(Matrix4x4).Length + 1; + + public override Matrix4x4 Read(IParser parser, Type type, ObjectDeserializer rootDeserializer) + { + string value = parser.Consume().Value; + string insideParenthesis = value[SUBSTRING_START_LENGTH..^1]; + string[] values = insideParenthesis.Split(", "); + return new Matrix4x4( + float.Parse(values[0]), float.Parse(values[1]), float.Parse(values[2]), float.Parse(values[3]), + float.Parse(values[4]), float.Parse(values[5]), float.Parse(values[6]), float.Parse(values[7]), + float.Parse(values[8]), float.Parse(values[9]), float.Parse(values[10]), float.Parse(values[11]), + float.Parse(values[12]), float.Parse(values[13]), float.Parse(values[14]), float.Parse(values[15]) + ); + } + + public override void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) + { + Matrix4x4 m = (Matrix4x4)value!; + emitter.Emit(new Scalar($"{nameof(Matrix4x4)}({m.M11}, {m.M12}, {m.M13}, {m.M14},{m.M21}, {m.M22}, {m.M23}, {m.M24},{m.M31}, {m.M32}, {m.M33}, {m.M34},{m.M41}, {m.M42}, {m.M43}, {m.M44})")); + } +} -- 2.49.1 From 3b1c291588d12f926387425d1b82b46b43b73937 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Mon, 9 Feb 2026 13:15:13 +0300 Subject: [PATCH 29/51] feat: IConfiguration for system and other configurations --- Engine.Core/Config/BasicConfiguration.cs | 50 +++++++++++++++++++ Engine.Core/Config/ConfigurationExtensions.cs | 8 +++ Engine.Core/Config/IConfiguration.cs | 23 +++++++++ Engine.Core/Config/SystemConfiguration.cs | 11 ++++ 4 files changed, 92 insertions(+) create mode 100644 Engine.Core/Config/BasicConfiguration.cs create mode 100644 Engine.Core/Config/ConfigurationExtensions.cs create mode 100644 Engine.Core/Config/IConfiguration.cs create mode 100644 Engine.Core/Config/SystemConfiguration.cs diff --git a/Engine.Core/Config/BasicConfiguration.cs b/Engine.Core/Config/BasicConfiguration.cs new file mode 100644 index 0000000..9e8d2f1 --- /dev/null +++ b/Engine.Core/Config/BasicConfiguration.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; + +namespace Engine.Core.Config; + +public class BasicConfiguration : IConfiguration +{ + public Event OnAdded { get; } = new(); + public Event OnSet { get; } = new(); + public Event OnRemoved { get; } = new(); + + private readonly Dictionary values = []; + + public IReadOnlyDictionary Values => values; + + public T? Get(string key, T? defaultValue = default) + { + if (!values.TryGetValue(key, out object? value)) + return defaultValue; + + if (value is T castedObject) + return castedObject; + + try { return (T?)System.Convert.ChangeType(value, typeof(T)); } catch { } + + return defaultValue; + } + + public object? Get(string key) + { + values.TryGetValue(key, out object? value); + return value; + } + + public bool Has(string key) => values.ContainsKey(key); + + public void Remove(string key) + { + if (values.Remove(key)) + OnRemoved.Invoke(this, new(key)); + } + + public void Set(string key, T value) + { + if (!values.TryAdd(key, value)) + values[key] = value; + else + OnAdded.Invoke(this, new(key)); + OnSet.Invoke(this, new(key)); + } +} diff --git a/Engine.Core/Config/ConfigurationExtensions.cs b/Engine.Core/Config/ConfigurationExtensions.cs new file mode 100644 index 0000000..f83416f --- /dev/null +++ b/Engine.Core/Config/ConfigurationExtensions.cs @@ -0,0 +1,8 @@ +using Engine.Core.Exceptions; + +namespace Engine.Core.Config; + +public static class ConfigurationExtensions +{ + public static T GetRequired(this IConfiguration configuration, string key) => configuration.Get(key) ?? throw new NotFoundException($"Type of {typeof(T).FullName} with the key {key} was not present in the {configuration.GetType().FullName}"); +} diff --git a/Engine.Core/Config/IConfiguration.cs b/Engine.Core/Config/IConfiguration.cs new file mode 100644 index 0000000..0bd9dfe --- /dev/null +++ b/Engine.Core/Config/IConfiguration.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace Engine.Core.Config; + +public interface IConfiguration +{ + static IConfiguration System { get; set; } = new SystemConfiguration(); + static IConfiguration Shared { get; set; } = new BasicConfiguration(); + + Event OnAdded { get; } + Event OnSet { get; } + Event OnRemoved { get; } + + IReadOnlyDictionary Values { get; } + + bool Has(string key); + object? Get(string key); + T? Get(string key, T? defaultValue = default); + void Set(string key, T value); + void Remove(string key); + + readonly record struct ConfigUpdateArguments(string Key); +} diff --git a/Engine.Core/Config/SystemConfiguration.cs b/Engine.Core/Config/SystemConfiguration.cs new file mode 100644 index 0000000..895b4bf --- /dev/null +++ b/Engine.Core/Config/SystemConfiguration.cs @@ -0,0 +1,11 @@ +namespace Engine.Core.Config; + +public class SystemConfiguration : BasicConfiguration, IConfiguration +{ + public SystemConfiguration() + { + foreach (System.Collections.DictionaryEntry entry in System.Environment.GetEnvironmentVariables()) + if (entry is { Key: string key, Value: not null }) + Set(key, entry.Value); + } +} -- 2.49.1 From 45bd505da7a4880d95d860e1f823f81d49407708 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Mon, 9 Feb 2026 13:17:42 +0300 Subject: [PATCH 30/51] chore: renamed parameter names for ISerializer methods --- Engine.Core/Serialization/ISerializer.cs | 12 +++--- .../YamlConfiguration.cs | 39 +++++++++++++++++++ .../Engine.Integration.Yaml/YamlSerializer.cs | 28 ++++++------- 3 files changed, 59 insertions(+), 20 deletions(-) create mode 100644 Engine.Integration/Engine.Integration.Yaml/YamlConfiguration.cs diff --git a/Engine.Core/Serialization/ISerializer.cs b/Engine.Core/Serialization/ISerializer.cs index 0ada93c..783f2f7 100644 --- a/Engine.Core/Serialization/ISerializer.cs +++ b/Engine.Core/Serialization/ISerializer.cs @@ -4,15 +4,15 @@ namespace Engine.Core.Serialization; public interface ISerializer { - object Deserialize(string configuration); - object Deserialize(string configuration, Type type); - T Deserialize(string configuration); + object Deserialize(string content); + object Deserialize(string content, Type type); + T Deserialize(string content); string Serialize(object instance); - ProgressiveTask DeserializeAsync(string configuration); - ProgressiveTask DeserializeAsync(string configuration, Type type); - ProgressiveTask DeserializeAsync(string configuration); + ProgressiveTask DeserializeAsync(string content); + ProgressiveTask DeserializeAsync(string content, Type type); + ProgressiveTask DeserializeAsync(string content); ProgressiveTask SerializeAsync(object instance); } diff --git a/Engine.Integration/Engine.Integration.Yaml/YamlConfiguration.cs b/Engine.Integration/Engine.Integration.Yaml/YamlConfiguration.cs new file mode 100644 index 0000000..3c18410 --- /dev/null +++ b/Engine.Integration/Engine.Integration.Yaml/YamlConfiguration.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.IO; + +using Engine.Core.Config; + +namespace Engine.Serializers.Yaml; + +public class YamlConfiguration : BasicConfiguration +{ + public readonly string FilePath; + + private readonly YamlSerializer yamlSerializer = new(); + + public YamlConfiguration(string filePath) + { + if (!filePath.EndsWith(".yaml")) + filePath += ".yaml"; + + FilePath = filePath; + + bool isRelativePath = Path.GetFullPath(filePath).CompareTo(filePath) != 0; + + if (isRelativePath) + FilePath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, filePath)); + + if (Path.GetDirectoryName(FilePath) is string directoryPath) + Directory.CreateDirectory(directoryPath); + + if (!File.Exists(FilePath)) + return; + + string yamlFileText = File.ReadAllText(FilePath); + Dictionary valuePairs = yamlSerializer.Deserialize>(yamlFileText); + + foreach ((string key, string value) in valuePairs) + Set(key, value); + } +} diff --git a/Engine.Integration/Engine.Integration.Yaml/YamlSerializer.cs b/Engine.Integration/Engine.Integration.Yaml/YamlSerializer.cs index 6df1e67..3357040 100644 --- a/Engine.Integration/Engine.Integration.Yaml/YamlSerializer.cs +++ b/Engine.Integration/Engine.Integration.Yaml/YamlSerializer.cs @@ -62,65 +62,65 @@ public class YamlSerializer : Core.Serialization.ISerializer } } - public object Deserialize(string configuration) + public object Deserialize(string content) { lock (Lock) { identifiableRegistry.Reset(); - object result = deserializer.Deserialize(configuration)!; + object result = deserializer.Deserialize(content)!; identifiableRegistry.AssignAll(); return result; } } - public object Deserialize(string configuration, Type type) + public object Deserialize(string content, Type type) { lock (Lock) { identifiableRegistry.Reset(); - object result = deserializer.Deserialize(configuration, type)!; + object result = deserializer.Deserialize(content, type)!; identifiableRegistry.AssignAll(); return result; } } - public T Deserialize(string configuration) + public T Deserialize(string content) { lock (Lock) { identifiableRegistry.Reset(); - T result = deserializer.Deserialize(configuration); + T result = deserializer.Deserialize(content); identifiableRegistry.AssignAll(); return result; } } - public ProgressiveTask DeserializeAsync(string configuration) + public ProgressiveTask DeserializeAsync(string content) { lock (Lock) { progressionTracker.Reset(); - Task task = Task.Run(() => Deserialize(configuration)); + Task task = Task.Run(() => Deserialize(content)); return new ProgressiveTask(progressionTracker, task); } } - public ProgressiveTask DeserializeAsync(string configuration, Type type) + public ProgressiveTask DeserializeAsync(string content, Type type) { lock (Lock) { progressionTracker.Reset(); - Task task = Task.Run(() => Deserialize(configuration, type)); + Task task = Task.Run(() => Deserialize(content, type)); return new ProgressiveTask(progressionTracker, task); } } - public ProgressiveTask DeserializeAsync(string configuration) + public ProgressiveTask DeserializeAsync(string content) { lock (Lock) { progressionTracker.Reset(); - Task task = Task.Run(() => Deserialize(configuration)); + Task task = Task.Run(() => Deserialize(content)); return new ProgressiveTask(progressionTracker, task); } } @@ -135,8 +135,8 @@ public class YamlSerializer : Core.Serialization.ISerializer } } - internal object InternalDeserialize(string configuration, Type type) + internal object InternalDeserialize(string content, Type type) { - return deserializer.Deserialize(configuration, type)!; + return deserializer.Deserialize(content, type)!; } } -- 2.49.1 From d653774357a0693c42268e1ad50141635d04d689 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Mon, 9 Feb 2026 13:23:22 +0300 Subject: [PATCH 31/51] fix: forgotten Save method for YamlConfiguration --- .../Engine.Integration.Yaml/YamlConfiguration.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Engine.Integration/Engine.Integration.Yaml/YamlConfiguration.cs b/Engine.Integration/Engine.Integration.Yaml/YamlConfiguration.cs index 3c18410..57eef32 100644 --- a/Engine.Integration/Engine.Integration.Yaml/YamlConfiguration.cs +++ b/Engine.Integration/Engine.Integration.Yaml/YamlConfiguration.cs @@ -36,4 +36,9 @@ public class YamlConfiguration : BasicConfiguration foreach ((string key, string value) in valuePairs) Set(key, value); } + + public void Save() + { + File.WriteAllText(FilePath, yamlSerializer.Serialize(Values)); + } } -- 2.49.1 From aadc87d78a97bf9cdba51626f66311e1f8705899 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Fri, 20 Feb 2026 11:00:49 +0300 Subject: [PATCH 32/51] perf: memory allocation improvements on ITween.LoopIndefinitely method --- Engine.Systems/Tween/TweenExtensions.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Engine.Systems/Tween/TweenExtensions.cs b/Engine.Systems/Tween/TweenExtensions.cs index e0ffc09..8ff8cc3 100644 --- a/Engine.Systems/Tween/TweenExtensions.cs +++ b/Engine.Systems/Tween/TweenExtensions.cs @@ -23,16 +23,17 @@ public static class TweenExtensions public static ITween LoopInfinitely(this ITween tween) { - Tween tweenConcrete = (Tween)tween; - tweenConcrete.OnCompleted.AddListener(_ => - { - tweenConcrete.Reset(); - tweenConcrete.State = TweenState.Playing; - }); - + tween.OnCompleted.AddListener(repeaterDelegate); return tween; } + private static readonly Core.Event.EventHandler repeaterDelegate = sender => + { + Tween tweenConcrete = (Tween)sender; + tweenConcrete.Reset(); + tweenConcrete.State = TweenState.Playing; + }; + public static ITween Ease(this ITween tween, IEasing easing) { Tween tweenConcrete = (Tween)tween; -- 2.49.1 From 9f54f89f6d45d9b2cc2db00d909c38410c9f043d Mon Sep 17 00:00:00 2001 From: Syntriax Date: Fri, 20 Feb 2026 11:22:00 +0300 Subject: [PATCH 33/51] fix: ITween.OnEnded getting multiple calls and getting unnecessary calls on repeats fixed --- Engine.Systems/Tween/Tween.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Engine.Systems/Tween/Tween.cs b/Engine.Systems/Tween/Tween.cs index 0642edf..bea0c59 100644 --- a/Engine.Systems/Tween/Tween.cs +++ b/Engine.Systems/Tween/Tween.cs @@ -27,8 +27,16 @@ internal class Tween : ITween field = value; switch (value) { - case TweenState.Completed: OnCompleted?.Invoke(this); OnEnded?.Invoke(this); break; - case TweenState.Cancelled: OnCancelled?.Invoke(this); OnEnded?.Invoke(this); break; + case TweenState.Completed: + OnCompleted?.Invoke(this); + if (State == TweenState.Completed) + OnEnded?.Invoke(this); + break; + case TweenState.Cancelled: + OnCancelled?.Invoke(this); + if (State == TweenState.Cancelled) + OnEnded?.Invoke(this); + break; case TweenState.Paused: OnPaused?.Invoke(this); break; case TweenState.Playing: if (previousState == TweenState.Idle) -- 2.49.1 From 785fee9b6b45369097579d2ece44065c6ebb8828 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Fri, 20 Feb 2026 11:22:34 +0300 Subject: [PATCH 34/51] perf: memory allocation improvements on ITween.Loop method --- Engine.Systems/Tween/TweenExtensions.cs | 32 +++++++++++++++++-------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/Engine.Systems/Tween/TweenExtensions.cs b/Engine.Systems/Tween/TweenExtensions.cs index 8ff8cc3..b2f1b1a 100644 --- a/Engine.Systems/Tween/TweenExtensions.cs +++ b/Engine.Systems/Tween/TweenExtensions.cs @@ -4,23 +4,35 @@ namespace Engine.Systems.Tween; public static class TweenExtensions { + private static readonly System.Collections.Generic.Dictionary loopDictionary = []; + public static ITween Loop(this ITween tween, int count) { - Tween tweenConcrete = (Tween)tween; - int counter = count; + if (!loopDictionary.TryAdd(tween, count)) + throw new($"Tween already has a loop in progress."); - tweenConcrete.OnCompleted.AddListener(_ => - { - if (counter-- <= 0) - return; - - tweenConcrete.Reset(); - tweenConcrete.State = TweenState.Playing; - }); + tween.OnCompleted.AddListener(looperDelegate); + tween.OnEnded.AddListener(looperEndDelegate); return tween; } + private static readonly Core.Event.EventHandler looperEndDelegate = sender => loopDictionary.Remove(sender); + private static readonly Core.Event.EventHandler looperDelegate = sender => + { + int counter = loopDictionary[sender] = loopDictionary[sender] - 1; + + if (counter <= 0) + { + loopDictionary.Remove(sender); + return; + } + + Tween tweenConcrete = (Tween)sender; + tweenConcrete.Reset(); + tweenConcrete.State = TweenState.Playing; + }; + public static ITween LoopInfinitely(this ITween tween) { tween.OnCompleted.AddListener(repeaterDelegate); -- 2.49.1 From 1d3dd8b04639b59b153fd262f76194dc68d46bc1 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Tue, 3 Mar 2026 23:14:37 +0300 Subject: [PATCH 35/51] chore: fixed IIdentifiable file name and class name being different --- Engine.Core/Abstract/{IHasId.cs => IIdentifiable.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Engine.Core/Abstract/{IHasId.cs => IIdentifiable.cs} (100%) diff --git a/Engine.Core/Abstract/IHasId.cs b/Engine.Core/Abstract/IIdentifiable.cs similarity index 100% rename from Engine.Core/Abstract/IHasId.cs rename to Engine.Core/Abstract/IIdentifiable.cs -- 2.49.1 From fa514531bf75e52a66638dda2964d6c1cef6207d Mon Sep 17 00:00:00 2001 From: Syntriax Date: Tue, 3 Mar 2026 23:30:56 +0300 Subject: [PATCH 36/51] feat: added IIdentifiable comparison interface & IsIdentical extension method --- Engine.Core/Abstract/IIdentifiable.cs | 2 +- Engine.Core/BaseEntity.cs | 2 ++ Engine.Core/Extensions/IdentifiableExtensions.cs | 7 +++++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 Engine.Core/Extensions/IdentifiableExtensions.cs diff --git a/Engine.Core/Abstract/IIdentifiable.cs b/Engine.Core/Abstract/IIdentifiable.cs index c4cd50a..403cace 100644 --- a/Engine.Core/Abstract/IIdentifiable.cs +++ b/Engine.Core/Abstract/IIdentifiable.cs @@ -3,7 +3,7 @@ namespace Engine.Core; /// /// Represents any instance in the engine with an id. /// -public interface IIdentifiable +public interface IIdentifiable : System.IComparable { /// /// Event triggered when the of the changes. diff --git a/Engine.Core/BaseEntity.cs b/Engine.Core/BaseEntity.cs index e259b21..87982f2 100644 --- a/Engine.Core/BaseEntity.cs +++ b/Engine.Core/BaseEntity.cs @@ -101,4 +101,6 @@ public abstract class BaseEntity : IEntity protected BaseEntity() => Id = Guid.NewGuid().ToString("D"); protected BaseEntity(string id) => Id = id; + + public int CompareTo(IIdentifiable? other) => Id.CompareTo(other?.Id); } diff --git a/Engine.Core/Extensions/IdentifiableExtensions.cs b/Engine.Core/Extensions/IdentifiableExtensions.cs new file mode 100644 index 0000000..35d7d02 --- /dev/null +++ b/Engine.Core/Extensions/IdentifiableExtensions.cs @@ -0,0 +1,7 @@ +namespace Engine.Core; + +public static class IdentifiableExtensions +{ + public static bool IsIdentical(this IIdentifiable? left, IIdentifiable? right) + => left?.CompareTo(right) == 0; +} -- 2.49.1 From 4326d5615e94b208729aab1c91c783110e61cef5 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Wed, 4 Mar 2026 12:32:58 +0300 Subject: [PATCH 37/51] feat: INetworkCommunicatorServer.SendToClients extension method added --- .../Network/Extensions/CommunicatorExtensions.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 Engine.Systems/Network/Extensions/CommunicatorExtensions.cs diff --git a/Engine.Systems/Network/Extensions/CommunicatorExtensions.cs b/Engine.Systems/Network/Extensions/CommunicatorExtensions.cs new file mode 100644 index 0000000..0d68c56 --- /dev/null +++ b/Engine.Systems/Network/Extensions/CommunicatorExtensions.cs @@ -0,0 +1,13 @@ + +using System.Collections.Generic; + +namespace Engine.Systems.Network; + +public static class CommunicatorExtensions +{ + public static void SendToClients(this INetworkCommunicatorServer server, IEnumerable connections, T packet, PacketDelivery packetDelivery = PacketDelivery.ReliableInOrder) where T : class, new() + { + foreach (IConnection connection in connections) + server.SendToClient(connection, packet, packetDelivery); + } +} -- 2.49.1 From e84c6edce152a05c903286c0bfbaa0c2e187602d Mon Sep 17 00:00:00 2001 From: Syntriax Date: Wed, 4 Mar 2026 20:16:07 +0300 Subject: [PATCH 38/51] refactor: removed the IComparable from IIdentifiable and implemented in extension method --- Engine.Core/Abstract/IIdentifiable.cs | 2 +- Engine.Core/BaseEntity.cs | 2 -- Engine.Core/Extensions/IdentifiableExtensions.cs | 7 ++++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Engine.Core/Abstract/IIdentifiable.cs b/Engine.Core/Abstract/IIdentifiable.cs index 403cace..c4cd50a 100644 --- a/Engine.Core/Abstract/IIdentifiable.cs +++ b/Engine.Core/Abstract/IIdentifiable.cs @@ -3,7 +3,7 @@ namespace Engine.Core; /// /// Represents any instance in the engine with an id. /// -public interface IIdentifiable : System.IComparable +public interface IIdentifiable { /// /// Event triggered when the of the changes. diff --git a/Engine.Core/BaseEntity.cs b/Engine.Core/BaseEntity.cs index 87982f2..e259b21 100644 --- a/Engine.Core/BaseEntity.cs +++ b/Engine.Core/BaseEntity.cs @@ -101,6 +101,4 @@ public abstract class BaseEntity : IEntity protected BaseEntity() => Id = Guid.NewGuid().ToString("D"); protected BaseEntity(string id) => Id = id; - - public int CompareTo(IIdentifiable? other) => Id.CompareTo(other?.Id); } diff --git a/Engine.Core/Extensions/IdentifiableExtensions.cs b/Engine.Core/Extensions/IdentifiableExtensions.cs index 35d7d02..0bb8f74 100644 --- a/Engine.Core/Extensions/IdentifiableExtensions.cs +++ b/Engine.Core/Extensions/IdentifiableExtensions.cs @@ -3,5 +3,10 @@ namespace Engine.Core; public static class IdentifiableExtensions { public static bool IsIdentical(this IIdentifiable? left, IIdentifiable? right) - => left?.CompareTo(right) == 0; + { + if (left == null || right == null) + return false; + + return left?.Id?.CompareTo(right?.Id) == 0; + } } -- 2.49.1 From 7ae8b4feb0a11cfdc312bfb13188c827fb6212bc Mon Sep 17 00:00:00 2001 From: Syntriax Date: Thu, 5 Mar 2026 23:48:57 +0300 Subject: [PATCH 39/51] feat: added NetworkBehaviour for oth client & server communication --- Engine.Systems/Network/NetworkBehaviour.cs | 40 ++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 Engine.Systems/Network/NetworkBehaviour.cs diff --git a/Engine.Systems/Network/NetworkBehaviour.cs b/Engine.Systems/Network/NetworkBehaviour.cs new file mode 100644 index 0000000..21d6683 --- /dev/null +++ b/Engine.Systems/Network/NetworkBehaviour.cs @@ -0,0 +1,40 @@ +using Engine.Core; + +namespace Engine.Systems.Network; + +/// +/// Basic network behaviour that supports both client & server behaviour. Finds both +/// the and the +/// in the universe in it's first active frame. Recommended to use or +///
+/// Disclaimer: It implements and in virtual methods. +///
+public class NetworkBehaviour : Behaviour, IFirstFrameUpdate, ILastFrameUpdate +{ + protected INetworkCommunicatorServer? server = null!; + protected INetworkCommunicatorClient? client = null!; + + protected bool IsServer { get; private set; } = false; + protected bool IsClient { get; private set; } = false; + + public INetworkCommunicatorServer Server => server ?? throw new Core.Exceptions.NotFoundException($"Universe does not contain a {nameof(INetworkCommunicatorServer)}"); + public INetworkCommunicatorClient Client => client ?? throw new Core.Exceptions.NotFoundException($"Universe does not contain a {nameof(INetworkCommunicatorClient)}"); + + public virtual void FirstActiveFrame() + { + client = Universe.FindBehaviour(); + server = Universe.FindBehaviour(); + + IsServer = server is not null; + IsClient = client is not null; + } + + public virtual void LastActiveFrame() + { + client = null!; + server = null!; + + IsServer = false; + IsClient = false; + } +} -- 2.49.1 From 6ca3f22b17bcafe65d26ca5b47486f00b5e76468 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Fri, 6 Mar 2026 11:39:46 +0300 Subject: [PATCH 40/51] feat: added UpdateManager.CallFirstActiveFrameImmediately method for early calls of behaviours --- Engine.Core/Systems/UpdateManager.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Engine.Core/Systems/UpdateManager.cs b/Engine.Core/Systems/UpdateManager.cs index 6af1be4..f07ee7b 100644 --- a/Engine.Core/Systems/UpdateManager.cs +++ b/Engine.Core/Systems/UpdateManager.cs @@ -44,6 +44,17 @@ public class UpdateManager : Behaviour, IEnterUniverse, IExitUniverse universe.OnPostUpdate.RemoveListener(OnPostUpdate); } + /// + /// Call the early if it's in queue to be called by this the . + /// It will not be called in the next natural cycle. + /// + /// The instance that will be called now rather than later. + public void CallFirstActiveFrameImmediately(IFirstFrameUpdate instance) + { + if (toCallFirstFrameUpdates.Remove(instance)) + instance.FirstActiveFrame(); + } + private void OnFirstUpdate(IUniverse sender, IUniverse.UpdateArguments args) { for (int i = toCallFirstFrameUpdates.Count - 1; i >= 0; i--) -- 2.49.1 From 35c7eb9578ca0f2a36d9959096fefe9a2fbc6ac4 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Fri, 6 Mar 2026 11:55:10 +0300 Subject: [PATCH 41/51] refactor: renamed NetworkBehaviour to CommonNetworkBehaviour --- .../Network/{NetworkBehaviour.cs => CommonNetworkBehaviour.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename Engine.Systems/Network/{NetworkBehaviour.cs => CommonNetworkBehaviour.cs} (94%) diff --git a/Engine.Systems/Network/NetworkBehaviour.cs b/Engine.Systems/Network/CommonNetworkBehaviour.cs similarity index 94% rename from Engine.Systems/Network/NetworkBehaviour.cs rename to Engine.Systems/Network/CommonNetworkBehaviour.cs index 21d6683..7b7043b 100644 --- a/Engine.Systems/Network/NetworkBehaviour.cs +++ b/Engine.Systems/Network/CommonNetworkBehaviour.cs @@ -9,7 +9,7 @@ namespace Engine.Systems.Network; ///
/// Disclaimer: It implements and in virtual methods. ///
-public class NetworkBehaviour : Behaviour, IFirstFrameUpdate, ILastFrameUpdate +public class CommonNetworkBehaviour : Behaviour, IFirstFrameUpdate, ILastFrameUpdate { protected INetworkCommunicatorServer? server = null!; protected INetworkCommunicatorClient? client = null!; -- 2.49.1 From 1418927c32a20e988c545309f3629238ea0e48c8 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Sun, 8 Mar 2026 12:53:02 +0300 Subject: [PATCH 42/51] refactor: renamed WaitForSeconds to a more general WaitForTime class --- Engine.Core/Systems/Yields/WaitForSecondsYield.cs | 10 ---------- Engine.Core/Systems/Yields/WaitForTimeYield.cs | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 10 deletions(-) delete mode 100644 Engine.Core/Systems/Yields/WaitForSecondsYield.cs create mode 100644 Engine.Core/Systems/Yields/WaitForTimeYield.cs diff --git a/Engine.Core/Systems/Yields/WaitForSecondsYield.cs b/Engine.Core/Systems/Yields/WaitForSecondsYield.cs deleted file mode 100644 index da82083..0000000 --- a/Engine.Core/Systems/Yields/WaitForSecondsYield.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Engine.Core; - -public class WaitForSecondsYield(float seconds) : ICoroutineYield -{ - private readonly DateTime triggerTime = DateTime.UtcNow.AddSeconds(seconds); - - public bool Yield() => DateTime.UtcNow < triggerTime; -} diff --git a/Engine.Core/Systems/Yields/WaitForTimeYield.cs b/Engine.Core/Systems/Yields/WaitForTimeYield.cs new file mode 100644 index 0000000..6d213ef --- /dev/null +++ b/Engine.Core/Systems/Yields/WaitForTimeYield.cs @@ -0,0 +1,14 @@ +using System; + +namespace Engine.Core; + +public class WaitForTimeYield(float seconds = 0f, float milliseconds = 0f, float minutes = 0f, float hours = 0f) : ICoroutineYield +{ + private readonly DateTime triggerTime = DateTime.UtcNow + .AddHours(hours) + .AddMinutes(minutes) + .AddSeconds(seconds) + .AddMilliseconds(milliseconds); + + public bool Yield() => DateTime.UtcNow < triggerTime; +} -- 2.49.1 From 51534606c83a04c926ce6f391cef80c8541bb614 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Sun, 8 Mar 2026 12:54:02 +0300 Subject: [PATCH 43/51] feat: added WaitForTask yield --- .../Systems/Yields/WaitForTaskYield.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 Engine.Core/Systems/Yields/WaitForTaskYield.cs diff --git a/Engine.Core/Systems/Yields/WaitForTaskYield.cs b/Engine.Core/Systems/Yields/WaitForTaskYield.cs new file mode 100644 index 0000000..bc39cba --- /dev/null +++ b/Engine.Core/Systems/Yields/WaitForTaskYield.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; + +using static Engine.Core.WaitForTaskYield; + +namespace Engine.Core; + +public class WaitForTaskYield(Task task, TaskCompletionStatus completionStatus = TaskCompletionStatus.Either) : ICoroutineYield +{ + public bool Yield() + { + switch (completionStatus) + { + case TaskCompletionStatus.Successful: + if (task.IsCanceled) + throw new("Task has been canceled."); + if (task.IsFaulted) + throw new("Task has faulted."); + return task.IsCompletedSuccessfully; + + case TaskCompletionStatus.Failed: + if (task.IsCompletedSuccessfully) + throw new("Task was completed successfully."); + return task.IsFaulted; + } + + return task.IsCompleted; + } + + public enum TaskCompletionStatus + { + Either, + Successful, + Failed + } +} -- 2.49.1 From 105b87da3ab50e4a8b72bafbf451e486afe58cb9 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Sun, 8 Mar 2026 15:12:16 +0300 Subject: [PATCH 44/51] fix: file & type name mismatches fixed --- Engine.Core/Debug/{AssertHelpers.cs => Assert.cs} | 0 .../{EngineConverter.cs => EngineConverterExtensions.cs} | 0 .../Converters/BehaviourControllerConverter.cs | 2 +- .../Engine.Integration.Yaml/Converters/BehaviourConverter.cs | 2 +- .../Converters/EngineTypeYamlConverterBase.cs | 2 +- .../Converters/Primitives/AABB2DConverter.cs | 2 +- .../Converters/Primitives/AABB3DConverter.cs | 2 +- .../Converters/Primitives/CircleConverter.cs | 2 +- .../Converters/Primitives/ColorHSVAConverter.cs | 2 +- .../Converters/Primitives/ColorHSVConverter.cs | 2 +- .../Converters/Primitives/ColorRGBAConverter.cs | 2 +- .../Converters/Primitives/ColorRGBConverter.cs | 2 +- .../Converters/Primitives/Line2DConverter.cs | 2 +- .../Converters/Primitives/Line2DEquationConverter.cs | 2 +- .../Converters/Primitives/Line3DConverter.cs | 2 +- .../Converters/Primitives/Matrix4x4Converter.cs | 2 +- .../Converters/Primitives/Projection1DConverter.cs | 2 +- .../Converters/Primitives/QuaternionConverter.cs | 2 +- .../Converters/Primitives/Ray2DConverter.cs | 2 +- .../Converters/Primitives/Ray3DConverter.cs | 2 +- .../Converters/Primitives/Shape2DConverter.cs | 2 +- .../Converters/Primitives/Sphere3DConverter.cs | 2 +- .../Converters/Primitives/TriangleConverter.cs | 2 +- .../Converters/Primitives/Vector2DConverter.cs | 2 +- .../Converters/Primitives/Vector2DIntConverter.cs | 2 +- .../Converters/Primitives/Vector3DConverter.cs | 2 +- .../Converters/Primitives/Vector3DIntConverter.cs | 2 +- .../Converters/Primitives/Vector4DConverter.cs | 2 +- .../Converters/SerializedClassConverter.cs | 2 +- .../Engine.Integration.Yaml/Converters/StateEnableConverter.cs | 2 +- .../Converters/TypeContainerConverter.cs | 2 +- .../Engine.Integration.Yaml/Converters/UniverseConverter.cs | 2 +- .../Converters/UniverseObjectConverter.cs | 2 +- Engine.Systems/Tween/Yields/WaitForTweenDoneCoroutineYield.cs | 2 +- 34 files changed, 32 insertions(+), 32 deletions(-) rename Engine.Core/Debug/{AssertHelpers.cs => Assert.cs} (100%) rename Engine.Integration/Engine.Integration.MonoGame/{EngineConverter.cs => EngineConverterExtensions.cs} (100%) diff --git a/Engine.Core/Debug/AssertHelpers.cs b/Engine.Core/Debug/Assert.cs similarity index 100% rename from Engine.Core/Debug/AssertHelpers.cs rename to Engine.Core/Debug/Assert.cs diff --git a/Engine.Integration/Engine.Integration.MonoGame/EngineConverter.cs b/Engine.Integration/Engine.Integration.MonoGame/EngineConverterExtensions.cs similarity index 100% rename from Engine.Integration/Engine.Integration.MonoGame/EngineConverter.cs rename to Engine.Integration/Engine.Integration.MonoGame/EngineConverterExtensions.cs diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/BehaviourControllerConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/BehaviourControllerConverter.cs index 530ae2c..a0f6941 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/BehaviourControllerConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/BehaviourControllerConverter.cs @@ -11,7 +11,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class BehaviourControllerConverter : EngineTypeYamlSerializerBase +public class BehaviourControllerConverter : EngineTypeYamlConverterBase { private const string BEHAVIOURS_SCALAR_NAME = "Behaviours"; diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/BehaviourConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/BehaviourConverter.cs index a475f96..99bbff4 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/BehaviourConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/BehaviourConverter.cs @@ -9,7 +9,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class BehaviourConverter : EngineTypeYamlSerializerBase +public class BehaviourConverter : EngineTypeYamlConverterBase { public override IBehaviour? Read(IParser parser, Type type, ObjectDeserializer rootDeserializer) { diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/EngineTypeYamlConverterBase.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/EngineTypeYamlConverterBase.cs index 99167ee..e074ccb 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/EngineTypeYamlConverterBase.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/EngineTypeYamlConverterBase.cs @@ -8,7 +8,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public abstract class EngineTypeYamlSerializerBase : IEngineTypeYamlConverter +public abstract class EngineTypeYamlConverterBase : IEngineTypeYamlConverter { protected const string SERIALIZED_SCALAR_NAME = "Properties"; diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/AABB2DConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/AABB2DConverter.cs index 1c2a967..3f52477 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/AABB2DConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/AABB2DConverter.cs @@ -8,7 +8,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class AABB2DConverter : EngineTypeYamlSerializerBase +public class AABB2DConverter : EngineTypeYamlConverterBase { public override AABB2D Read(IParser parser, Type type, ObjectDeserializer rootDeserializer) { diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/AABB3DConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/AABB3DConverter.cs index 83558e8..a50f8b8 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/AABB3DConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/AABB3DConverter.cs @@ -8,7 +8,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class AABB3DConverter : EngineTypeYamlSerializerBase +public class AABB3DConverter : EngineTypeYamlConverterBase { public override AABB3D Read(IParser parser, Type type, ObjectDeserializer rootDeserializer) { diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/CircleConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/CircleConverter.cs index d06bc53..494c960 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/CircleConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/CircleConverter.cs @@ -8,7 +8,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class CircleConverter : EngineTypeYamlSerializerBase +public class CircleConverter : EngineTypeYamlConverterBase { public override Circle Read(IParser parser, Type type, ObjectDeserializer rootDeserializer) { diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/ColorHSVAConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/ColorHSVAConverter.cs index 42aff81..df3bcba 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/ColorHSVAConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/ColorHSVAConverter.cs @@ -8,7 +8,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class ColorHSVAConverter : EngineTypeYamlSerializerBase +public class ColorHSVAConverter : EngineTypeYamlConverterBase { private static readonly int SUBSTRING_START_LENGTH = nameof(ColorHSVA).Length + 1; diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/ColorHSVConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/ColorHSVConverter.cs index a03f490..ec81ae5 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/ColorHSVConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/ColorHSVConverter.cs @@ -8,7 +8,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class ColorHSVConverter : EngineTypeYamlSerializerBase +public class ColorHSVConverter : EngineTypeYamlConverterBase { private static readonly int SUBSTRING_START_LENGTH = nameof(ColorHSV).Length + 1; diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/ColorRGBAConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/ColorRGBAConverter.cs index 50ec7aa..d1e3a73 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/ColorRGBAConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/ColorRGBAConverter.cs @@ -8,7 +8,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class ColorRGBAConverter : EngineTypeYamlSerializerBase +public class ColorRGBAConverter : EngineTypeYamlConverterBase { private static readonly int SUBSTRING_START_LENGTH = nameof(ColorRGBA).Length + 1; diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/ColorRGBConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/ColorRGBConverter.cs index 8133885..f769547 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/ColorRGBConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/ColorRGBConverter.cs @@ -8,7 +8,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class ColorRGBConverter : EngineTypeYamlSerializerBase +public class ColorRGBConverter : EngineTypeYamlConverterBase { private static readonly int SUBSTRING_START_LENGTH = nameof(ColorRGB).Length + 1; diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Line2DConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Line2DConverter.cs index 1d45331..8901d1b 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Line2DConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Line2DConverter.cs @@ -8,7 +8,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class Line2DConverter : EngineTypeYamlSerializerBase +public class Line2DConverter : EngineTypeYamlConverterBase { public override Line2D Read(IParser parser, Type type, ObjectDeserializer rootDeserializer) { diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Line2DEquationConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Line2DEquationConverter.cs index ed13cc4..113f6dc 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Line2DEquationConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Line2DEquationConverter.cs @@ -8,7 +8,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class Line2DEquationConverter : EngineTypeYamlSerializerBase +public class Line2DEquationConverter : EngineTypeYamlConverterBase { public override Line2DEquation Read(IParser parser, Type type, ObjectDeserializer rootDeserializer) { diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Line3DConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Line3DConverter.cs index a49fd16..d11f050 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Line3DConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Line3DConverter.cs @@ -8,7 +8,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class Line3DConverter : EngineTypeYamlSerializerBase +public class Line3DConverter : EngineTypeYamlConverterBase { public override Line3D Read(IParser parser, Type type, ObjectDeserializer rootDeserializer) { diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Matrix4x4Converter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Matrix4x4Converter.cs index 030e5ad..0071d67 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Matrix4x4Converter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Matrix4x4Converter.cs @@ -8,7 +8,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class Matrix4x4Converter : EngineTypeYamlSerializerBase +public class Matrix4x4Converter : EngineTypeYamlConverterBase { private static readonly int SUBSTRING_START_LENGTH = nameof(Matrix4x4).Length + 1; diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Projection1DConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Projection1DConverter.cs index b1ea9fe..0ba7ef1 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Projection1DConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Projection1DConverter.cs @@ -8,7 +8,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class Projection1DConverter : EngineTypeYamlSerializerBase +public class Projection1DConverter : EngineTypeYamlConverterBase { public override Projection1D Read(IParser parser, Type type, ObjectDeserializer rootDeserializer) { diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/QuaternionConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/QuaternionConverter.cs index 76317b3..a979536 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/QuaternionConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/QuaternionConverter.cs @@ -8,7 +8,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class QuaternionConverter : EngineTypeYamlSerializerBase +public class QuaternionConverter : EngineTypeYamlConverterBase { private static readonly int SUBSTRING_START_LENGTH = nameof(Quaternion).Length + 1; diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Ray2DConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Ray2DConverter.cs index 300b00d..f3c8c9b 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Ray2DConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Ray2DConverter.cs @@ -8,7 +8,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class Ray2DConverter : EngineTypeYamlSerializerBase +public class Ray2DConverter : EngineTypeYamlConverterBase { public override Ray2D Read(IParser parser, Type type, ObjectDeserializer rootDeserializer) { diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Ray3DConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Ray3DConverter.cs index 0120690..f9295a5 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Ray3DConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Ray3DConverter.cs @@ -8,7 +8,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class Ray3DConverter : EngineTypeYamlSerializerBase +public class Ray3DConverter : EngineTypeYamlConverterBase { public override Ray3D Read(IParser parser, Type type, ObjectDeserializer rootDeserializer) { diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Shape2DConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Shape2DConverter.cs index cd88d96..e3f6b60 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Shape2DConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Shape2DConverter.cs @@ -9,7 +9,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class Shape2DConverter : EngineTypeYamlSerializerBase +public class Shape2DConverter : EngineTypeYamlConverterBase { public override Shape2D Read(IParser parser, Type type, ObjectDeserializer rootDeserializer) { diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Sphere3DConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Sphere3DConverter.cs index 1fc0e71..531bb02 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Sphere3DConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Sphere3DConverter.cs @@ -8,7 +8,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class Sphere3DConverter : EngineTypeYamlSerializerBase +public class Sphere3DConverter : EngineTypeYamlConverterBase { public override Sphere3D Read(IParser parser, Type type, ObjectDeserializer rootDeserializer) { diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/TriangleConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/TriangleConverter.cs index c544b37..a999ca7 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/TriangleConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/TriangleConverter.cs @@ -8,7 +8,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class TriangleConverter : EngineTypeYamlSerializerBase +public class TriangleConverter : EngineTypeYamlConverterBase { public override Triangle Read(IParser parser, Type type, ObjectDeserializer rootDeserializer) { diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Vector2DConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Vector2DConverter.cs index 654416c..1e1c62b 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Vector2DConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Vector2DConverter.cs @@ -8,7 +8,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class Vector2DConverter : EngineTypeYamlSerializerBase +public class Vector2DConverter : EngineTypeYamlConverterBase { private static readonly int SUBSTRING_START_LENGTH = nameof(Vector2D).Length + 1; diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Vector2DIntConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Vector2DIntConverter.cs index 8b0eea3..ba70c11 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Vector2DIntConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Vector2DIntConverter.cs @@ -8,7 +8,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class Vector2DIntConverter : EngineTypeYamlSerializerBase +public class Vector2DIntConverter : EngineTypeYamlConverterBase { private static readonly int SUBSTRING_START_LENGTH = nameof(Vector2DInt).Length + 1; diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Vector3DConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Vector3DConverter.cs index 4bdca07..9f0a17c 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Vector3DConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Vector3DConverter.cs @@ -8,7 +8,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class Vector3DConverter : EngineTypeYamlSerializerBase +public class Vector3DConverter : EngineTypeYamlConverterBase { private static readonly int SUBSTRING_START_LENGTH = nameof(Vector3D).Length + 1; diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Vector3DIntConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Vector3DIntConverter.cs index d1adf01..3efae7f 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Vector3DIntConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Vector3DIntConverter.cs @@ -8,7 +8,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class Vector3DIntConverter : EngineTypeYamlSerializerBase +public class Vector3DIntConverter : EngineTypeYamlConverterBase { private static readonly int SUBSTRING_START_LENGTH = nameof(Vector3DInt).Length + 1; diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Vector4DConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Vector4DConverter.cs index 5249c10..8a4bf1d 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Vector4DConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/Primitives/Vector4DConverter.cs @@ -8,7 +8,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class Vector4DConverter : EngineTypeYamlSerializerBase +public class Vector4DConverter : EngineTypeYamlConverterBase { private static readonly int SUBSTRING_START_LENGTH = nameof(Vector4D).Length + 1; diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/SerializedClassConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/SerializedClassConverter.cs index 525de45..5a970a2 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/SerializedClassConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/SerializedClassConverter.cs @@ -12,7 +12,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class SerializedClassConverter : EngineTypeYamlSerializerBase +public class SerializedClassConverter : EngineTypeYamlConverterBase { public override SerializedClass? Read(IParser parser, Type type, ObjectDeserializer rootDeserializer) { diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/StateEnableConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/StateEnableConverter.cs index d5c0bc0..3847ca7 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/StateEnableConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/StateEnableConverter.cs @@ -9,7 +9,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class StateEnableConverter : EngineTypeYamlSerializerBase +public class StateEnableConverter : EngineTypeYamlConverterBase { public override IStateEnable? Read(IParser parser, Type type, ObjectDeserializer rootDeserializer) { diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/TypeContainerConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/TypeContainerConverter.cs index 94f045c..eaf591c 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/TypeContainerConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/TypeContainerConverter.cs @@ -9,7 +9,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class TypeContainerConverter : EngineTypeYamlSerializerBase +public class TypeContainerConverter : EngineTypeYamlConverterBase { public override TypeContainer Read(IParser parser, Type type, ObjectDeserializer rootDeserializer) { diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/UniverseConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/UniverseConverter.cs index 4a21d0e..1a1a291 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/UniverseConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/UniverseConverter.cs @@ -11,7 +11,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class UniverseConverter : EngineTypeYamlSerializerBase +public class UniverseConverter : EngineTypeYamlConverterBase { public override IUniverse? Read(IParser parser, Type type, ObjectDeserializer rootDeserializer) { diff --git a/Engine.Integration/Engine.Integration.Yaml/Converters/UniverseObjectConverter.cs b/Engine.Integration/Engine.Integration.Yaml/Converters/UniverseObjectConverter.cs index 47d8513..79bae1e 100644 --- a/Engine.Integration/Engine.Integration.Yaml/Converters/UniverseObjectConverter.cs +++ b/Engine.Integration/Engine.Integration.Yaml/Converters/UniverseObjectConverter.cs @@ -11,7 +11,7 @@ using YamlDotNet.Serialization; namespace Engine.Serializers.Yaml; -public class UniverseObjectSerializer : EngineTypeYamlSerializerBase +public class UniverseObjectConverter : EngineTypeYamlConverterBase { public override IUniverseObject? Read(IParser parser, Type type, ObjectDeserializer rootDeserializer) { diff --git a/Engine.Systems/Tween/Yields/WaitForTweenDoneCoroutineYield.cs b/Engine.Systems/Tween/Yields/WaitForTweenDoneCoroutineYield.cs index c57853e..ac66745 100644 --- a/Engine.Systems/Tween/Yields/WaitForTweenDoneCoroutineYield.cs +++ b/Engine.Systems/Tween/Yields/WaitForTweenDoneCoroutineYield.cs @@ -2,4 +2,4 @@ using Engine.Core; namespace Engine.Systems.Tween; -public class WaitWhileTweenActiveCoroutineYield(ITween tween) : WaitUntilYield(() => tween.State.CheckFlag(TweenState.Completed | TweenState.Cancelled)); +public class WaitForTweenDoneCoroutineYield(ITween tween) : WaitUntilYield(() => tween.State.CheckFlag(TweenState.Completed | TweenState.Cancelled)); -- 2.49.1 From 734649955ec1eb5e8153d4e4dab1cf17346ff0dc Mon Sep 17 00:00:00 2001 From: Syntriax Date: Sat, 28 Mar 2026 22:32:51 +0300 Subject: [PATCH 45/51] feat: added .ApplyMatrix extension method for Matrix4x4 --- Engine.Core/Primitives/Matrix4x4.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Engine.Core/Primitives/Matrix4x4.cs b/Engine.Core/Primitives/Matrix4x4.cs index 364fcfd..cc6632f 100644 --- a/Engine.Core/Primitives/Matrix4x4.cs +++ b/Engine.Core/Primitives/Matrix4x4.cs @@ -436,4 +436,9 @@ public static class Matrix4x4Extensions /// 's. + ///
+ public static Matrix4x4 ApplyMatrix(this Matrix4x4 left, Matrix4x4 right) => left * right; } -- 2.49.1 From 0707665481c000749b30038126e0aefd7e96b1d3 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Sat, 28 Mar 2026 22:35:01 +0300 Subject: [PATCH 46/51] feat: added a basic Viewport field to ICamera --- Engine.Core/Abstract/ICamera.cs | 5 +++++ .../Behaviours/MonoGameCamera2D.cs | 21 +++++++++---------- .../Behaviours/MonoGameCamera3D.cs | 16 +++++++------- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/Engine.Core/Abstract/ICamera.cs b/Engine.Core/Abstract/ICamera.cs index 850bce9..cc9912d 100644 --- a/Engine.Core/Abstract/ICamera.cs +++ b/Engine.Core/Abstract/ICamera.cs @@ -5,6 +5,11 @@ namespace Engine.Core; /// public interface ICamera { + /// + /// The viewport of the . + /// + Vector2D Viewport { get; } + /// /// View of the . /// diff --git a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera2D.cs b/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera2D.cs index eafd9db..aa15833 100644 --- a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera2D.cs +++ b/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera2D.cs @@ -1,5 +1,4 @@ using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; using Engine.Core; @@ -41,7 +40,7 @@ public class MonoGameCamera2D : Behaviour, ICamera2D, IFirstFrameUpdate, ILastFr } } = Matrix4x4.Identity; - public Viewport Viewport + public Vector2D Viewport { get; set @@ -72,8 +71,8 @@ public class MonoGameCamera2D : Behaviour, ICamera2D, IFirstFrameUpdate, ILastFr // TODO This causes delay since OnPreDraw calls assuming this is called in in Update public Vector2D ScreenToWorldPosition(Vector2D screenPosition) { - float x = 2f * screenPosition.X / Viewport.Width - 1f; - float y = 1f - 2f * screenPosition.Y / Viewport.Height; + float x = 2f * screenPosition.X / Viewport.X - 1f; + float y = 1f - 2f * screenPosition.Y / Viewport.Y; Vector4D normalizedCoordinates = new(x, y, 0f, 1f); Matrix4x4 invertedViewProjectionMatrix = (ProjectionMatrix * ViewMatrix).Inverse; @@ -96,8 +95,8 @@ public class MonoGameCamera2D : Behaviour, ICamera2D, IFirstFrameUpdate, ILastFr if (clip.W != 0f) clip /= clip.W; - float screenX = (clip.X + 1f) * .5f * Viewport.Width; - float screenY = (1f - clip.Y) * .5f * Viewport.Height; + float screenX = (clip.X + 1f) * .5f * Viewport.X; + float screenY = (1f - clip.Y) * .5f * Viewport.Y; return new(screenX, screenY); } @@ -106,17 +105,17 @@ public class MonoGameCamera2D : Behaviour, ICamera2D, IFirstFrameUpdate, ILastFr public void FirstActiveFrame() { Graphics = BehaviourController.UniverseObject.Universe.FindRequiredBehaviour().Window.Graphics; - Viewport = Graphics.GraphicsDevice.Viewport; + Viewport = new(Graphics.GraphicsDevice.Viewport.Width, Graphics.GraphicsDevice.Viewport.Height); Transform = BehaviourController.GetRequiredBehaviour(); } public void PreDraw() { - ProjectionMatrix = Matrix4x4.CreateOrthographicViewCentered(Viewport.Width, Viewport.Height); + ProjectionMatrix = Matrix4x4.CreateOrthographicViewCentered(Viewport.X, Viewport.Y); ViewMatrix = Matrix4x4.Identity + .ApplyTranslation(new Vector3D(-Transform.Position.X, -Transform.Position.Y, 0f)) + .ApplyRotationZ(Transform.Rotation * Math.DegreeToRadian) .ApplyScale(Transform.Scale.X.Max(Transform.Scale.Y)) - .ApplyScale(Zoom) - .ApplyRotationZ(-Transform.Rotation * Math.DegreeToRadian) - .ApplyTranslation(new Vector3D(-Transform.Position.X, -Transform.Position.Y, 0f)); + .ApplyScale(Zoom); } } diff --git a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera3D.cs b/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera3D.cs index 1784c85..b5b98e1 100644 --- a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera3D.cs +++ b/Engine.Integration/Engine.Integration.MonoGame/Behaviours/MonoGameCamera3D.cs @@ -49,7 +49,7 @@ public class MonoGameCamera3D : Behaviour, ICamera3D, IFirstFrameUpdate, ILastFr } } = Matrix4x4.Identity; - public Viewport Viewport + public Vector2D Viewport { get; set @@ -57,7 +57,7 @@ public class MonoGameCamera3D : Behaviour, ICamera3D, IFirstFrameUpdate, ILastFr if (field.Equals(value)) return; - Viewport previousViewport = field; + Vector2D previousViewport = field; field = value; SetForRecalculation(); OnViewportChanged.Invoke(this, new(previousViewport)); @@ -114,21 +114,21 @@ public class MonoGameCamera3D : Behaviour, ICamera3D, IFirstFrameUpdate, ILastFr Matrix projection = ProjectionMatrix.ToXnaMatrix(); Matrix view = ViewMatrix.ToXnaMatrix(); - Vector3 worldNear = Viewport.Unproject(nearPoint, projection, view, Matrix.Identity); - Vector3 worldFar = Viewport.Unproject(farPoint, projection, view, Matrix.Identity); + Vector3 worldNear = Graphics.GraphicsDevice.Viewport.Unproject(nearPoint, projection, view, Matrix.Identity); + Vector3 worldFar = Graphics.GraphicsDevice.Viewport.Unproject(farPoint, projection, view, Matrix.Identity); Vector3 direction = Vector3.Normalize(worldFar - worldNear); return new(worldNear.ToVector3D(), direction.ToVector3D()); } - public Vector2D WorldToScreenPosition(Vector3D worldPosition) => Viewport.Project(worldPosition.ToVector3(), ProjectionMatrix.ToXnaMatrix(), ViewMatrix.ToXnaMatrix(), Matrix.Identity).ToVector3D(); + public Vector2D WorldToScreenPosition(Vector3D worldPosition) => Graphics.GraphicsDevice.Viewport.Project(worldPosition.ToVector3(), ProjectionMatrix.ToXnaMatrix(), ViewMatrix.ToXnaMatrix(), Matrix.Identity).ToVector3D(); public void LastActiveFrame() => Transform.OnTransformUpdated.RemoveListener(SetDirtyOnTransformUpdate); public void FirstActiveFrame() { Transform = BehaviourController.GetRequiredBehaviour(); Graphics = BehaviourController.UniverseObject.Universe.FindRequiredBehaviour().Window.Graphics; - Viewport = Graphics.GraphicsDevice.Viewport; + Viewport = new(Graphics.GraphicsDevice.Viewport.Width, Graphics.GraphicsDevice.Viewport.Height); Transform.OnTransformUpdated.AddListener(SetDirtyOnTransformUpdate); } @@ -167,7 +167,7 @@ public class MonoGameCamera3D : Behaviour, ICamera3D, IFirstFrameUpdate, ILastFr private void CalculateProjection() { float yScale = 1f / (float)Math.Tan(fieldOfView / 2f); - float xScale = yScale / Viewport.AspectRatio; + float xScale = yScale / (Viewport.X / Viewport.Y); ProjectionMatrix = new Matrix4x4( xScale, 0, 0, 0, @@ -179,5 +179,5 @@ public class MonoGameCamera3D : Behaviour, ICamera3D, IFirstFrameUpdate, ILastFr public readonly record struct ViewChangedArguments(Matrix4x4 PreviousView); public readonly record struct ProjectionChangedArguments(Matrix4x4 PreviousProjection); - public readonly record struct ViewportChangedArguments(Viewport PreviousViewport); + public readonly record struct ViewportChangedArguments(Vector2D PreviousViewport); } -- 2.49.1 From fe0173b091a00e02dd0ebe59f6678228e0d6f540 Mon Sep 17 00:00:00 2001 From: Syntriax Date: Sat, 28 Mar 2026 22:35:47 +0300 Subject: [PATCH 47/51] fix: SpriteBatcher not drawing correctly --- .../Behaviours/SpriteBatcher.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/SpriteBatcher.cs b/Engine.Integration/Engine.Integration.MonoGame/Behaviours/SpriteBatcher.cs index 7ca0b88..c34d84f 100644 --- a/Engine.Integration/Engine.Integration.MonoGame/Behaviours/SpriteBatcher.cs +++ b/Engine.Integration/Engine.Integration.MonoGame/Behaviours/SpriteBatcher.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +using Microsoft.Xna.Framework.Graphics; + using Engine.Core; namespace Engine.Integration.MonoGame; @@ -12,6 +14,7 @@ public class SpriteBatcher : Behaviour, IFirstFrameUpdate, IDraw private ISpriteBatch spriteBatch = null!; private MonoGameCamera2D camera2D = null!; + private readonly RasterizerState rasterizerState = new() { CullMode = CullMode.CullClockwiseFace }; private readonly ActiveBehaviourCollectorOrdered drawableSprites = new(GetPriority(), SortByPriority()); public void FirstActiveFrame() @@ -26,7 +29,13 @@ public class SpriteBatcher : Behaviour, IFirstFrameUpdate, IDraw public void Draw() { - spriteBatch.Begin(transformMatrix: camera2D.ViewMatrix.ToXnaMatrix()); + Matrix4x4 transformMatrix = Matrix4x4.Identity + .ApplyTranslation(new Vector2D(camera2D.Viewport.X, camera2D.Viewport.Y) * .5f) + .ApplyScale(new Vector3D(1f, -1f, 1f)) + .ApplyMatrix(camera2D.ViewMatrix) + .Transposed; + + spriteBatch.Begin(transformMatrix: transformMatrix.ToXnaMatrix(), rasterizerState: rasterizerState); for (int i = 0; i < drawableSprites.Count; i++) drawableSprites[i].Draw(spriteBatch); spriteBatch.End(); -- 2.49.1 From c11aced8d2e81ae967d55b91ab76b8fcb2089a3a Mon Sep 17 00:00:00 2001 From: Syntriax Date: Tue, 31 Mar 2026 17:21:18 +0300 Subject: [PATCH 48/51] feat: added default valueless Get to IConfiguration for nullables --- Engine.Core/Config/BasicConfiguration.cs | 7 ++++--- Engine.Core/Config/IConfiguration.cs | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Engine.Core/Config/BasicConfiguration.cs b/Engine.Core/Config/BasicConfiguration.cs index 9e8d2f1..8f833c7 100644 --- a/Engine.Core/Config/BasicConfiguration.cs +++ b/Engine.Core/Config/BasicConfiguration.cs @@ -12,17 +12,18 @@ public class BasicConfiguration : IConfiguration public IReadOnlyDictionary Values => values; - public T? Get(string key, T? defaultValue = default) + public T Get(string key, T defaultValue) => Get(key) ?? defaultValue; + public T? Get(string key) { if (!values.TryGetValue(key, out object? value)) - return defaultValue; + return default; if (value is T castedObject) return castedObject; try { return (T?)System.Convert.ChangeType(value, typeof(T)); } catch { } - return defaultValue; + return default; } public object? Get(string key) diff --git a/Engine.Core/Config/IConfiguration.cs b/Engine.Core/Config/IConfiguration.cs index 0bd9dfe..15abbb9 100644 --- a/Engine.Core/Config/IConfiguration.cs +++ b/Engine.Core/Config/IConfiguration.cs @@ -15,7 +15,8 @@ public interface IConfiguration bool Has(string key); object? Get(string key); - T? Get(string key, T? defaultValue = default); + T Get(string key, T defaultValue); + T? Get(string key); void Set(string key, T value); void Remove(string key); -- 2.49.1 From 2266f6992753cdf68aec11323166fdb3ccac5b8e Mon Sep 17 00:00:00 2001 From: Syntriax Date: Tue, 31 Mar 2026 21:01:10 +0300 Subject: [PATCH 49/51] fix: color lerping not working from bright to darker --- Engine.Core/Primitives/ColorRGB.cs | 2 +- Engine.Core/Primitives/ColorRGBA.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Engine.Core/Primitives/ColorRGB.cs b/Engine.Core/Primitives/ColorRGB.cs index 1fada67..cd93a23 100644 --- a/Engine.Core/Primitives/ColorRGB.cs +++ b/Engine.Core/Primitives/ColorRGB.cs @@ -95,7 +95,7 @@ public readonly struct ColorRGB(byte r, byte g, byte b) : IEquatable int greenDiff = to.G - from.G; int blueDiff = to.B - from.B; - return from + new ColorRGB((byte)(redDiff * t), (byte)(greenDiff * t), (byte)(blueDiff * t)); + return new((byte)(from.R - redDiff * t), (byte)(from.G - greenDiff * t), (byte)(from.B - blueDiff * t)); } /// diff --git a/Engine.Core/Primitives/ColorRGBA.cs b/Engine.Core/Primitives/ColorRGBA.cs index 8163840..c8be913 100644 --- a/Engine.Core/Primitives/ColorRGBA.cs +++ b/Engine.Core/Primitives/ColorRGBA.cs @@ -125,7 +125,7 @@ public readonly struct ColorRGBA(byte r, byte g, byte b, byte a = 255) : IEquata int blueDiff = to.B - from.B; int alphaDiff = to.A - from.A; - return from + new ColorRGBA((byte)(redDiff * t), (byte)(greenDiff * t), (byte)(blueDiff * t), (byte)(alphaDiff * t)); + return new((byte)(from.R - redDiff * t), (byte)(from.G - greenDiff * t), (byte)(from.B - blueDiff * t), (byte)(from.A - alphaDiff * t)); } /// -- 2.49.1 From 24046cf60c5ef52080a799442cf331b8ecaa25ec Mon Sep 17 00:00:00 2001 From: Syntriax Date: Tue, 31 Mar 2026 22:51:59 +0300 Subject: [PATCH 50/51] fix: triangle batch not drawing transparent colors properly --- .../Engine.Integration.MonoGame/MonoGameTriangleBatch.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Engine.Integration/Engine.Integration.MonoGame/MonoGameTriangleBatch.cs b/Engine.Integration/Engine.Integration.MonoGame/MonoGameTriangleBatch.cs index da575f6..73cf212 100644 --- a/Engine.Integration/Engine.Integration.MonoGame/MonoGameTriangleBatch.cs +++ b/Engine.Integration/Engine.Integration.MonoGame/MonoGameTriangleBatch.cs @@ -39,7 +39,7 @@ public class MonoGameTriangleBatch : Behaviour, ITriangleBatch, IFirstFrameUpdat Vector2 A = triangle.A.ToVector2(); Vector2 B = triangle.B.ToVector2(); Vector2 C = triangle.C.ToVector2(); - Color color = colorRGBA.ToColor(); + Color color = colorRGBA.ToPreMultipliedColor(); vertices[verticesIndex++] = new(new(A.X, A.Y, 0f), color); vertices[verticesIndex++] = new(new(B.X, B.Y, 0f), color); @@ -60,6 +60,8 @@ public class MonoGameTriangleBatch : Behaviour, ITriangleBatch, IFirstFrameUpdat return; graphicsDevice.RasterizerState = rasterizerState; + graphicsDevice.BlendState = BlendState.AlphaBlend; + basicEffect.Projection = projection; basicEffect.View = view; vertexBuffer.SetData(vertices); @@ -67,8 +69,10 @@ public class MonoGameTriangleBatch : Behaviour, ITriangleBatch, IFirstFrameUpdat graphicsDevice.SetVertexBuffer(vertexBuffer); foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes) + { pass.Apply(); - graphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, verticesIndex / 3); + graphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, verticesIndex / 3); + } verticesIndex = 0; } -- 2.49.1 From a47f8e4944ecfdeda8177b3e188dcc4563889d8b Mon Sep 17 00:00:00 2001 From: Syntriax Date: Tue, 31 Mar 2026 23:11:42 +0300 Subject: [PATCH 51/51] fix: color lerping not working properly --- Engine.Core/Primitives/ColorRGB.cs | 6 +++++- Engine.Core/Primitives/ColorRGBA.cs | 8 +++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Engine.Core/Primitives/ColorRGB.cs b/Engine.Core/Primitives/ColorRGB.cs index cd93a23..afcc5e7 100644 --- a/Engine.Core/Primitives/ColorRGB.cs +++ b/Engine.Core/Primitives/ColorRGB.cs @@ -95,7 +95,11 @@ public readonly struct ColorRGB(byte r, byte g, byte b) : IEquatable int greenDiff = to.G - from.G; int blueDiff = to.B - from.B; - return new((byte)(from.R - redDiff * t), (byte)(from.G - greenDiff * t), (byte)(from.B - blueDiff * t)); + return new( + (byte)(from.R + redDiff * t), + (byte)(from.G + greenDiff * t), + (byte)(from.B + blueDiff * t) + ); } /// diff --git a/Engine.Core/Primitives/ColorRGBA.cs b/Engine.Core/Primitives/ColorRGBA.cs index c8be913..ce14ac8 100644 --- a/Engine.Core/Primitives/ColorRGBA.cs +++ b/Engine.Core/Primitives/ColorRGBA.cs @@ -1,4 +1,5 @@ using System; +using Engine.Core.Debug; namespace Engine.Core; @@ -125,7 +126,12 @@ public readonly struct ColorRGBA(byte r, byte g, byte b, byte a = 255) : IEquata int blueDiff = to.B - from.B; int alphaDiff = to.A - from.A; - return new((byte)(from.R - redDiff * t), (byte)(from.G - greenDiff * t), (byte)(from.B - blueDiff * t), (byte)(from.A - alphaDiff * t)); + return new( + (byte)(from.R + redDiff * t), + (byte)(from.G + greenDiff * t), + (byte)(from.B + blueDiff * t), + (byte)(from.A + alphaDiff * t) + ); } /// -- 2.49.1