Skip to content
This repository was archived by the owner on Jul 8, 2025. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 86 additions & 28 deletions EXILED/Exiled.Events/EventArgs/Player/ShotEventArgs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,42 +7,61 @@

namespace Exiled.Events.EventArgs.Player
{
using System;

using API.Features;
using Exiled.API.Features.Items;
using Interfaces;
using InventorySystem.Items.Firearms.Modules;
using InventorySystem.Items.Firearms.Modules.Misc;
using UnityEngine;

/// <summary>
/// Contains all information after a player has fired a weapon.
/// </summary>
public class ShotEventArgs : IPlayerEvent, IFirearmEvent
public class ShotEventArgs : IFirearmEvent
{
/// <summary>
/// Initializes a new instance of the <see cref="ShotEventArgs"/> class.
/// </summary>
/// <param name="hitscanResult">Hitscan result that contains all hit information.</param>
/// <param name="hitregModule">Hitreg module that calculated the shot.</param>
/// <param name="hitInfo">Raycast hit info.</param>
/// <param name="firearm">The firearm used.</param>
/// <param name="destructible">The IDestructible that was hit. Can be null.</param>
/// <param name="damage"><inheritdoc cref="Damage"/></param>
public ShotEventArgs(HitscanHitregModuleBase hitregModule, RaycastHit hitInfo, InventorySystem.Items.Firearms.Firearm firearm, IDestructible destructible, float damage)
public ShotEventArgs(HitscanResult hitscanResult, HitscanHitregModuleBase hitregModule)
{
HitregModule = hitregModule;
RaycastHit = hitInfo;
Destructible = destructible;
Firearm = Item.Get<Firearm>(firearm);
Damage = damage;

ShotResult = hitscanResult;
Firearm = Item.Get<Firearm>(HitregModule.Firearm);
Player = Firearm.Owner;

if (Destructible is HitboxIdentity hitboxIdentity)
if (ShotResult.Destructibles.Count != 0)
{
Hitbox = hitboxIdentity;
Target = Player.Get(Hitbox.TargetHub);
DestructibleHitPair firstPair = ShotResult.Destructibles[0];
RaycastHit = firstPair.Hit;
Destructible = firstPair.Destructible;
Hitbox = Destructible as HitboxIdentity;
Target = Player.Get(Hitbox?.TargetHub);
}
else
{
if (ShotResult.Obstacles.Count > 0)
RaycastHit = ShotResult.Obstacles[0].Hit;
else
RaycastHit = ApproximateHit();
}

foreach (DestructibleDamageRecord damageRecord in ShotResult.DamagedDestructibles)
{
Damage += damageRecord.AppliedDamage;
}
}

/// <summary>
/// Gets the HitscanResult object which represents the result of weapon shot raycasts and calculations.
/// Each weapon controls how it is generated to determine the specific behavior. You can change that behaviour by modifying that object.
/// </summary>
/// <seealso cref="ResetShotResult"/>
public HitscanResult ShotResult { get; }

/// <summary>
/// Gets the player who fired the shot.
/// </summary>
Expand All @@ -62,48 +81,87 @@ public ShotEventArgs(HitscanHitregModuleBase hitregModule, RaycastHit hitInfo, I
public HitscanHitregModuleBase HitregModule { get; }

/// <summary>
/// Gets the raycast info.
/// Gets the hit info of the first hit.
/// </summary>
public RaycastHit RaycastHit { get; }

/// <summary>
/// Gets the bullet travel distance.
/// Gets the bullet travel distance of the first bullet.
/// </summary>
public float Distance => RaycastHit.distance;

/// <summary>
/// Gets the position of the hit.
/// Gets the position of the first hit.
/// </summary>
public Vector3 Position => RaycastHit.point;

/// <summary>
/// Gets the firearm base damage at the hit distance. Actual inflicted damage may vary.
/// Gets the direction of the first bullet.
/// </summary>
public Vector3 Direction => Position - Player.CameraTransform.position;

/// <summary>
/// Gets or sets a value indicating whether the shot can deal damage to <see cref="IDestructible"/> objects such as players or glass windows.
/// Damage can be controlled on per instance basis using <see cref="ShotResult"/>.
/// </summary>
public bool CanDamageDestructibles { get; set; } = true;

/// <summary>
/// Gets or sets a value indicating whether the shot can deal damage to obstacles such as walls - spawning impact effects, or doors - breaking them in case of <see cref="ItemType.ParticleDisruptor"/>.
/// Damage can be controlled on per instance basis using <see cref="ShotResult"/>.
/// </summary>
public bool CanDamageObstacles { get; set; } = true;

/// <summary>
/// Gets the sum of the damage that is going to be dealt by the shot if <see cref="ShotResult"/> remains unchanged.
/// </summary>
public float Damage { get; }

/// <summary>
/// Gets the target player. Can be null.
/// Gets or sets a value indicating whether the shot can produce impact effects (e.g. bullet holes).
/// </summary>
public Player Target { get; }
[Obsolete("Use CanDamageObstacles instead.")]
public bool CanSpawnImpactEffects { get => CanDamageObstacles; set => CanDamageObstacles = value; }

/// <summary>
/// Gets the <see cref="HitboxIdentity"/> component of the target player that was hit. Can be null.
/// Gets or sets a value indicating whether the shot can deal damage.
/// </summary>
public HitboxIdentity Hitbox { get; }
[Obsolete("Use CanDamageDestructibles instead.")]
public bool CanHurt { get => CanDamageDestructibles; set => CanDamageDestructibles = value; }

/// <summary>
/// Gets the <see cref="IDestructible"/> component of the hit collider. Can be null.
/// Gets the <see cref="IDestructible" /> component of the first hit collider. Can be null.
/// </summary>
public IDestructible Destructible { get; }

/// <summary>
/// Gets or sets a value indicating whether the shot can deal damage.
/// Gets the <see cref="HitboxIdentity" /> component of the first target player that was hit. Can be null.
/// </summary>
public bool CanHurt { get; set; } = true;
public HitboxIdentity Hitbox { get; }

/// <summary>
/// Gets or sets a value indicating whether the shot can produce impact effects (e.g. bullet holes).
/// Gets the first target player. Can be null.
/// </summary>
public Player Target { get; }

/// <summary>
/// Reset the shot result generated by the firearm, allowing you to generate your own result.
/// </summary>
public bool CanSpawnImpactEffects { get; set; } = true;
public void ResetShotResult()
{
ShotResult.Clear();
}

private RaycastHit ApproximateHit()
{
Ray ray = new(Player.CameraTransform.position, Player.CameraTransform.forward);
float maxDistance = HitregModule.DamageFalloffDistance + HitregModule.FullDamageDistance;
return new RaycastHit
{
distance = maxDistance,
point = ray.GetPoint(maxDistance),
normal = -ray.direction,
};
}
}
}
}
146 changes: 27 additions & 119 deletions EXILED/Exiled.Events/Patches/Events/Player/Shot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,145 +7,53 @@

