diff --git a/configs/addons/counterstrikesharp/gamedata/gamedata.json b/configs/addons/counterstrikesharp/gamedata/gamedata.json
index 05c93afac..208172b6d 100644
--- a/configs/addons/counterstrikesharp/gamedata/gamedata.json
+++ b/configs/addons/counterstrikesharp/gamedata/gamedata.json
@@ -296,5 +296,12 @@
"windows": "44 89 4C 24 ? 44 88 44 24",
"linux": "55 48 89 E5 41 57 49 89 F7 41 56 41 55 41 54 4D 89 C4"
}
+ },
+ "CCSNavArea_IsValidNavMesh": {
+ "signatures": {
+ "library": "server",
+ "windows": "48 83 3D ? ? ? ? ? 0F 95 C0 C3 CC CC CC CC C2",
+ "linux": "48 8D 05 ? ? ? ? 48 83 38 ? 0F 95 C0 C3"
+ }
}
-}
+}
\ No newline at end of file
diff --git a/examples/HelloWorld/HelloWorld.csproj b/examples/HelloWorld/HelloWorld.csproj
index 161acea22..a4626c9c7 100644
--- a/examples/HelloWorld/HelloWorld.csproj
+++ b/examples/HelloWorld/HelloWorld.csproj
@@ -6,4 +6,7 @@
false
false
+
+
+
diff --git a/examples/WarcraftPlugin/WarcraftPlugin.csproj b/examples/WarcraftPlugin/WarcraftPlugin.csproj
index 4f28b7f3e..794610a44 100644
--- a/examples/WarcraftPlugin/WarcraftPlugin.csproj
+++ b/examples/WarcraftPlugin/WarcraftPlugin.csproj
@@ -21,5 +21,7 @@
-
-
\ No newline at end of file
+
+
+
+
diff --git a/examples/WithCheckTransmit/WithCheckTransmit.csproj b/examples/WithCheckTransmit/WithCheckTransmit.csproj
index 9ed914b5b..18ae3a433 100644
--- a/examples/WithCheckTransmit/WithCheckTransmit.csproj
+++ b/examples/WithCheckTransmit/WithCheckTransmit.csproj
@@ -6,4 +6,7 @@
enable
+
+
+
diff --git a/examples/WithCommands/WithCommands.csproj b/examples/WithCommands/WithCommands.csproj
index 161acea22..a4626c9c7 100644
--- a/examples/WithCommands/WithCommands.csproj
+++ b/examples/WithCommands/WithCommands.csproj
@@ -6,4 +6,7 @@
false
false
+
+
+
diff --git a/examples/WithConfig/WithConfig.csproj b/examples/WithConfig/WithConfig.csproj
index 161acea22..a4626c9c7 100644
--- a/examples/WithConfig/WithConfig.csproj
+++ b/examples/WithConfig/WithConfig.csproj
@@ -6,4 +6,7 @@
false
false
+
+
+
diff --git a/examples/WithDatabaseDapper/WithDatabaseDapper.csproj b/examples/WithDatabaseDapper/WithDatabaseDapper.csproj
index 147a7f197..029a11ca4 100644
--- a/examples/WithDatabaseDapper/WithDatabaseDapper.csproj
+++ b/examples/WithDatabaseDapper/WithDatabaseDapper.csproj
@@ -10,4 +10,7 @@
+
+
+
diff --git a/examples/WithDependencyInjection/WithDependencyInjection.csproj b/examples/WithDependencyInjection/WithDependencyInjection.csproj
index 161acea22..a4626c9c7 100644
--- a/examples/WithDependencyInjection/WithDependencyInjection.csproj
+++ b/examples/WithDependencyInjection/WithDependencyInjection.csproj
@@ -6,4 +6,7 @@
false
false
+
+
+
diff --git a/examples/WithEntityOutputHooks/WithEntityOutputHooks.csproj b/examples/WithEntityOutputHooks/WithEntityOutputHooks.csproj
index 161acea22..a4626c9c7 100644
--- a/examples/WithEntityOutputHooks/WithEntityOutputHooks.csproj
+++ b/examples/WithEntityOutputHooks/WithEntityOutputHooks.csproj
@@ -6,4 +6,7 @@
false
false
+
+
+
diff --git a/examples/WithFakeConvars/WithFakeConvars.csproj b/examples/WithFakeConvars/WithFakeConvars.csproj
index 161acea22..a4626c9c7 100644
--- a/examples/WithFakeConvars/WithFakeConvars.csproj
+++ b/examples/WithFakeConvars/WithFakeConvars.csproj
@@ -6,4 +6,7 @@
false
false
+
+
+
diff --git a/examples/WithGameEventHandlers/WithGameEventHandlers.csproj b/examples/WithGameEventHandlers/WithGameEventHandlers.csproj
index 161acea22..a4626c9c7 100644
--- a/examples/WithGameEventHandlers/WithGameEventHandlers.csproj
+++ b/examples/WithGameEventHandlers/WithGameEventHandlers.csproj
@@ -6,4 +6,7 @@
false
false
+
+
+
diff --git a/examples/WithSharedTypes/WithSharedTypes.csproj b/examples/WithSharedTypes/WithSharedTypes.csproj
index 382b8a1df..1aaa7ea8a 100644
--- a/examples/WithSharedTypes/WithSharedTypes.csproj
+++ b/examples/WithSharedTypes/WithSharedTypes.csproj
@@ -8,5 +8,6 @@
+
diff --git a/examples/WithSharedTypesConsumer/WithSharedTypesConsumer.csproj b/examples/WithSharedTypesConsumer/WithSharedTypesConsumer.csproj
index 382b8a1df..1aaa7ea8a 100644
--- a/examples/WithSharedTypesConsumer/WithSharedTypesConsumer.csproj
+++ b/examples/WithSharedTypesConsumer/WithSharedTypesConsumer.csproj
@@ -8,5 +8,6 @@
+
diff --git a/examples/WithTranslations/WithTranslations.csproj b/examples/WithTranslations/WithTranslations.csproj
index c31ae7f09..c7459d3b3 100644
--- a/examples/WithTranslations/WithTranslations.csproj
+++ b/examples/WithTranslations/WithTranslations.csproj
@@ -9,4 +9,7 @@
+
+
+
diff --git a/examples/WithUserMessages/WithUserMessages.csproj b/examples/WithUserMessages/WithUserMessages.csproj
index 161acea22..a4626c9c7 100644
--- a/examples/WithUserMessages/WithUserMessages.csproj
+++ b/examples/WithUserMessages/WithUserMessages.csproj
@@ -6,4 +6,7 @@
false
false
+
+
+
diff --git a/examples/WithVirtualFunctions/WithVirtualFunctions.csproj b/examples/WithVirtualFunctions/WithVirtualFunctions.csproj
index 161acea22..a4626c9c7 100644
--- a/examples/WithVirtualFunctions/WithVirtualFunctions.csproj
+++ b/examples/WithVirtualFunctions/WithVirtualFunctions.csproj
@@ -6,4 +6,7 @@
false
false
+
+
+
diff --git a/examples/WithVoiceOverrides/WithVoiceOverrides.csproj b/examples/WithVoiceOverrides/WithVoiceOverrides.csproj
index 161acea22..a4626c9c7 100644
--- a/examples/WithVoiceOverrides/WithVoiceOverrides.csproj
+++ b/examples/WithVoiceOverrides/WithVoiceOverrides.csproj
@@ -6,4 +6,7 @@
false
false
+
+
+
diff --git a/managed/CounterStrikeSharp.API/Core/Model/CCSNavArea.cs b/managed/CounterStrikeSharp.API/Core/Model/CCSNavArea.cs
new file mode 100644
index 000000000..4687eeb5a
--- /dev/null
+++ b/managed/CounterStrikeSharp.API/Core/Model/CCSNavArea.cs
@@ -0,0 +1,332 @@
+using System.Runtime.InteropServices;
+using System.Runtime.CompilerServices;
+using CounterStrikeSharp.API.Modules.Utils;
+using CounterStrikeSharp.API.Modules.Memory;
+
+namespace CounterStrikeSharp.API.Core;
+
+public class CCSNavArea : NativeObject
+{
+ public CCSNavArea(IntPtr pointer) : base(pointer)
+ {
+ }
+ private static readonly Lazy IsWindows = new(() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows));
+
+ ///
+ /// Gets the nav area identifier.
+ ///
+ public uint Id => Data.Id;
+
+ ///
+ /// Gets the center position of the nav area.
+ ///
+ public Vector Center => new(Data.CenterX, Data.CenterY, Data.CenterZ);
+
+ ///
+ /// Gets the surface normal of the nav area.
+ ///
+ public Vector Normal => new(Data.NormalX, Data.NormalY, Data.NormalZ);
+
+ ///
+ /// Gets the minimum bounds of the nav area.
+ ///
+ public Vector Min => new(Math.Min(Data.MinX, Data.MaxX), Math.Min(Data.MinY, Data.MaxY), Math.Min(Data.MinZ, Data.MaxZ));
+
+ ///
+ /// Gets the maximum bounds of the nav area.
+ ///
+ public Vector Max => new(Math.Max(Data.MinX, Data.MaxX), Math.Max(Data.MinY, Data.MaxY), Math.Max(Data.MinZ, Data.MaxZ));
+
+ ///
+ /// Gets the width of the nav area on the X axis.
+ ///
+ public float Width
+ {
+ get
+ {
+ var min = Min;
+ var max = Max;
+ return Math.Abs(max.X - min.X);
+ }
+ }
+
+ ///
+ /// Gets the height of the nav area on the Y axis.
+ ///
+ public float Height
+ {
+ get
+ {
+ var min = Min;
+ var max = Max;
+ return Math.Abs(max.Y - min.Y);
+ }
+ }
+
+ ///
+ /// Gets the two-dimensional area size.
+ ///
+ public float Area2D => Width * Height;
+
+ ///
+ /// Returns whether the specified position is inside this nav area.
+ ///
+ /// World position to test.
+ /// Allowed Z distance from the nav area surface.
+ /// if the position is inside this nav area; otherwise, .
+ public bool ContainsPoint(Vector position, float zTolerance = 32f)
+ {
+ var min = Min;
+ var max = Max;
+
+ return position.X >= min.X && position.X <= max.X && position.Y >= min.Y && position.Y <= max.Y && Math.Abs(position.Z - GetHeightAtPosition(position.X, position.Y)) <= zTolerance;
+ }
+
+ ///
+ /// Returns whether the specified box is fully contained inside this nav area.
+ ///
+ /// Minimum bounds of the box.
+ /// Maximum bounds of the box.
+ /// Allowed Z distance from the nav area surface.
+ /// if the box is contained inside this nav area; otherwise, .
+ public bool ContainsBox(Vector mins, Vector maxs, float zTolerance = 32f)
+ {
+ var min = Min;
+ var max = Max;
+ var boxMinX = Math.Min(mins.X, maxs.X);
+ var boxMaxX = Math.Max(mins.X, maxs.X);
+ var boxMinY = Math.Min(mins.Y, maxs.Y);
+ var boxMaxY = Math.Max(mins.Y, maxs.Y);
+ var boxBottomZ = Math.Min(mins.Z, maxs.Z);
+
+ return boxMinX >= min.X && boxMaxX <= max.X && boxMinY >= min.Y && boxMaxY <= max.Y
+ && Math.Abs(boxBottomZ - GetHeightAtPosition(boxMinX, boxMinY)) <= zTolerance
+ && Math.Abs(boxBottomZ - GetHeightAtPosition(boxMinX, boxMaxY)) <= zTolerance
+ && Math.Abs(boxBottomZ - GetHeightAtPosition(boxMaxX, boxMinY)) <= zTolerance
+ && Math.Abs(boxBottomZ - GetHeightAtPosition(boxMaxX, boxMaxY)) <= zTolerance;
+ }
+
+ ///
+ /// Returns whether the specified box intersects this nav area.
+ ///
+ /// Minimum bounds of the box.
+ /// Maximum bounds of the box.
+ /// Allowed Z distance from the nav area surface.
+ /// if the box intersects this nav area; otherwise, .
+ public bool IntersectsBox(Vector mins, Vector maxs, float zTolerance = 32f)
+ {
+ var min = Min;
+ var max = Max;
+ var boxMinX = Math.Min(mins.X, maxs.X);
+ var boxMaxX = Math.Max(mins.X, maxs.X);
+ var boxMinY = Math.Min(mins.Y, maxs.Y);
+ var boxMaxY = Math.Max(mins.Y, maxs.Y);
+
+ if (boxMaxX < min.X || boxMinX > max.X || boxMaxY < min.Y || boxMinY > max.Y)
+ {
+ return false;
+ }
+
+ var x = Math.Clamp((boxMinX + boxMaxX) * 0.5f, min.X, max.X);
+ var y = Math.Clamp((boxMinY + boxMaxY) * 0.5f, min.Y, max.Y);
+ var z = GetHeightAtPosition(x, y);
+
+ return z >= Math.Min(mins.Z, maxs.Z) - zTolerance && z <= Math.Max(mins.Z, maxs.Z) + zTolerance;
+ }
+
+ ///
+ /// Gets the closest point on this nav area to the specified position.
+ ///
+ /// World position to compare against.
+ /// The closest point on this nav area.
+ public Vector GetClosestPoint(Vector position)
+ {
+ var min = Min;
+ var max = Max;
+ var x = Math.Clamp(position.X, min.X, max.X);
+ var y = Math.Clamp(position.Y, min.Y, max.Y);
+
+ return new Vector(x, y, GetHeightAtPosition(x, y));
+ }
+
+ ///
+ /// Gets the distance from this nav area to the specified position.
+ ///
+ /// World position to compare against.
+ /// The distance to this nav area.
+ public float GetDistanceToPoint(Vector position)
+ {
+ return MathF.Sqrt(GetDistanceSquaredToPoint(position));
+ }
+
+ ///
+ /// Gets the closest nav area to the specified position.
+ ///
+ /// World position to compare against.
+ /// Maximum search distance. A value less than or equal to zero disables the limit.
+ /// The closest nav area, or if none was found.
+ public static CCSNavArea? GetClosestNavArea(Vector position, float maximumDistance = -1)
+ {
+ return GetClosestNavArea(position, out _, maximumDistance);
+ }
+
+ ///
+ /// Gets the closest nav area to the specified position and outputs its distance.
+ ///
+ /// World position to compare against.
+ /// Distance to the closest nav area, or if none was found.
+ /// Maximum search distance. A value less than or equal to zero disables the limit.
+ /// The closest nav area, or if none was found.
+ public static CCSNavArea? GetClosestNavArea(Vector position, out float distance, float maximumDistance = -1)
+ {
+ var navAreas = GetAllNavAreas();
+ var maximumDistanceSquared = maximumDistance > 0 ? maximumDistance * maximumDistance : -1;
+ var closestDistanceSquared = float.MaxValue;
+ CCSNavArea? closestNavArea = null;
+ distance = float.MaxValue;
+
+ foreach (var navArea in navAreas)
+ {
+ var distanceSquared = navArea.GetDistanceSquaredToPoint(position);
+ if (maximumDistanceSquared > 0 && distanceSquared > maximumDistanceSquared)
+ {
+ continue;
+ }
+
+ if (distanceSquared < closestDistanceSquared)
+ {
+ closestDistanceSquared = distanceSquared;
+ closestNavArea = navArea;
+ }
+ }
+
+ if (closestNavArea != null)
+ {
+ distance = MathF.Sqrt(closestDistanceSquared);
+ }
+
+ return closestNavArea;
+ }
+
+ ///
+ /// Gets all nav areas from the current nav mesh.
+ ///
+ /// All nav areas, or an empty list if the nav mesh is unavailable.
+ public static IReadOnlyList GetAllNavAreas()
+ {
+ var navMeshAddress = GetNavMeshAddress();
+ if (navMeshAddress == IntPtr.Zero)
+ {
+ return Array.Empty();
+ }
+
+ var navMeshData = Marshal.PtrToStructure(navMeshAddress);
+ if (navMeshData.Count <= 0 || navMeshData.Areas == IntPtr.Zero)
+ {
+ return Array.Empty();
+ }
+
+ var navAreas = new List(navMeshData.Count);
+ for (var index = 0; index < navMeshData.Count; index++)
+ {
+ var navAreaAddress = Marshal.ReadIntPtr(navMeshData.Areas, index * IntPtr.Size);
+ if (navAreaAddress != IntPtr.Zero)
+ {
+ navAreas.Add(new CCSNavArea(navAreaAddress));
+ }
+ }
+
+ return navAreas;
+ }
+
+ private float GetHeightAtPosition(float x, float y)
+ {
+ var normal = Normal;
+ if (Math.Abs(normal.Z) <= 0.0001f)
+ {
+ return Center.Z;
+ }
+
+ var center = Center;
+ return center.Z - ((normal.X * (x - center.X)) + (normal.Y * (y - center.Y))) / normal.Z;
+ }
+
+ private float GetDistanceSquaredToPoint(Vector position)
+ {
+ return (position - GetClosestPoint(position)).LengthSqr();
+ }
+
+ private static IntPtr GetNavMeshAddress()
+ {
+ var signature = GameData.GetSignature("CCSNavArea_IsValidNavMesh");
+ if (string.IsNullOrWhiteSpace(signature))
+ {
+ return IntPtr.Zero;
+ }
+
+ var functionAddress = NativeAPI.FindSignature(Addresses.ServerPath, signature);
+ if (functionAddress == IntPtr.Zero)
+ {
+ return IntPtr.Zero;
+ }
+
+ var relativeOffset = Marshal.ReadInt32(functionAddress + 3);
+ var navMeshPointerAddress = functionAddress + relativeOffset + (IsWindows.Value ? 8 : 7);
+ return navMeshPointerAddress == IntPtr.Zero ? IntPtr.Zero : Marshal.ReadIntPtr(navMeshPointerAddress);
+ }
+
+ private unsafe ref CCSNavAreaData Data => ref Unsafe.AsRef((void*)Handle);
+
+ [StructLayout(LayoutKind.Explicit)]
+ private struct CCSNavAreaData
+ {
+ [FieldOffset(0x08)]
+ public uint Id;
+
+ [FieldOffset(0x0C)]
+ public float CenterX;
+
+ [FieldOffset(0x10)]
+ public float CenterY;
+
+ [FieldOffset(0x14)]
+ public float CenterZ;
+
+ [FieldOffset(0x18)]
+ public float NormalX;
+
+ [FieldOffset(0x1C)]
+ public float NormalY;
+
+ [FieldOffset(0x20)]
+ public float NormalZ;
+
+ [FieldOffset(0x24)]
+ public float MinX;
+
+ [FieldOffset(0x28)]
+ public float MinY;
+
+ [FieldOffset(0x2C)]
+ public float MinZ;
+
+ [FieldOffset(0x30)]
+ public float MaxX;
+
+ [FieldOffset(0x34)]
+ public float MaxY;
+
+ [FieldOffset(0x38)]
+ public float MaxZ;
+ }
+
+ [StructLayout(LayoutKind.Explicit)]
+ private struct CCSNavMeshData
+ {
+ [FieldOffset(0x08)]
+ public int Count;
+
+ [FieldOffset(0x10)]
+ public IntPtr Areas;
+ }
+}
diff --git a/managed/CounterStrikeSharp.Tests.Native/NativeTestsPlugin.csproj b/managed/CounterStrikeSharp.Tests.Native/NativeTestsPlugin.csproj
index 2c9265659..2498f5a2a 100644
--- a/managed/CounterStrikeSharp.Tests.Native/NativeTestsPlugin.csproj
+++ b/managed/CounterStrikeSharp.Tests.Native/NativeTestsPlugin.csproj
@@ -16,4 +16,8 @@
+
+
+
+
diff --git a/managed/TestPlugin/TestPlugin.csproj b/managed/TestPlugin/TestPlugin.csproj
index 733439900..80e20f740 100644
--- a/managed/TestPlugin/TestPlugin.csproj
+++ b/managed/TestPlugin/TestPlugin.csproj
@@ -13,4 +13,8 @@
PreserveNewest
+
+
+
+