Skip to content
Merged
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
83 changes: 40 additions & 43 deletions EXILED/Exiled.Events/EventArgs/Player/ShotEventArgs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,93 +19,90 @@ namespace Exiled.Events.EventArgs.Player
public class ShotEventArgs : IPlayerEvent, IFirearmEvent
{
/// <summary>
/// Initializes a new instance of the <see cref="ShotEventArgs" /> class.
/// Initializes a new instance of the <see cref="ShotEventArgs"/> class.
/// </summary>
/// <param name="instance">
/// The <see cref="HitscanHitregModuleBase"/> instance.
/// </param>
/// <param name="firearm">
/// <inheritdoc cref="Firearm"/>
/// </param>
/// <param name="ray">
/// <inheritdoc cref="Ray" />
/// </param>
/// <param name="maxDistance">
/// <inheritdoc cref="MaxDistance" />
/// </param>
public ShotEventArgs(HitscanHitregModuleBase instance, InventorySystem.Items.Firearms.Firearm firearm, Ray ray, float maxDistance)
/// <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>
public ShotEventArgs(HitscanHitregModuleBase hitregModule, RaycastHit hitInfo, InventorySystem.Items.Firearms.Firearm firearm, IDestructible destructible)
{
Player = Player.Get(firearm.Owner);
HitregModule = hitregModule;
RaycastHit = hitInfo;
Destructible = destructible;
Firearm = Item.Get<Firearm>(firearm);
MaxDistance = maxDistance;

if (!Physics.Raycast(ray, out RaycastHit hitInfo, maxDistance, HitscanHitregModuleBase.HitregMask))
return;

Distance = hitInfo.distance;
Position = hitInfo.point;
Damage = hitInfo.collider.TryGetComponent(out IDestructible component) ? instance.DamageAtDistance(hitInfo.distance) : 0f;
Destructible = component;
RaycastHit = hitInfo;
Player = Firearm.Owner;
Damage = Destructible is not null ? HitregModule.DamageAtDistance(hitInfo.distance) : 0f;

if (component is HitboxIdentity identity)
if (Destructible is HitboxIdentity hitboxIdentity)
{
Hitbox = identity;
Hitbox = hitboxIdentity;
Target = Player.Get(Hitbox.TargetHub);
}
}

/// <summary>
/// Gets the player who shot.
/// Gets the player who fired the shot.
/// </summary>
public Player Player { get; }

/// <summary>
/// Gets the firearm used to shoot.
/// Gets the firearm used to fire the shot.
/// </summary>
public Firearm Firearm { get; }

/// <inheritdoc/>
public Item Item => Firearm;

/// <summary>
/// Gets the max distance of the shot.
/// Gets the firearm hitreg module responsible for the shot.
/// </summary>
public float MaxDistance { get; }
public HitscanHitregModuleBase HitregModule { get; }

/// <summary>
/// Gets the shot distance. Can be <c>0.0f</c> if the raycast doesn't hit collider.
/// Gets the raycast info.
/// </summary>
public float Distance { get; }
public RaycastHit RaycastHit { get; }

/// <summary>
/// Gets the shot position. Can be <see langword="null"/> if the raycast doesn't hit collider.
/// Gets the bullet travel distance.
/// </summary>
public Vector3 Position { get; }
public float Distance => RaycastHit.distance;

/// <summary>
/// Gets the <see cref="IDestructible"/> component of the hit collider. Can be <see langword="null"/>.
/// Gets the position of the hit.
/// </summary>
public IDestructible Destructible { get; }
public Vector3 Position => RaycastHit.point;

/// <summary>
/// Gets the inflicted damage.
/// Gets the firearm base damage at the hit distance. Actual inflicted damage may vary.
/// </summary>
public float Damage { get; }

/// <summary>
/// Gets the raycast result.
/// Gets the target player. Can be null.
/// </summary>
public RaycastHit RaycastHit { get; }
public Player Target { get; }

/// <summary>
/// Gets the target of the shot. Can be <see langword="null"/>.
/// Gets the <see cref="HitboxIdentity"/> component of the target player that was hit. Can be null.
/// </summary>
public Player Target { get; }
public HitboxIdentity Hitbox { get; }

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

/// <summary>
/// Gets or sets a value indicating whether the shot can deal damage.
/// </summary>
public bool CanHurt { get; set; } = true;

/// <summary>
/// Gets or sets a value indicating whether the shot can produce impact effects (e.g. bullet holes).
/// </summary>
public bool CanSpawnImpactEffects { get; set; } = true;
}
}
105 changes: 90 additions & 15 deletions EXILED/Exiled.Events/Patches/Events/Player/Shot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
namespace Exiled.Events.Patches.Events.Player
{
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;

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

using static HarmonyLib.AccessTools;

Expand All @@ -26,37 +28,110 @@ namespace Exiled.Events.Patches.Events.Player
[HarmonyPatch(typeof(HitscanHitregModuleBase), nameof(HitscanHitregModuleBase.ServerPerformHitscan))]
internal static class Shot
{
private static void ProcessRaycastMiss(HitscanHitregModuleBase hitregModule, Ray ray, float maxDistance)
{
RaycastHit hit = new()
{
distance = maxDistance,
point = ray.GetPoint(maxDistance),
normal = -ray.direction,
};

var ev = new ShotEventArgs(hitregModule, hit, hitregModule.Firearm, null);
Handlers.Player.OnShot(ev);
}

private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, ILGenerator generator)
{
List<CodeInstruction> newInstructions = ListPool<CodeInstruction>.Pool.Get(instructions);

Label returnLabel = generator.DefineLabel();

int offset = 3;
int index = newInstructions.FindIndex(x => x.opcode == OpCodes.Ldarg_2) + offset;
/*
IL_0020: ldarg.1 // targetRay
IL_0021: ldloca.s hitInfo
IL_0023: ldloc.0 // maxDistance
IL_0024: ldsfld class CachedLayerMask InventorySystem.Items.Firearms.Modules.HitscanHitregModuleBase::HitregMask
IL_0029: call int32 CachedLayerMask::op_Implicit(class CachedLayerMask)
IL_002e: call bool [UnityEngine.PhysicsModule]UnityEngine.Physics::Raycast(valuetype [UnityEngine.CoreModule]UnityEngine.Ray, valuetype [UnityEngine.PhysicsModule]UnityEngine.RaycastHit&, float32, int32)
IL_0033: brtrue.s IL_0037
[] <= Here
*/
MethodInfo raycastMethod = Method(typeof(Physics), nameof(Physics.Raycast), new[] { typeof(Ray), typeof(RaycastHit).MakeByRefType(), typeof(float), typeof(int) });
int raycastFailIndex = newInstructions.FindIndex(i => i.Calls(raycastMethod)) + 2;

newInstructions.InsertRange(
index,
raycastFailIndex,
new CodeInstruction[]
{
// instance
// ProcessRaycastMiss(this, targetRay, maxDistance);
new(OpCodes.Ldarg_0),
new(OpCodes.Ldarg_1),
new(OpCodes.Ldloc_0),
new(OpCodes.Call, Method(typeof(Shot), nameof(ProcessRaycastMiss))),
});

// this.Firearm
new(OpCodes.Ldarg_0),
new(OpCodes.Callvirt, PropertyGetter(typeof(HitscanHitregModuleBase), nameof(HitscanHitregModuleBase.Firearm))),
/*
IL_0037: ldloca.s hitInfo
IL_0039: call instance class [UnityEngine.PhysicsModule]UnityEngine.Collider [UnityEngine.PhysicsModule]UnityEngine.RaycastHit::get_collider()
IL_003e: ldloca.s component
IL_0040: callvirt instance bool [UnityEngine.CoreModule]UnityEngine.Component::TryGetComponent<class IDestructible>(!!0/*class IDestructible* /&)
[] <= Here // This position is reached whether IDestructible is found or not.
IL_0045: brfalse.s IL_005e
*/
int destructibleGetIndex = newInstructions.FindIndex(i => i.operand is MethodInfo { Name: nameof(Component.TryGetComponent) }) + 1;

// ray
new(OpCodes.Ldarg_1),
Label continueLabel = generator.DefineLabel();

// maxDistance
new(OpCodes.Ldloc_0),
LocalBuilder ev = generator.DeclareLocal(typeof(ShotEventArgs));

// ShotEventArgs ev = new(HitscanHitregModuleBase, Firearm, Ray, float)
newInstructions.InsertRange(
destructibleGetIndex,
new[]
{
// var ev = new ShotEventArgs(this, hitInfo, firearm, component);
new(OpCodes.Ldarg_0), // this
new(OpCodes.Ldloc_1), // hitInfo
new(OpCodes.Ldarg_0), // this.Firearm
new(OpCodes.Callvirt, PropertyGetter(typeof(HitscanHitregModuleBase), nameof(HitscanHitregModuleBase.Firearm))),
new(OpCodes.Ldloc_2), // component
new(OpCodes.Newobj, GetDeclaredConstructors(typeof(ShotEventArgs))[0]),
new(OpCodes.Dup), // Leave ShotEventArgs on the stack
new(OpCodes.Stloc_S, ev.LocalIndex),

// Handlers.Player.OnShot(ev)
new(OpCodes.Dup), // Leave ShotEventArgs on the stack
new(OpCodes.Call, Method(typeof(Handlers.Player), nameof(Handlers.Player.OnShot))),

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

new(OpCodes.Ldloca_S, 1), // hitInfo address
new(OpCodes.Ldloc_0), // maxDistance
new(OpCodes.Call, PropertySetter(typeof(RaycastHit), nameof(RaycastHit.distance))), // hitInfo.distance = maxDistance

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

/*
[] <= Here
IL_0067: ldarg.0 // this
IL_0068: call instance class InventorySystem.Items.Firearms.Firearm InventorySystem.Items.Firearms.FirearmSubcomponentBase::get_Firearm()
IL_006d: ldloca.s module
IL_006f: ldc.i4.1
IL_0070: call bool InventorySystem.Items.Firearms.Modules.ModulesUtils::TryGetModule<class InventorySystem.Items.Firearms.Modules.ImpactEffectsModule>(class InventorySystem.Items.Firearms.Firearm, !!0/*class InventorySystem.Items.Firearms.Modules.ImpactEffectsModule* /&, bool)
IL_0075: brtrue.s IL_0079
*/
int impactEffectsIndex = newInstructions.FindIndex(i => i.operand is MethodInfo { Name: nameof(ModulesUtils.TryGetModule) }) - 4;
List<Label> impactEffectsLabels = newInstructions[impactEffectsIndex].ExtractLabels();
Label returnLabel = generator.DefineLabel();

newInstructions.InsertRange(
impactEffectsIndex,
new[]
{
// if (!ev.CanSpawnImpactEffects) return;
new CodeInstruction(OpCodes.Ldloc_S, ev.LocalIndex).WithLabels(impactEffectsLabels),
new(OpCodes.Callvirt, PropertyGetter(typeof(ShotEventArgs), nameof(ShotEventArgs.CanSpawnImpactEffects))),
new(OpCodes.Brfalse, returnLabel),
});

newInstructions[newInstructions.Count - 1].WithLabels(returnLabel);
Expand Down