namespace Exiled.Events.Patches.Events.Player
{
#pragma warning disable SA1402
#pragma warning disable SA1649
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using System;

using API.Features.Pools;
using Exiled.API.Features;
using Exiled.Events.Attributes;
using Exiled.Events.EventArgs.Player;
using HarmonyLib;
using InventorySystem.Items.Firearms.Modules;
using InventorySystem.Items.Firearms.Modules.Misc;
using UnityEngine;

using static HarmonyLib.AccessTools;

/// <summary>
/// Patches <see cref="HitscanHitregModuleBase.ServerApplyDestructibleDamage" />.
/// Patches <see cref="HitscanHitregModuleBase.ServerApplyDamage" />.
/// Adds the <see cref="Handlers.Player.Shot" /> event.
/// </summary>
[EventPatch(typeof(Handlers.Player), nameof(Handlers.Player.Shot))]
[HarmonyPatch(typeof(HitscanHitregModuleBase), nameof(HitscanHitregModuleBase.ServerApplyDestructibleDamage))]
internal static class ShotTarget
[HarmonyPatch(typeof(HitscanHitregModuleBase), nameof(HitscanHitregModuleBase.ServerApplyDamage))]
internal static class Shot
{
private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, ILGenerator generator)
// public virtual void ServerApplyDamage(HitscanResult result)
#pragma warning disable SA1313
private static bool Prefix(HitscanResult result, HitscanHitregModuleBase __instance)
#pragma warning restore SA1313
{
List<CodeInstruction> newInstructions = ListPool<CodeInstruction>.Pool.Get(instructions);

int index = newInstructions.FindIndex(i => i.opcode == OpCodes.Ldloc_2);

Label continueLabel = generator.DefineLabel();
ShotEventArgs args = new(result, __instance);
Handlers.Player.OnShot(args);

LocalBuilder ev = generator.DeclareLocal(typeof(ShotEventArgs));

newInstructions.InsertRange(
index,
new[]
try
{
if (args.CanDamageDestructibles)
{
// this
new(OpCodes.Ldarg_0),

// target.Raycast.Hit
new(OpCodes.Ldarg_1),
new(OpCodes.Ldfld, Field(typeof(DestructibleHitPair), nameof(DestructibleHitPair.Raycast))),
new(OpCodes.Ldfld, Field(typeof(HitRayPair), nameof(HitRayPair.Hit))),

// this.Firearm
new(OpCodes.Ldarg_0),
new(OpCodes.Callvirt, PropertyGetter(typeof(HitscanHitregModuleBase), nameof(HitscanHitregModuleBase.Firearm))),

// destructible
new(OpCodes.Ldloc_2),

// damage
new(OpCodes.Ldloc_0),
foreach (DestructibleHitPair destructible in result.Destructibles)
__instance.ServerApplyDestructibleDamage(destructible, result);
}

// ShotEventArgs ev = new ShotEventArgs(this, hitInfo, firearm, component, damage);
new(OpCodes.Newobj, GetDeclaredConstructors(typeof(ShotEventArgs))[0]),
new(OpCodes.Dup),
new(OpCodes.Dup),
new(OpCodes.Stloc_S, ev.LocalIndex),

new(OpCodes.Call, Method(typeof(Handlers.Player), nameof(Handlers.Player.OnShot))),

// if (!ev.CanHurt) num = 0;
new(OpCodes.Callvirt, PropertyGetter(typeof(ShotEventArgs), nameof(ShotEventArgs.CanHurt))),
new(OpCodes.Brtrue, continueLabel),

new(OpCodes.Ldc_I4_0),
new(OpCodes.Stloc_0),

new CodeInstruction(OpCodes.Nop).WithLabels(continueLabel),
});

index = newInstructions.FindLastIndex(i => i.opcode == OpCodes.Ldarg_0);
Label returnLabel = generator.DefineLabel();

newInstructions.InsertRange(
index,
new CodeInstruction[]
if (args.CanDamageObstacles)
{
// if (!ev.CanSpawnImpactEffects) return;
new(OpCodes.Ldloc_S, ev.LocalIndex),
new(OpCodes.Callvirt, PropertyGetter(typeof(ShotEventArgs), nameof(ShotEventArgs.CanSpawnImpactEffects))),
new(OpCodes.Brfalse_S, returnLabel),
});

newInstructions[newInstructions.Count - 1].labels.Add(returnLabel);

for (int z = 0; z < newInstructions.Count; z++)
yield return newInstructions[z];
foreach (HitRayPair obstacle in result.Obstacles)
__instance.ServerApplyObstacleDamage(obstacle, result);
}

ListPool<CodeInstruction>.Pool.Return(newInstructions);
}
}

