update readme

This commit is contained in:
SlynxCZ 2026-01-31 19:58:28 +04:00
parent 8e0e61acd1
commit 11f9e2e541
2 changed files with 247 additions and 259 deletions

310
README.md
View file

@ -1,13 +1,12 @@
# Ray-Trace
**Shared ray tracing interface for Metamod:Source & CounterStrikeSharp
plugins**
**Shared ray tracing interface for Metamod:Source & CounterStrikeSharp plugins**
------------------------------------------------------------------------
## Overview
`Ray-Trace` is a lightweight **Metamod interface module** for\
`Ray-Trace` is a lightweight **Metamod interface module** for
**Counter-Strike 2** servers.
It exposes a shared interface: `CRayTraceInterface001` which can be
@ -21,8 +20,7 @@ from C# by calling its **vtable functions directly** using a native
handle.
The goal is to provide a **single tracing backend** usable from both
worlds\
without duplicating engine detours.
worlds without duplicating engine detours.
------------------------------------------------------------------------
@ -42,7 +40,7 @@ without duplicating engine detours.
## Exposed Interface (C++)
``` cpp
```cpp
class CRayTraceInterface
{
public:
@ -72,18 +70,20 @@ public:
TraceResult* pOutResult
) = 0;
};
```
````
**Return value:** - true → trace hit something, TraceResult is valid\
- false → no hit
**Return value:**
------------------------------------------------------------------------
* `true` → trace hit something, `TraceResult` is valid
* `false` → no hit
---
## Getting the interface
**C++ (Metamod plugin)**
### C++ (Metamod plugin)
``` cpp
```cpp
CRayTraceInterface* g_pRayTrace = nullptr;
bool g_bRayTraceLoaded = false;
@ -106,36 +106,28 @@ bool LoadRayTrace()
}
```
**C# (CounterStrikeSharp plugin)**
### C# (CounterStrikeSharp plugin)
``` csharp
private nint g_pRayTraceHandle = nint.Zero;
private bool g_bRayTraceLoaded = false;
For managed plugins, use the provided official wrapper:
public override void Load(bool hotReload)
{
g_pRayTraceHandle = Utilities.MetaFactory("CRayTraceInterface001");
if (g_pRayTraceHandle == nint.Zero)
{
throw new Exception("Failed to get Ray-Trace interface handle");
}
Bind();
g_bRayTraceLoaded = true;
}
```
public/Example.cs
```
The returned handle is a pointer to the native CRayTraceInterface
object.
This file contains:
------------------------------------------------------------------------
* Correct vtable bindings for Linux & Windows
* Native-compatible struct layouts (`TraceOptions`, `TraceResult`)
* High-level safe API for tracing
* No need to manually bind delegates
---
## Calling methods from C++ (Metamod)
**TraceShape example**
### TraceShape example
``` cpp
```cpp
Vector vecOrigin{};
QAngle angView{};
TraceOptions traceOpts{};
@ -165,250 +157,72 @@ if (g_pRayTrace && g_bRayTraceLoaded)
}
```
**TraceEndShape example**
``` cpp
TraceResult traceResult{};
bool bHit = g_pRayTrace->TraceEndShape(
&vecStartPos,
&vecEndPos,
nullptr,
&traceOpts,
&traceResult
);
```
**TraceShapeEx (low-level)**
``` cpp
Ray_t ray{};
CTraceFilter filter(
static_cast<uint64_t>(MASK_SHOT_FULL),
COLLISION_GROUP_DEFAULT,
true
);
TraceResult traceResult{};
bool bHit = g_pRayTrace->TraceShapeEx(
&vecStartPos,
&vecEndPos,
&filter,
ray,
&traceResult
);
```
------------------------------------------------------------------------
---
## Calling methods from C# (CounterStrikeSharp plugin)
``` csharp
private delegate bool TraceShapeFn(
nint pThis,
nint pOrigin,
nint pAngles,
nint pIgnoreEntity,
nint pOptions,
nint pOutResult
);
The official managed API is implemented in:
private TraceShapeFn? _traceShape;
private TraceShapeFn? _traceEndShape;
private TraceShapeFn? _traceShapeEx;
private void Bind()
{
_traceShape = VirtualFunction.Create<TraceShapeFn>(g_pRayTraceHandle, 1);
_traceEndShape = VirtualFunction.Create<TraceShapeFn>(g_pRayTraceHandle, 2);
_traceShapeEx = VirtualFunction.Create<TraceShapeFn>(g_pRayTraceHandle, 3);
}
public bool TraceShape(
nint pOrigin,
nint pAngles,
nint pIgnoreEntity,
nint pOptions,
nint pOutResult)
{
if (!g_bRayTraceLoaded || g_pRayTraceHandle == nint.Zero)
return false;
return _traceShape!(
g_pRayTraceHandle,
pOrigin,
pAngles,
pIgnoreEntity,
pOptions,
pOutResult
);
}
```
public/Example.cs
```
------------------------------------------------------------------------
### Example usage
## Memory allocation from C# (Important)
```csharp
using RayTrace;
using CounterStrikeSharp.API.Modules.Utils;
When calling `TraceShape` or `TraceEndShape` from C#, the plugin **must
allocate native memory** for the following structures:
- `TraceOptions`
- `TraceResult`
These parameters are native pointers in C++ (`TraceOptions*` and
`TraceResult*`) and must remain valid for the duration of the call.
The recommended and safest approach is using **stackalloc** (or unsafe
stack variables) to provide native memory on the stack.
Failing to allocate valid memory for these parameters will result in
crashes or undefined behavior.
### Example (C# stackalloc)
``` csharp
[StructLayout(LayoutKind.Sequential, Pack = 8)]
public struct TraceOptions
public void DoTrace(CCSPlayerController player)
{
public ulong InteractsWith;
public ulong InteractsExclude;
public int DrawBeam;
}
Vector origin = player.PlayerPawn.Value!.AbsOrigin;
QAngle angles = player.PlayerPawn.Value!.EyeAngles;
[StructLayout(LayoutKind.Sequential, Pack = 8)]
public struct TraceResult
{
public Vector EndPos;
public nint HitEntity;
public float Fraction;
public int AllSolid;
public Vector Normal;
}
unsafe
{
Vector origin = player.Position;
QAngle angles = player.ViewAngles;
TraceOptions* opts = stackalloc TraceOptions[1];
opts->InteractsWith = (ulong)MASK_SHOT_FULL;
opts->InteractsExclude = 0;
opts->DrawBeam = 0;
TraceResult* result = stackalloc TraceResult[1];
bool hit = _traceShape!(
g_pRayTraceHandle,
(nint)&origin,
(nint)&angles,
nint.Zero,
(nint)opts,
(nint)result
TraceOptions options = new(
InteractionLayers.MASK_SHOT_FULL
);
if (hit)
if (CRayTrace.TraceShape(origin, angles, null, options, out TraceResult result))
{
Console.WriteLine($"Hit at: {result->EndPos}");
Console.WriteLine($"Hit fraction: {result.Fraction}");
Console.WriteLine($"EndPos: {result.EndPos}");
}
}
```
------------------------------------------------------------------------
---
## Low-level usage from C# (Ray_t & CTraceFilter)
## VTable offsets (ABI note)
When using the low-level method `TraceShapeEx` from a C# plugin, the
plugin must provide its own native-compatible implementations of the
following engine structures:
Due to C++ ABI differences:
- `Ray_t`
- `CTraceFilter`
| Platform | TraceShape index | TraceEndShape index |
| ------------------- | ---------------- | ------------------- |
| Linux (Itanium ABI) | 2 | 3 |
| Windows (MSVC ABI) | 1 | 2 |
These structures are not exposed directly by the Ray-Trace interface and
must be recreated in managed code with correct memory layout.
`public/Example.cs` already applies the correct offsets internally.
### Ray_t (C#)
The C# plugin must define a struct that matches the native `Ray_t`
layout used by the engine.
Example (simplified):
``` csharp
[StructLayout(LayoutKind.Sequential)]
public struct Ray_t
{
public Vector3 m_vecStart;
public Vector3 m_vecDelta;
public byte m_IsRay;
public byte m_IsSwept;
}
```
(Exact layout depends on the engine version and must match native
memory.)
**CTraceFilter (C#)**\
For `CTraceFilter`, the plugin must:
- Define a managed struct matching the native layout.
- Resolve the CTraceFilter vtable pointer using a signature scan.
- Assign the resolved vtable to the struct before calling
TraceShapeEx.
Example concept:
``` csharp
[StructLayout(LayoutKind.Sequential)]
public unsafe struct CTraceFilter
{
public nint __vtable;
public ulong m_nInteractsWith;
public T ...;
}
```
**Important notes** - This setup is only required when using the
low-level API: - TraceShapeEx(...) - High-level functions (TraceShape,
TraceEndShape) do not require custom Ray_t or CTraceFilter handling from
C#. - Incorrect structure layout or invalid vtable resolution will
result in crashes or undefined behavior. - This is considered an
advanced use case intended for engine-level plugins.
------------------------------------------------------------------------
---
## Notes about ABI & Destructor
- CRayTraceInterface has a virtual destructor.
- The object is owned by the Ray-Trace Metamod module.
- Plugins must never call delete on the interface pointer.
- C# must only clear its handle on unload.
- All parameters are passed as native pointers (nint).
* `CRayTraceInterface` has a virtual destructor.
* The object is owned by the Ray-Trace Metamod module.
* Plugins must never call `delete` on the interface pointer.
* C# must only clear its handle on unload.
* All parameters are passed as native pointers (`nint`).
# Build
## Requirements
- HL2SDK-CS2
- Metamod:Source
- CMake
``` bash
git clone https://github.com/FUNPLAY-pro-CS2/Ray-Trace.git
cd Ray-Trace
git submodule update --init --recursive
docker compose -f docker/docker-compose.yml up
```
------------------------------------------------------------------------
---
## License
GPLv3\
https://www.gnu.org/licenses/gpl-3.0.en.html
GPLv3
[https://www.gnu.org/licenses/gpl-3.0.en.html](https://www.gnu.org/licenses/gpl-3.0.en.html)
---
## Author
**Michal "Slynx" Přikryl**\
https://slynxdev.cz
**Michal "Slynx" Přikryl**
[https://slynxdev.cz](https://slynxdev.cz)

174
public/Example.cs Normal file
View file

@ -0,0 +1,174 @@
using System.Numerics;
using System.Runtime.InteropServices;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Utils;
using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
namespace RayTrace
{
#region Native Structs (matching C++ layout exactly)
[Flags]
public enum InteractionLayers: ulong
{
Solid = 0x1,
Hitboxes = 0x2,
Trigger = 0x4,
Sky = 0x8,
PlayerClip = 0x10,
NPCClip = 0x20,
BlockLOS = 0x40,
BlockLight = 0x80,
Ladder = 0x100,
Pickup = 0x200,
BlockSound = 0x400,
NoDraw = 0x800,
Window = 0x1000,
PassBullets = 0x2000,
WorldGeometry = 0x4000,
Water = 0x8000,
Slime = 0x10000,
TouchAll = 0x20000,
Player = 0x40000,
NPC = 0x80000,
Debris = 0x100000,
Physics_Prop = 0x200000,
NavIgnore = 0x400000,
NavLocalIgnore = 0x800000,
PostProcessingVolume = 0x1000000,
UnusedLayer3 = 0x2000000,
CarriedObject = 0x4000000,
PushAway = 0x8000000,
ServerEntityOnClient = 0x10000000,
CarriedWeapon = 0x20000000,
StaticLevel = 0x40000000,
csgo_team1 = 0x80000000,
csgo_team2 = 0x100000000,
csgo_grenadeclip = 0x200000000,
csgo_droneclip = 0x400000000,
csgo_moveable = 0x800000000,
csgo_opaque = 0x1000000000,
csgo_monster = 0x2000000000,
csgo_thrown_grenade = 0x8000000000,
MASK_SHOT_PHYSICS = Solid | PlayerClip | Window | PassBullets | Player | NPC | Physics_Prop,
MASK_SHOT_HITBOX = Hitboxes | Player | NPC,
MASK_SHOT_FULL = MASK_SHOT_PHYSICS | Hitboxes,
MASK_WORLD_ONLY = Solid | Window | PassBullets,
MASK_GRENADE = Solid | Window | Physics_Prop | PassBullets,
MASK_BRUSH_ONLY = Solid | Window,
MASK_PLAYER_MOVE = Solid | Window | PlayerClip | PassBullets,
MASK_NPC_MOVE = Solid | Window | NPCClip | PassBullets
}
[StructLayout(LayoutKind.Explicit, Size = 24)]
public struct TraceOptions
{
[FieldOffset(0)] public ulong InteractsWith;
[FieldOffset(8)] public ulong InteractsExclude;
[FieldOffset(16)] public int DrawBeam;
public TraceOptions()
{
InteractsWith = (ulong)InteractionLayers.MASK_SHOT_PHYSICS;
InteractsExclude = 0;
DrawBeam = 0;
}
public TraceOptions(InteractionLayers interactsWith, InteractionLayers interactsExclude = 0, bool drawBeam = false)
{
InteractsWith = (ulong)interactsWith;
InteractsExclude = (ulong)interactsExclude;
DrawBeam = drawBeam ? 1 : 0;
}
}
[StructLayout(LayoutKind.Explicit, Size = 44)]
public struct TraceResult
{
[FieldOffset(0)] public float EndPosX;
[FieldOffset(4)] public float EndPosY;
[FieldOffset(8)] public float EndPosZ;
[FieldOffset(16)] public nint HitEntity;
[FieldOffset(24)] public float Fraction;
[FieldOffset(28)] public int AllSolid;
[FieldOffset(32)] public float NormalX;
[FieldOffset(36)] public float NormalY;
[FieldOffset(40)] public float NormalZ;
public Vector3 EndPos => new(EndPosX, EndPosY, EndPosZ);
public Vector3 Normal => new(NormalX, NormalY, NormalZ);
public bool DidHit => Fraction < 1.0f;
public bool IsAllSolid => AllSolid != 0;
}
#endregion
public static class CRayTrace
{
private static nint g_pRayTraceHandle = nint.Zero;
private static bool g_bRayTraceLoaded = false;
private static Func<nint, nint, nint, nint, nint, nint, bool>? _traceShape;
private static Func<nint, nint, nint, nint, nint, nint, bool>? _traceEndShape;
public static void Init()
{
g_pRayTraceHandle = (nint)Utilities.MetaFactory("CRayTraceInterface001")!;
if (g_pRayTraceHandle == nint.Zero)
throw new Exception("Failed to get Ray-Trace interface handle. Is Ray-Trace MetaMod module loaded?");
Bind();
g_bRayTraceLoaded = true;
}
private static void Bind()
{
_traceShape = VirtualFunction.Create<nint, nint, nint, nint, nint, nint, bool>(g_pRayTraceHandle, 2);
_traceEndShape = VirtualFunction.Create<nint, nint, nint, nint, nint, nint, bool>(g_pRayTraceHandle, 3);
}
public static unsafe bool TraceShape(Vector origin, QAngle angles, CBaseEntity? ignoreEntity, TraceOptions options, out TraceResult result)
{
result = default;
if (!g_bRayTraceLoaded || g_pRayTraceHandle == nint.Zero)
return false;
TraceResult resultBuffer = default;
TraceOptions optionsBuffer = options;
bool success = _traceShape!(g_pRayTraceHandle,
origin.Handle,
angles.Handle,
ignoreEntity?.Handle ?? nint.Zero,
(nint)(&optionsBuffer),
(nint)(&resultBuffer));
result = resultBuffer;
return success;
}
public static unsafe bool TraceEndShape(Vector origin, Vector endOrigin, CBaseEntity? ignoreEntity, TraceOptions options, out TraceResult result)
{
result = default;
if (!g_bRayTraceLoaded || g_pRayTraceHandle == nint.Zero)
return false;
TraceResult resultBuffer = default;
TraceOptions optionsBuffer = options;
bool success = _traceEndShape!(g_pRayTraceHandle,
origin.Handle,
endOrigin.Handle,
ignoreEntity?.Handle ?? nint.Zero,
(nint)(&optionsBuffer),
(nint)(&resultBuffer));
result = resultBuffer;
return success;
}
}
}