|
| 1 | +using System; |
| 2 | +using System.Runtime.InteropServices; |
| 3 | +using Microsoft.Extensions.Configuration; |
| 4 | +using Sharp.Shared; |
| 5 | +using Sharp.Shared.Definition; |
| 6 | +using Sharp.Shared.Enums; |
| 7 | +using Sharp.Shared.GameEntities; |
| 8 | +using Sharp.Shared.Objects; |
| 9 | +using Sharp.Shared.Types; |
| 10 | + |
| 11 | +namespace TraceNativeExample; |
| 12 | + |
| 13 | +public sealed class TraceNativeExample : IModSharpModule |
| 14 | +{ |
| 15 | + public string DisplayName => "Trace (Native) example"; |
| 16 | + public string DisplayAuthor => "Modsharp dev team"; |
| 17 | + |
| 18 | + private readonly ISharedSystem _sharedSystem; |
| 19 | + |
| 20 | + private static TraceNativeExample _instance = null!; |
| 21 | + |
| 22 | + public TraceNativeExample(ISharedSystem sharedSystem, |
| 23 | + string dllPath, |
| 24 | + string sharpPath, |
| 25 | + Version version, |
| 26 | + IConfiguration coreConfiguration, |
| 27 | + bool hotReload) |
| 28 | + { |
| 29 | + _sharedSystem = sharedSystem; |
| 30 | + _instance = this; |
| 31 | + } |
| 32 | + |
| 33 | + public bool Init() |
| 34 | + { |
| 35 | + // client chat/console |
| 36 | + _sharedSystem.GetClientManager().InstallCommandCallback("trace_line", OnCommandTraceLine); |
| 37 | + _sharedSystem.GetClientManager().InstallCommandCallback("trace_custom", OnCommandTraceCustom); |
| 38 | + |
| 39 | + return true; |
| 40 | + } |
| 41 | + |
| 42 | + public void Shutdown() |
| 43 | + { |
| 44 | + // must uninstall the callbacks on shutdown |
| 45 | + _sharedSystem.GetClientManager().RemoveCommandCallback("trace_line", OnCommandTraceLine); |
| 46 | + _sharedSystem.GetClientManager().RemoveCommandCallback("trace_custom", OnCommandTraceCustom); |
| 47 | + } |
| 48 | + |
| 49 | + private unsafe ECommandAction OnCommandTraceLine(IGameClient client, StringCommand command) |
| 50 | + { |
| 51 | + if (client.GetPlayerController() is not { } controller |
| 52 | + || controller.GetPlayerPawn() is not { IsValidEntity: true, IsAlive: true } pawn) |
| 53 | + { |
| 54 | + return ECommandAction.Handled; |
| 55 | + } |
| 56 | + |
| 57 | + var eyeAngles = pawn.GetEyeAngles(); |
| 58 | + |
| 59 | + // we start from player's eye position |
| 60 | + var start = pawn.GetEyePosition(); |
| 61 | + var direction = eyeAngles.AnglesToVectorForward(); |
| 62 | + |
| 63 | + const float maxDistance = 4096.0f; |
| 64 | + |
| 65 | + var end = start + (direction * maxDistance); |
| 66 | + |
| 67 | + // we use bullet attribute here |
| 68 | + var attribute = RnQueryShapeAttr.Bullets(); |
| 69 | + |
| 70 | + // make it ignore the player themselves, because we start tracing from player's |
| 71 | + // eye position and that is within the player's hitbox |
| 72 | + attribute.SetEntityToIgnore(pawn, 0); |
| 73 | + |
| 74 | + // this function is called for every entity the trace HITS, letting you decide if you |
| 75 | + // want to IGNORE that hit and continue tracing. we'll only use it if the |
| 76 | + // command has an argument (e.g., "trace_line filter_on"). |
| 77 | + // the `delegate* unmanaged` syntax creates a function pointer that the game engine can call directly, |
| 78 | + // without .NET runtime overhead. |
| 79 | + nint? filter = command.ArgCount > 0 |
| 80 | + ? (nint) (delegate* unmanaged<CTraceFilter*, nint, bool>) (&CTraceFilter_ShouldHitPlayerOnCT) |
| 81 | + : null; |
| 82 | + |
| 83 | + controller.Print(HudPrintChannel.Chat, $"Tracing {(filter == null ? "without" : "with")} ShouldHitPlayerOnCT filter"); |
| 84 | + |
| 85 | + var traceResult = _sharedSystem.GetPhysicsQueryManager() |
| 86 | + .TraceLine(start, |
| 87 | + end, |
| 88 | + attribute, |
| 89 | + filter); |
| 90 | + |
| 91 | + if (traceResult.DidHit()) |
| 92 | + { |
| 93 | + var hitEntity = _sharedSystem.GetEntityManager().MakeEntityFromPointer<IBaseEntity>(traceResult.Entity); |
| 94 | + |
| 95 | + controller.Print(HudPrintChannel.Chat, |
| 96 | + $" Hit entity!!! {ChatColor.NewLine}" |
| 97 | + + $" entity classname: {hitEntity.Classname} {ChatColor.NewLine}" |
| 98 | + + $" hit position: {traceResult.EndPosition} {ChatColor.NewLine}" |
| 99 | + + $" fraction: {traceResult.Fraction}"); |
| 100 | + } |
| 101 | + else |
| 102 | + { |
| 103 | + controller.Print(HudPrintChannel.Chat, "Failed to hit any entity."); |
| 104 | + } |
| 105 | + |
| 106 | + return ECommandAction.Handled; |
| 107 | + } |
| 108 | + |
| 109 | + private unsafe ECommandAction OnCommandTraceCustom(IGameClient client, StringCommand command) |
| 110 | + { |
| 111 | + if (client.GetPlayerController() is not { } controller |
| 112 | + || controller.GetPlayerPawn() is not { IsValidEntity: true, IsAlive: true } pawn) |
| 113 | + { |
| 114 | + return ECommandAction.Handled; |
| 115 | + } |
| 116 | + |
| 117 | + var eyeAngles = pawn.GetEyeAngles(); |
| 118 | + |
| 119 | + // we start from player's eye position |
| 120 | + var start = pawn.GetEyePosition(); |
| 121 | + var direction = eyeAngles.AnglesToVectorForward(); |
| 122 | + |
| 123 | + const float maxDistance = 4096.0f; |
| 124 | + |
| 125 | + var end = start + (direction * maxDistance); |
| 126 | + |
| 127 | + // we create our custom attribute here. |
| 128 | + // if you don't want to create a custom one, modsharp has a few built-in attributes: |
| 129 | + // RnQueryShapeAttr.Bullets(), |
| 130 | + // RnQueryShapeAttr.PlayerMovement(pawn.InteractsWith), |
| 131 | + // RnQueryShapeAttr.Knife() |
| 132 | + var attribute = new RnQueryShapeAttr |
| 133 | + { |
| 134 | + // what layers should this query attribute look for |
| 135 | + // you can think of it as a "whitelist", if an entity/object |
| 136 | + // doesn't have one of the flags in it, the trace will ignore it |
| 137 | + |
| 138 | + // here it is looking for anything that a bullet can interact with |
| 139 | + m_nInteractsWith = UsefulInteractionLayers.FireBullets, |
| 140 | + |
| 141 | + // what layers should this query attribute ignore |
| 142 | + // think of it as a "blacklist", if an entity/object has one of |
| 143 | + // the flags, the trace will ignore it |
| 144 | + // we don't want to hurt chickens, let's ignore it :) |
| 145 | + m_nInteractsExclude = InteractionLayers.CStrikeChicken, |
| 146 | + |
| 147 | + // what layers should this query represents as |
| 148 | + // here we are representing this layer as a player in T |
| 149 | + m_nInteractsAs = InteractionLayers.Player | InteractionLayers.CStrikeTeam1, |
| 150 | + |
| 151 | + // hit game entities and static entities |
| 152 | + m_nObjectSetMask = RnQueryObjectSet.All, |
| 153 | + m_nCollisionGroup = CollisionGroupType.ConditionallySolid, |
| 154 | + |
| 155 | + // if true, will hit solid geometry and entities |
| 156 | + HitSolid = true, |
| 157 | + |
| 158 | + // if true, HitSolid will require the query and shape have contacts |
| 159 | + HitSolidRequiresGenerateContacts = true, |
| 160 | + |
| 161 | + // if true, will hit trigger entities |
| 162 | + HitTrigger = false, |
| 163 | + |
| 164 | + // if true, then ignores if the query and shape entity IDs are in collision pairs |
| 165 | + // in other words, it will respect the entities you set to ignore with SetEntityToIgnore |
| 166 | + ShouldIgnoreDisabledPairs = true, |
| 167 | + |
| 168 | + // if true, then ignores if both query and shape interact with InteractionLayers.HitBoxes |
| 169 | + IgnoreIfBothInteractWithHitBoxes = false, |
| 170 | + |
| 171 | + // if true, will hit any objects in any conditions |
| 172 | + ForceHitEverything = false, |
| 173 | + |
| 174 | + // not sure what it does but the game sets it to true by default |
| 175 | + Unknown = true, |
| 176 | + }; |
| 177 | + |
| 178 | + // ignore ourselves because the query starts from ourselves |
| 179 | + // you can only set the index with 0 or 1 |
| 180 | + var index = 0; |
| 181 | + attribute.SetEntityToIgnore(pawn, index); |
| 182 | + |
| 183 | + // we create a hull shape here |
| 184 | + // if mins and maxs are identical the game will treat it as ShapeLine |
| 185 | + var hull = new TraceShapeHull { Mins = new Vector(-32, -32, -32), Maxs = new Vector(32, 32, 32) }; |
| 186 | + |
| 187 | + // or you can create the following shapes |
| 188 | + /* |
| 189 | + var line = new TraceShapeLine |
| 190 | + { |
| 191 | + }; |
| 192 | +
|
| 193 | + var sphere = new TraceShapeSphere |
| 194 | + { |
| 195 | + }; |
| 196 | +
|
| 197 | + var capsule = new TraceShapeCapsule |
| 198 | + { |
| 199 | + }; |
| 200 | + */ |
| 201 | + |
| 202 | + nint? filter = command.ArgCount > 0 |
| 203 | + ? (nint) (delegate* unmanaged<CTraceFilter*, nint, bool>) (&CTraceFilter_ShouldHitPlayerOnCT) |
| 204 | + : null; |
| 205 | + |
| 206 | + controller.Print(HudPrintChannel.Chat, $"Tracing {(filter == null ? "without" : "with")} ShouldHitPlayerOnCT filter"); |
| 207 | + |
| 208 | + var traceResult = _sharedSystem.GetPhysicsQueryManager() |
| 209 | + .TraceShape(new TraceShapeRay(hull), start, end, attribute, filter); |
| 210 | + |
| 211 | + if (traceResult.DidHit()) |
| 212 | + { |
| 213 | + var hitEntity = _sharedSystem.GetEntityManager().MakeEntityFromPointer<IBaseEntity>(traceResult.Entity); |
| 214 | + |
| 215 | + controller.Print(HudPrintChannel.Chat, |
| 216 | + $" Hit entity!!! {ChatColor.NewLine}" |
| 217 | + + $" entity classname: {hitEntity.Classname} {ChatColor.NewLine}" |
| 218 | + + $" hit position: {traceResult.EndPosition} {ChatColor.NewLine}" |
| 219 | + + $" fraction: {traceResult.Fraction}"); |
| 220 | + } |
| 221 | + else |
| 222 | + { |
| 223 | + controller.Print(HudPrintChannel.Chat, "Failed to hit any entity."); |
| 224 | + } |
| 225 | + |
| 226 | + return ECommandAction.Handled; |
| 227 | + } |
| 228 | + |
| 229 | + [UnmanagedCallersOnly] |
| 230 | + private static unsafe bool CTraceFilter_ShouldHitPlayerOnCT(CTraceFilter* filter, nint entityPtr) |
| 231 | + { |
| 232 | + // not a valid entity, don't hit, although this should never happen, just a safeguard |
| 233 | + if (_instance._sharedSystem.GetEntityManager().MakeEntityFromPointer<IBaseEntity>(entityPtr) is not |
| 234 | + { |
| 235 | + IsValidEntity: true, |
| 236 | + } entity) |
| 237 | + { |
| 238 | + return false; |
| 239 | + } |
| 240 | + |
| 241 | + // custom logic here |
| 242 | + // if we hit a player pawn |
| 243 | + if (entity is { IsPlayerPawn: true, IsAlive: true }) |
| 244 | + { |
| 245 | + // we only want to accept the hit if they are on the CT team. |
| 246 | + // if they are on the T team, this function returns false, and the trace will continue |
| 247 | + // through them until it hits a CT player or a wall. |
| 248 | + return entity.Team == CStrikeTeam.CT; |
| 249 | + } |
| 250 | + |
| 251 | + return true; |
| 252 | + } |
| 253 | +} |
0 commit comments