/// <summary>
/// Patches <see cref="HitscanHitregModuleBase.ServerAppendPrescan" />.
/// Adds the <see cref="Handlers.Player.Shot" /> event.
/// </summary>
[HarmonyPatch(typeof(HitscanHitregModuleBase), nameof(HitscanHitregModuleBase.ServerAppendPrescan))]
internal static class ShotMiss
{
private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
List<CodeInstruction> newInstructions = ListPool<CodeInstruction>.Pool.Get(instructions);

CodeInstruction[] eventInstructions =
__instance.ServerSendAllIndicators(result);
}
catch (Exception e)
{
// hitInfo
new(OpCodes.Ldloc_1),

// this.Firearm
new(OpCodes.Ldarg_0),
new(OpCodes.Callvirt, PropertyGetter(typeof(HitscanHitregModuleBase), nameof(HitscanHitregModuleBase.Firearm))),

// (IDestructible)null
new(OpCodes.Ldnull),

// 0f
new(OpCodes.Ldc_R4, 0f),

// ShotEventArgs = new(this, hitInfo, this.Firearm, (IDestructible)null, 0f)
new(OpCodes.Newobj, GetDeclaredConstructors(typeof(ShotEventArgs))[0]),

// Handlers.Player.OnShot(ev);
new(OpCodes.Call, Method(typeof(Handlers.Player), nameof(Handlers.Player.OnShot))),
};

int index = newInstructions.FindIndex(x => x.opcode == OpCodes.Brtrue_S) + 1;
newInstructions.InsertRange(index, new CodeInstruction[] { new(OpCodes.Ldarg_0), }.AddRangeToArray(eventInstructions));

index = newInstructions.FindLastIndex(i => i.opcode == OpCodes.Ldarg_2);
newInstructions.InsertRange(index, new[] { new CodeInstruction(OpCodes.Ldarg_0).MoveLabelsFrom(newInstructions[index]) }.AddRangeToArray(eventInstructions));

for (int z = 0; z < newInstructions.Count; z++)
yield return newInstructions[z];
Log.Error($"Error in {nameof(Shot)} event - invalid modification of ev.{nameof(ShotEventArgs.ShotResult)} has caused an exception: {e}");
}

ListPool<CodeInstruction>.Pool.Return(newInstructions);
return false;
}
}
}