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