v1.2.4.b3

#### Compatibility
* **CounterStrikeSharp v1.0.375** - after the CS2 update, RayTraceImpl stopped working. All traces now use CounterStrikeSharp's native `Trace` API, so **RayTrace-CSS-API and RayTrace-MM are no longer required**. `RayTraceApi.dll` was removed from the project and the Ray-Trace step was removed from the README install guide.
* **Gamedata** - CS2 1.41.8.3 broke the Windows `HEGrenadeProjectile_CreateFunc`, `CMolotovProjectile_CreateFunc` and `SnapViewAngles` signatures, and the Linux `SmokeGrenadeProjectile_CreateFunc` signature. `INetworkGameServer_Slots` moved from 584 to 616 on both platforms; the old value read a garbage size and pointer without throwing. All signatures now match exactly one function on both platforms, and the offsets were confirmed at runtime.

#### Skills
* **No Recoil** - the client received `weapon_accuracy_nospread`, but the server still applied spread, so the crosshair looked straight while bullets spread. The weapon's spread and inaccuracy values are now also zeroed on the server for that tick. Weapon data is shared per weapon type, so a weapon type is only patched while no other living player holds the same type; the values are restored on the next tick, at map end and on unload.
* **Long Zeus** - kills went through suicide, so they did not count as Zeus kills and the ragdoll was marked `world`. The suicide damage is now rewritten to the attacker's Zeus with shock damage, so the engine credits the kill natively (killfeed, kill sound, scoreboard, ragdoll). The victim's body also gets the electric shock effect, which the engine only plays on real Taser hits.
* **Long Knife** - long-range slashes now draw a tracer to where they hit.
* **Fragile Bomb** - the bomb position was only stored by the `bomb_planted` event, which is sent only to skills active at that moment, so receiving the skill after the plant never detected hits. The planted bomb is now looked up when needed. Hits were also measured from the impact point, so shots passing through the bomb and landing behind it were missed; the bullet's path is now checked instead.
* **Tripwire** - wires stayed visible after the skill changed. Beams were hidden from clients in the same tick they were cleared, so the "hide" update never reached the client. Beams are no longer hidden from clients before removal.

#### Reliability
* **Skill distribution** - an exception thrown during skill assignment aborted the round's distribution silently. It is now caught, logged and retried. Spectators without a pawn no longer count as connected players, which had caused unnecessary retries.
This commit is contained in:
ByDexter 2026-09-25 03:34:10 +03:00
parent 20dafd2a7c
commit 71ef1acbe9
24 changed files with 446 additions and 159 deletions

View file

@ -98,6 +98,7 @@ namespace src.player
!Instance.SkillPlayer.Any(p => !p.IsDrawing && p.Skill == Skills.Fortnite))
Instance.SkillAction("Fortnite", "OnTakeDamage", args);
SkillUtils.ApplyNativeKill(entity, info);
return HookResult.Continue;
}
}

View file

@ -6,7 +6,6 @@ using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
using CounterStrikeSharp.API.Modules.UserMessages;
using CounterStrikeSharp.API.Modules.Utils;
using RayTraceAPI;
using src.player.skills;
using src.utils;
using System.Collections.Concurrent;
@ -108,6 +107,7 @@ namespace src.player
TryUnhook(() => VirtualFunctions.CCSPlayer_ItemServices_CanAcquireFunc.Unhook(OnWeaponCanAcquire, HookMode.Pre));
TryUnhook(() => Instance.UnhookUserMessage(208, PlayerMakeSound));
TryUnhook(() => Instance.RemoveListener<CheckTransmit>(CheckTransmit));
TryUnhook(NoRecoil.RestoreSpread);
}
private static void TryUnhook(Action unhook)
@ -507,6 +507,7 @@ namespace src.player
private static void OnTick()
{
ReportStall();
NoRecoil.RestoreSpread();
long perfStart = PerfLog.Start();
lock (setLock)
@ -794,7 +795,7 @@ namespace src.player
Vector eyePos = new(pawn.AbsOrigin.X, pawn.AbsOrigin.Y, pawn.AbsOrigin.Z + pawn.ViewOffset.Z);
Vector endPos = eyePos + SkillUtils.GetForwardVector(pawn.EyeAngles) * 80;
ulong mask = (ulong)(InteractionLayers.MASK_WORLD_ONLY | InteractionLayers.Player | InteractionLayers.NPC);
ulong mask = RayTrace.WorldOnlyMask | (ulong)(Contents.Player | Contents.Npc);
ulong contents = 0;
var result = RayTrace.TraceShape(player, eyePos, endPos, mask, contents);

View file

@ -42,6 +42,7 @@ namespace src.player
private static void OnMapStart(string mapName)
{
skills.NoRecoil.ForgetSpread();
PlayerManager.SetServerActive(true);
Instance.GameRules = null;
Event.OnMapChange();
@ -50,6 +51,8 @@ namespace src.player
private static void OnMapEnd()
{
skills.NoRecoil.RestoreSpread();
skills.NoRecoil.ForgetSpread();
PerfLog.Info("===== MAP END (clean map change) =====");
Debug.WriteToDebug("===== MAP END (clean map change) =====");
BotManager.Stop();

View file

@ -7,7 +7,6 @@ using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
using CounterStrikeSharp.API.Modules.UserMessages;
using CounterStrikeSharp.API.Modules.Utils;
using RayTraceAPI;
using src.player.skills;
using src.utils;
using System.Collections.Concurrent;
@ -98,14 +97,14 @@ namespace src.player
{
lock (setLock)
{
bool isWarmup = Instance.GameRules == null || Instance.GameRules.WarmupPeriod == true;
bool isWarmup = IsWarmupPeriod();
isTransmitRegistered = false;
setSkillRetries = 0;
SkillUtils.ClearKillCredits();
SkillUtils.ClearCurses();
Instance.AddTimer(.1f, () => DisableAll(), CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
foreach (var player in Utilities.GetPlayers().Where(p => p != null && p.IsValid && !p.IsHLTV && p.Team is CsTeam.CounterTerrorist or CsTeam.Terrorist))
foreach (var player in Utilities.GetPlayers().Where(p => p != null && p.IsValid && !p.IsHLTV && InPlayingTeam(p)))
{
var skillPlayer = PlayerManager.GetPlayerByIndex(player!.Index);
if (skillPlayer == null) continue;
@ -115,18 +114,21 @@ namespace src.player
Instance.RemoveListener<CheckTransmit>(CheckTransmit);
int freezetime = SkillUtils.CvarValue("mp_freezetime", 0);
freezeTimeEnd = DateTime.Now.AddSeconds(freezetime + (Instance?.GameRules?.TeamIntroPeriod == true ? 7 : 0));
int introDelay = IsTeamIntroPeriod() ? 7 : 0;
freezeTimeEnd = DateTime.Now.AddSeconds(freezetime + introDelay);
setSkillTimer?.Kill();
if (isWarmup)
{
setSkillTimer = Instance?.AddTimer(1f, SetSkill, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
Debug.WriteToDebug("RoundStart: warmup, SetSkill scheduled in 1.00s.", DebugCategory.Round);
return HookResult.Continue;
}
float timeToDraw = (Instance?.GameRules?.TeamIntroPeriod == true ? 7 : 0) + Math.Max(freezetime - Config.LoadedConfig.SkillTimeBeforeStart, 0) + .3f;
float timeToDraw = introDelay + Math.Max(freezetime - Config.LoadedConfig.SkillTimeBeforeStart, 0) + .3f;
setSkillTimer = Instance?.AddTimer(timeToDraw, SetSkill, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
Debug.WriteToDebug($"RoundStart: SetSkill scheduled in {timeToDraw:F2}s (freezetime={freezetime}, intro={introDelay}, timer={(setSkillTimer == null ? "null" : "ok")}).", DebugCategory.Round);
return HookResult.Continue;
}
}
@ -293,7 +295,22 @@ namespace src.player
private static void SetSkill()
{
long perfStart = PerfLog.Start();
SetSkillCore();
try
{
SetSkillCore();
}
catch (Exception ex)
{
Debug.WriteToDebug($"SetSkill threw: {ex}", DebugCategory.Skill);
if (setSkillRetries < MaxSetSkillRetries)
{
setSkillRetries++;
setSkillTimer = Instance?.AddTimer(.5f, SetSkill, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
}
}
PerfLog.End("SetSkill total", perfStart, 2.0);
}
@ -472,10 +489,46 @@ namespace src.player
private static int CountConnectedPlayers()
{
try { return Utilities.GetPlayers().Count(p => p != null && p.IsValid && !p.IsHLTV); }
try
{
return Utilities.GetPlayers().Count(p =>
{
if (p == null || !p.IsValid || p.IsHLTV) return false;
var pawn = p.PlayerPawn?.Value;
return pawn != null && pawn.IsValid;
});
}
catch { return 0; }
}
private static bool InPlayingTeam(CCSPlayerController player)
{
try { return player.Team is CsTeam.CounterTerrorist or CsTeam.Terrorist; }
catch { return false; }
}
private static bool IsWarmupPeriod()
{
try
{
var gameRules = Instance?.GameRules;
if (gameRules == null || gameRules.Handle == IntPtr.Zero) return true;
return gameRules.WarmupPeriod;
}
catch { return true; }
}
private static bool IsTeamIntroPeriod()
{
try
{
var gameRules = Instance?.GameRules;
if (gameRules == null || gameRules.Handle == IntPtr.Zero) return false;
return gameRules.TeamIntroPeriod;
}
catch { return false; }
}
private static void SetSkillCore()
{
setSkillTimer = null;
@ -484,7 +537,7 @@ namespace src.player
if (Instance == null) return;
// GameRules null = not ready; keep polling so skills land right after warmup ends.
if (Instance.GameRules == null || Instance.GameRules.WarmupPeriod == true)
if (IsWarmupPeriod())
{
if (++gameRulesPolls % 10 == 0)
Debug.WriteToDebug($"SetSkill waiting: gameRules={(Instance.GameRules == null ? "null" : "warmup")}, poll {gameRulesPolls}.", DebugCategory.Skill);

View file

@ -4,7 +4,6 @@ using CounterStrikeSharp.API.Modules.Commands.Targeting;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
using CounterStrikeSharp.API.Modules.Timers;
using CounterStrikeSharp.API.Modules.Utils;
using RayTraceAPI;
using src.utils;
using System.Collections.Concurrent;
using System.Drawing;
@ -341,7 +340,7 @@ namespace src.player.skills
Vector eyePos = new(playerPawn.AbsOrigin.X, playerPawn.AbsOrigin.Y, playerPawn.AbsOrigin.Z + playerPawn.ViewOffset.Z);
Vector endPos = eyePos + SkillUtils.GetForwardVector(playerPawn.V_angle) * 4096f;
ulong mask = (ulong)(InteractionLayers.Solid | InteractionLayers.Window | InteractionLayers.PassBullets);
ulong mask = (ulong)(Contents.Solid | Contents.Window | Contents.PassBullets);
var result = RayTrace.TraceShape(player, eyePos, endPos, mask);
if (result.HasValue && result.Value.HitWorld(out _))
@ -370,7 +369,7 @@ namespace src.player.skills
Vector endPos = cameraVector + SkillUtils.GetForwardVector(new QAngle(0, playerPawn.V_angle.Y + 180, 0)) * -5;
cameraVector += SkillUtils.GetForwardVector(new QAngle(0, playerPawn.V_angle.Y + 180, 0)) * 5;
ulong mask = (ulong)(InteractionLayers.Solid | InteractionLayers.Window | InteractionLayers.PassBullets);
ulong mask = (ulong)(Contents.Solid | Contents.Window | Contents.PassBullets);
var result = RayTrace.TraceShape(player, cameraVector, endPos, mask);
if (result.HasValue && result.Value.HitWorld(out _))

View file

@ -29,10 +29,16 @@ namespace src.player.skills
}
public static void BombPlanted(EventBombPlanted _)
{
plantedC4 = FindPlantedC4();
}
private static Vector? FindPlantedC4()
{
var plantedBomb = Utilities.FindAllEntitiesByDesignerName<CPlantedC4>("planted_c4").FirstOrDefault();
if (plantedBomb == null || !plantedBomb.IsValid || plantedBomb.AbsOrigin == null) return;
plantedC4 = new(plantedBomb.AbsOrigin.X, plantedBomb.AbsOrigin.Y, plantedBomb.AbsOrigin.Z);
if (plantedBomb == null || !plantedBomb.IsValid || plantedBomb.AbsOrigin == null) return null;
if (!plantedBomb.BombTicking || plantedBomb.BombDefused) return null;
return new(plantedBomb.AbsOrigin.X, plantedBomb.AbsOrigin.Y, plantedBomb.AbsOrigin.Z);
}
private static void RemoveBomb()
@ -49,14 +55,21 @@ namespace src.player.skills
if (lastTick == Server.TickCount) return;
var player = PlayerManager.GetPlayerEvent(@event.Userid);
if (player == null || !player.IsValid || plantedC4 == null) return;
var pos = new Vector(@event.X, @event.Y, @event.Z);
if (player == null || !player.IsValid) return;
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo == null || playerInfo.Skill != skillName) return;
if (SkillUtils.Distance(plantedC4, pos) >= 8)
plantedC4 ??= FindPlantedC4();
if (plantedC4 == null) return;
var pawn = player.PlayerPawn.Value;
if (pawn == null || !pawn.IsValid || pawn.AbsOrigin == null) return;
var eyePos = new Vector(pawn.AbsOrigin.X, pawn.AbsOrigin.Y, pawn.AbsOrigin.Z + pawn.ViewOffset.Z);
var impactPos = new Vector(@event.X, @event.Y, @event.Z);
if (DistanceToSegment(plantedC4, eyePos, impactPos) >= 8)
return;
lastTick = Server.TickCount;
@ -71,6 +84,16 @@ namespace src.player.skills
Localization.PrintTranslationToChatAll($" {ChatColors.Gold}{{0}}: {ChatColors.Red}{bombHealth}{ChatColors.Gold}/{ChatColors.Green}{maxBombHealth}", ["fragilebomb_bomb_health"]);
}
private static float DistanceToSegment(Vector point, Vector start, Vector end)
{
float lineX = end.X - start.X, lineY = end.Y - start.Y, lineZ = end.Z - start.Z;
float lengthSquared = lineX * lineX + lineY * lineY + lineZ * lineZ;
float t = lengthSquared <= .0001f ? 0f : Math.Clamp(((point.X - start.X) * lineX + (point.Y - start.Y) * lineY + (point.Z - start.Z) * lineZ) / lengthSquared, 0f, 1f);
float dx = start.X + lineX * t - point.X, dy = start.Y + lineY * t - point.Y, dz = start.Z + lineZ * t - point.Z;
return MathF.Sqrt(dx * dx + dy * dy + dz * dz);
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#5d00ff", CsTeam onlyTeam = CsTeam.CounterTerrorist, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = 1, Rarity rarity = Rarity.Common, int maxBombHealth = 1000) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public int MaxBombHealth { get; set; } = maxBombHealth;

View file

@ -15,6 +15,7 @@ namespace src.player.skills
private const string victimSound = "Player.DamageBody.Victim";
private const string heavyHitSound = "Weapon_Knife.Hit.Heavy.Flesh";
private const string lightHitSound = "Weapon_Knife.Hit.Light.Flesh";
private const string tracerParticle = "particles/weapons/cs_weapon_fx/weapon_tracers_rifle_wisp.vpcf";
private static bool hooked = false;
private const int actionCode = 503;
@ -30,6 +31,7 @@ namespace src.player.skills
public static void LoadSkill()
{
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
Instance.AddToManifest(tracerParticle);
}
public static void NewRound()
@ -113,6 +115,9 @@ namespace src.player.skills
if (result == null || !result.HasValue)
return;
if (result.Value.Distance() > 70)
SkillUtils.CreateTracer(player, tracerParticle, result.Value);
if (!result.Value.HitPlayer(out CCSPlayerController? target) || target == null)
return;

View file

@ -10,10 +10,12 @@ namespace src.player.skills
public class LongZeus : ISkill
{
private const Skills skillName = Skills.LongZeus;
private const string shockParticle = "particles/blood_impact/impact_taser_bodyfx.vpcf";
public static void LoadSkill()
{
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
Instance.AddToManifest(shockParticle);
}
public unsafe static void WeaponFire(EventWeaponFire @event)
@ -40,7 +42,12 @@ namespace src.player.skills
if (target.Handle == player.Handle) return;
if (!SkillsInfo.GetValue<bool>(skillName, "friendlyFire") && player.Team == target.Team) return;
SkillUtils.TakeHealth(target.PlayerPawn.Value, 9999, player, KillfeedIcons.Taser);
var targetPawn = target.PlayerPawn.Value;
if (targetPawn == null || !targetPawn.IsValid) return;
EntityManager.CreateEntityParticle(player.Index, shockParticle, targetPawn);
SkillUtils.RegisterNativeKill(targetPawn, pawn, activeWeapon, DamageTypes_t.DMG_SHOCK);
SkillUtils.TakeHealth(targetPawn, 9999);
}
public static void EnableSkill(CCSPlayerController player)

View file

@ -17,6 +17,27 @@ namespace src.player.skills
private static readonly ConcurrentDictionary<uint, byte> holders = [];
private static readonly ConcurrentDictionary<uint, string> originalNoSpreadValues = [];
private sealed class SpreadPatch(CCSWeaponBaseVData vdata)
{
public readonly CCSWeaponBaseVData VData = vdata;
public readonly CFiringModeFloat[] Fields =
[
vdata.Spread, vdata.InaccuracyCrouch, vdata.InaccuracyStand, vdata.InaccuracyJump,
vdata.InaccuracyLand, vdata.InaccuracyLadder, vdata.InaccuracyFire, vdata.InaccuracyMove
];
public readonly float[] Values = new float[16];
public int Count;
public float JumpInitial;
public float JumpApex;
public float Reload;
public bool Active;
}
private static readonly Dictionary<nint, SpreadPatch> spreadPatches = [];
private static readonly List<SpreadPatch> activePatches = [];
private static readonly Dictionary<nint, CCSWeaponBaseVData> wantedVData = [];
private static readonly HashSet<nint> blockedVData = [];
public static void LoadSkill()
{
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
@ -83,6 +104,106 @@ namespace src.player.skills
catch { }
}
public static void OnTick()
{
if (holders.IsEmpty) return;
wantedVData.Clear();
blockedVData.Clear();
foreach (var player in PlayerManager.GetTickPlayers())
{
if (player == null || !player.IsValid || !player.PawnIsAlive) continue;
var vdata = GetActiveWeaponVData(player);
if (vdata == null) continue;
bool isHolder = holders.ContainsKey(player.Index) && PlayerManager.GetPlayerByIndex(player.Index)?.Skill == skillName;
if (isHolder)
wantedVData[vdata.Handle] = vdata;
else
blockedVData.Add(vdata.Handle);
}
foreach (var (handle, vdata) in wantedVData)
{
if (!blockedVData.Contains(handle))
PatchSpread(vdata);
}
}
private static CCSWeaponBaseVData? GetActiveWeaponVData(CCSPlayerController player)
{
var weapon = player.PlayerPawn.Value?.WeaponServices?.ActiveWeapon.Value;
if (weapon == null || !weapon.IsValid) return null;
var vdata = weapon.As<CCSWeaponBase>().VData;
return vdata == null || vdata.Handle == IntPtr.Zero ? null : vdata;
}
private static void PatchSpread(CCSWeaponBaseVData vdata)
{
if (!spreadPatches.TryGetValue(vdata.Handle, out var patch))
{
patch = new SpreadPatch(vdata);
spreadPatches[vdata.Handle] = patch;
}
if (patch.Active) return;
int index = 0;
foreach (var field in patch.Fields)
{
var values = field.Values;
for (int i = 0; i < values.Length && index < patch.Values.Length; i++)
{
patch.Values[index++] = values[i];
values[i] = 0f;
}
}
patch.Count = index;
patch.JumpInitial = vdata.InaccuracyJumpInitial;
patch.JumpApex = vdata.InaccuracyJumpApex;
patch.Reload = vdata.InaccuracyReload;
vdata.InaccuracyJumpInitial = 0f;
vdata.InaccuracyJumpApex = 0f;
vdata.InaccuracyReload = 0f;
patch.Active = true;
activePatches.Add(patch);
}
public static void RestoreSpread()
{
if (activePatches.Count == 0) return;
foreach (var patch in activePatches)
{
int index = 0;
foreach (var field in patch.Fields)
{
var values = field.Values;
for (int i = 0; i < values.Length && index < patch.Count; i++)
values[i] = patch.Values[index++];
}
patch.VData.InaccuracyJumpInitial = patch.JumpInitial;
patch.VData.InaccuracyJumpApex = patch.JumpApex;
patch.VData.InaccuracyReload = patch.Reload;
patch.Active = false;
}
activePatches.Clear();
}
public static void ForgetSpread()
{
activePatches.Clear();
spreadPatches.Clear();
}
public static void WeaponFire(EventWeaponFire @event)
{
var player = PlayerManager.GetPlayerEvent(@event.Userid);

View file

@ -1,8 +1,7 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using jRandomSkills.src.utils;
using RayTraceAPI;
using src.utils;
using System.Collections.Concurrent;
using System.Drawing;
@ -22,7 +21,7 @@ namespace src.player.skills
private static readonly List<Flight> flights = [];
private static readonly object flightLock = new();
private static readonly Color tracerColor = Color.FromArgb(170, 255, 226, 140);
private static readonly ulong worldOnlyMask = (ulong)InteractionLayers.MASK_WORLD_ONLY;
private static readonly ulong worldOnlyMask = RayTrace.WorldOnlyMask;
public static void LoadSkill()
{

View file

@ -1,7 +1,6 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using RayTraceAPI;
using src.utils;
using System.Collections.Concurrent;
using System.Drawing;
@ -104,19 +103,7 @@ namespace src.player.skills
private static void DestroyWire(uint beamIndex)
{
var beam = Utilities.GetEntityFromIndex<CBeam>((int)beamIndex);
if (beam != null && beam.IsValid)
{
beam.Width = 0;
beam.EndWidth = 0;
beam.Render = Color.FromArgb(0, 0, 0, 0);
Utilities.SetStateChanged(beam, "CBeam", "m_fWidth");
Utilities.SetStateChanged(beam, "CBeam", "m_fEndWidth");
Utilities.SetStateChanged(beam, "CBaseModelEntity", "m_clrRender");
}
EntityManager.DestroyEntity(beamIndex);
EntityManager.DestroyBeam(beamIndex);
}
public static void UseSkill(CCSPlayerController player)
@ -175,7 +162,7 @@ namespace src.player.skills
Vector rightDir = SkillUtils.GetForwardVector(new QAngle(0, yaw - 90, 0));
Vector leftDir = SkillUtils.GetForwardVector(new QAngle(0, yaw + 90, 0));
ulong wallMask = (ulong)InteractionLayers.MASK_WORLD_ONLY;
ulong wallMask = RayTrace.WorldOnlyMask;
var rightHit = RayTrace.TraceShape(player, origin, origin + rightDir * maxDistance, wallMask, 0);
var leftHit = RayTrace.TraceShape(player, origin, origin + leftDir * maxDistance, wallMask, 0);