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
66 changes: 66 additions & 0 deletions EXILED/Exiled.Events/EventArgs/Scp049/FinishingSenseEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// -----------------------------------------------------------------------
// <copyright file="FinishingSenseEventArgs.cs" company="ExMod Team">
// Copyright (c) ExMod Team. All rights reserved.
// Licensed under the CC BY-SA 3.0 license.
// </copyright>
// -----------------------------------------------------------------------

namespace Exiled.Events.EventArgs.Scp049
{
using Exiled.API.Features;
using Exiled.API.Features.Roles;
using Exiled.Events.EventArgs.Interfaces;

/// <summary>
/// Contains all information before SCP-049 finishes his sense ability.
/// </summary>
public class FinishingSenseEventArgs : IScp049Event, IDeniableEvent
{
/// <summary>
/// Initializes a new instance of the <see cref="FinishingSenseEventArgs"/> class.
/// </summary>
/// <param name="scp049">The SCP-049 instance triggering the event.
/// <inheritdoc cref="Player" />
/// </param>
/// <param name="target">The player targeted by SCP-049's Sense ability.
/// <inheritdoc cref="Player" />
/// </param>
/// <param name="cooldowntime">The time in seconds before the Sense ability can be used again.
/// <inheritdoc cref="double" />
/// </param>
/// <param name="isAllowed">Specifies whether the Sense effect is allowed to finish.
/// <inheritdoc cref="bool" />
/// </param>
public FinishingSenseEventArgs(ReferenceHub scp049, ReferenceHub target, double cooldowntime, bool isAllowed = true)
{
Player = Player.Get(scp049);
Scp049 = Player.Role.As<Scp049Role>();
Target = Player.Get(target);
IsAllowed = isAllowed;
CooldownTime = cooldowntime;
}

/// <inheritdoc/>
public Scp049Role Scp049 { get; }

/// <summary>
/// Gets the player who is controlling SCP-049.
/// </summary>
public Player Player { get; }

/// <summary>
/// Gets the player who is SCP-049's active target. Can be null.
/// </summary>
public Player Target { get; }

/// <summary>
/// Gets or sets the cooldown duration of the Sense ability.
/// </summary>
public double CooldownTime { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the server will finishing or not finishing 049 Sense Ability.
/// </summary>
public bool IsAllowed { get; set; }
}
}
13 changes: 12 additions & 1 deletion EXILED/Exiled.Events/Handlers/Scp049.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ public static class Scp049
/// </summary>
public static Event<ActivatingSenseEventArgs> ActivatingSense { get; set; } = new();

/// <summary>
/// Invoked before SCP-049 finish the good sense of the doctor ability.
/// </summary>
public static Event<FinishingSenseEventArgs> FinishingSense { get; set; } = new();

/// <summary>
/// Invoked before SCP-049 uses the call ability.
/// </summary>
Expand Down Expand Up @@ -60,6 +65,12 @@ public static class Scp049
/// <param name="ev">The <see cref="ActivatingSenseEventArgs" /> instance.</param>
public static void OnActivatingSense(ActivatingSenseEventArgs ev) => ActivatingSense.InvokeSafely(ev);

/// <summary>
/// Called before SCP-049 finish the good sense of the doctor ability.
/// </summary>
/// <param name="ev">The <see cref="FinishingSenseEventArgs" /> instance.</param>
public static void OnFinishingSense(FinishingSenseEventArgs ev) => FinishingSense.InvokeSafely(ev);

/// <summary>
/// Called before SCP-049 starts the call ability.
/// </summary>
Expand All @@ -72,4 +83,4 @@ public static class Scp049
/// <param name="ev">The <see cref="AttackingEventArgs"/> instance.</param>
public static void OnAttacking(AttackingEventArgs ev) => Attacking.InvokeSafely(ev);
}
}
}
258 changes: 258 additions & 0 deletions EXILED/Exiled.Events/Patches/Events/Scp049/FinishingSense.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
// -----------------------------------------------------------------------
// <copyright file="FinishingSense.cs" company="ExMod Team">
// Copyright (c) ExMod Team. All rights reserved.
// Licensed under the CC BY-SA 3.0 license.
// </copyright>
// -----------------------------------------------------------------------

namespace Exiled.Events.Patches.Events.Scp049
{
#pragma warning disable SA1402 // File may only contain a single type
using System.Collections.Generic;
using System.Reflection.Emit;

using API.Features.Pools;
using Exiled.Events.Attributes;
using Exiled.Events.EventArgs.Scp049;
using HarmonyLib;

using PlayerRoles.PlayableScps.Scp049;
using PlayerRoles.Subroutines;

using static HarmonyLib.AccessTools;

/// <summary>
/// Patches <see cref="Scp049SenseAbility.ServerLoseTarget" />.
/// Adds the <see cref="Handlers.Scp049.FinishingSense" /> event.
/// </summary>
[EventPatch(typeof(Handlers.Scp049), nameof(Handlers.Scp049.FinishingSense))]
[HarmonyPatch(typeof(Scp049SenseAbility), nameof(Scp049SenseAbility.ServerLoseTarget))]
internal class FinishingSense
{
private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, ILGenerator generator)
{
List<CodeInstruction> newInstructions = ListPool<CodeInstruction>.Pool.Get(instructions);

LocalBuilder ev = generator.DeclareLocal(typeof(FinishingSenseEventArgs));
Label retLabel = generator.DefineLabel();

newInstructions.InsertRange(0, new CodeInstruction[]
{
// this.Owner
new(OpCodes.Ldarg_0),
new(OpCodes.Callvirt, PropertyGetter(typeof(Scp049SenseAbility), nameof(Scp049SenseAbility.Owner))),

// this.Target
new(OpCodes.Ldarg_0),
new(OpCodes.Callvirt, PropertyGetter(typeof(Scp049SenseAbility), nameof(Scp049SenseAbility.Target))),

// Scp049SenseAbility.TargetLostCooldown
new(OpCodes.Ldc_R8, (double)Scp049SenseAbility.TargetLostCooldown),

// true (IsAllowed)
new(OpCodes.Ldc_I4_1),

// FinishingSenseEventArgs ev = new FinishingSenseEventArgs(ReferenceHub, ReferenceHub, double, bool)
new(OpCodes.Newobj, GetDeclaredConstructors(typeof(FinishingSenseEventArgs))[0]),
new(OpCodes.Dup),
new(OpCodes.Stloc_S, ev.LocalIndex),

// Handlers.Scp049.OnFinishingSense(ev);
new(OpCodes.Call, Method(typeof(Handlers.Scp049), nameof(Handlers.Scp049.OnFinishingSense))),

// if (!ev.IsAllowed) return;
new(OpCodes.Ldloc_S, ev.LocalIndex),
new(OpCodes.Callvirt, PropertyGetter(typeof(FinishingSenseEventArgs), nameof(FinishingSenseEventArgs.IsAllowed))),
new(OpCodes.Brfalse_S, retLabel),
});

// this.Cooldown.Trigger((double)Scp049SenseAbility.TargetLostCooldown) index
int index = newInstructions.FindLastIndex(i => i.opcode == OpCodes.Ldc_R8 && (double)i.operand == Scp049SenseAbility.TargetLostCooldown);

// Replace "this.Cooldown.Trigger((double)Scp049SenseAbility.ReducedCooldown)" with "this.Cooldown.Trigger((double)ev.cooldowntime)"
newInstructions.RemoveAt(index);
newInstructions.InsertRange(index, new CodeInstruction[]
{
new(OpCodes.Ldloc, ev.LocalIndex),
new(OpCodes.Callvirt, PropertyGetter(typeof(FinishingSenseEventArgs), nameof(FinishingSenseEventArgs.CooldownTime))),
});

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

for (int i = 0; i < newInstructions.Count; i++)
yield return newInstructions[i];

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

/// <summary>
/// Patches <see cref="Scp049SenseAbility.ServerProcessKilledPlayer" />.
/// Adds the <see cref="Handlers.Scp049.FinishingSense" /> event.
/// </summary>
[EventPatch(typeof(Handlers.Scp049), nameof(Handlers.Scp049.FinishingSense))]
[HarmonyPatch(typeof(Scp049SenseAbility), nameof(Scp049SenseAbility.ServerProcessKilledPlayer))]
internal class FinishingSense2
{
private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, ILGenerator generator)
{
List<CodeInstruction> newInstructions = ListPool<CodeInstruction>.Pool.Get(instructions);

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

// Continue label for isAllowed check
Label retLabel = generator.DefineLabel();

// this.Cooldown.Trigger(Scp049SenseAbility.BaseCooldown) index
int offset = -2;
int index = newInstructions.FindIndex(i => i.opcode == OpCodes.Ldc_R8 && (double)i.operand == Scp049SenseAbility.BaseCooldown) + offset;

newInstructions.InsertRange(index, new CodeInstruction[]
{
// this.Owner
new(OpCodes.Ldarg_0),
new(OpCodes.Callvirt, PropertyGetter(typeof(Scp049SenseAbility), nameof(Scp049SenseAbility.Owner))),

// this.Target
new(OpCodes.Ldarg_0),
new(OpCodes.Callvirt, PropertyGetter(typeof(Scp049SenseAbility), nameof(Scp049SenseAbility.Target))),

// double CooldownTime = Scp049SenseAbility.BaseCooldown;
new(OpCodes.Ldc_R8, (double)Scp049SenseAbility.BaseCooldown),

// true (IsAllowed)
new(OpCodes.Ldc_I4_1),

// FinishingSenseEventArgs ev = new FinishingSenseEventArgs(ReferenceHub, ReferenceHub, double, bool)
new(OpCodes.Newobj, GetDeclaredConstructors(typeof(FinishingSenseEventArgs))[0]),
new(OpCodes.Dup),
new(OpCodes.Stloc_S, ev.LocalIndex),

// Handlers.Scp049.OnFinishingSense(ev);
new(OpCodes.Call, Method(typeof(Handlers.Scp049), nameof(Handlers.Scp049.OnFinishingSense))),

// if (!ev.IsAllowed) return;
new(OpCodes.Ldloc_S, ev.LocalIndex),
new(OpCodes.Callvirt, PropertyGetter(typeof(FinishingSenseEventArgs), nameof(FinishingSenseEventArgs.IsAllowed))),
new(OpCodes.Brfalse_S, retLabel),
});

// this.Cooldown.Trigger(Scp049SenseAbility.BaseCooldown) index
index = newInstructions.FindLastIndex(i => i.opcode == OpCodes.Ldc_R8 && (double)i.operand == Scp049SenseAbility.BaseCooldown);

newInstructions.RemoveAt(index);
newInstructions.InsertRange(index, new CodeInstruction[]
{
new(OpCodes.Ldloc, ev.LocalIndex),
new(OpCodes.Callvirt, PropertyGetter(typeof(FinishingSenseEventArgs), nameof(FinishingSenseEventArgs.CooldownTime))),
});

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

for (int i = 0; i < newInstructions.Count; i++)
yield return newInstructions[i];

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

/// <summary>
/// Patches <see cref="Scp049SenseAbility.ServerProcessCmd" />.
/// Adds the <see cref="Handlers.Scp049.FinishingSense" /> event.
/// </summary>
[EventPatch(typeof(Handlers.Scp049), nameof(Handlers.Scp049.FinishingSense))]
[HarmonyPatch(typeof(Scp049SenseAbility), nameof(Scp049SenseAbility.ServerProcessCmd), new[] { typeof(Mirror.NetworkReader) })]
internal class FinishingSense3
{
private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, ILGenerator generator)
{
List<CodeInstruction> newInstructions = ListPool<CodeInstruction>.Pool.Get(instructions);

LocalBuilder ev = generator.DeclareLocal(typeof(FinishingSenseEventArgs));
LocalBuilder isAbilityActive = generator.DeclareLocal(typeof(bool));

Label skipactivatingsense = generator.DefineLabel();
Label continueLabel = generator.DefineLabel();
Label allowed = generator.DefineLabel();

newInstructions.InsertRange(0, new CodeInstruction[]
{
// isAbilityActive = this.HasTarget;
new(OpCodes.Ldarg_0),
new(OpCodes.Callvirt, PropertyGetter(typeof(Scp049SenseAbility), nameof(Scp049SenseAbility.HasTarget))),
new(OpCodes.Stloc, isAbilityActive.LocalIndex),
});

// this.Cooldown.Trigger(2.5) index
int offset = -1;
int index = newInstructions.FindIndex(i =>
i.opcode == OpCodes.Ldfld &&
i.operand == (object)Field(typeof(Scp049SenseAbility), nameof(Scp049SenseAbility.Cooldown))) + offset;

newInstructions[index].labels.Add(continueLabel);

newInstructions.InsertRange(index, new CodeInstruction[]
{
// Skip if the ability is not active and this is an unsuccessful attempt
new(OpCodes.Ldloc, isAbilityActive.LocalIndex),
new(OpCodes.Brfalse_S, continueLabel),

// this.Owner;
new(OpCodes.Ldarg_0),
new(OpCodes.Callvirt, PropertyGetter(typeof(Scp049SenseAbility), nameof(Scp049SenseAbility.Owner))),

// this.Target;
new(OpCodes.Ldarg_0),
new(OpCodes.Callvirt, PropertyGetter(typeof(Scp049SenseAbility), nameof(Scp049SenseAbility.Target))),

// Scp049SenseAbility.AttemptFailCooldown;
new(OpCodes.Ldc_R8, (double)Scp049SenseAbility.AttemptFailCooldown),

// true (IsAllowed)
new(OpCodes.Ldc_I4_1),

// FinishingSenseEventArgs ev = new FinishingSenseEventArgs(ReferenceHub, ReferenceHub, double, bool)
new(OpCodes.Newobj, GetDeclaredConstructors(typeof(FinishingSenseEventArgs))[0]),
new(OpCodes.Dup),
new(OpCodes.Stloc_S, ev.LocalIndex),

// Handlers.Scp049.OnFinishingSense(ev);
new(OpCodes.Call, Method(typeof(Handlers.Scp049), nameof(Handlers.Scp049.OnFinishingSense))),

// if (!ev.IsAllowed) return;
new(OpCodes.Ldloc_S, ev.LocalIndex),
new(OpCodes.Callvirt, PropertyGetter(typeof(FinishingSenseEventArgs), nameof(FinishingSenseEventArgs.IsAllowed))),
new(OpCodes.Brtrue_S, allowed),

// If not allowed, set has target to true so as not to break the sense ability
// this.HasTarget = true;
new(OpCodes.Ldarg_0),
new(OpCodes.Ldc_I4_1),
new(OpCodes.Callvirt, PropertySetter(typeof(Scp049SenseAbility), nameof(Scp049SenseAbility.HasTarget))),

// return;
new(OpCodes.Ret),

// this.Cooldown.Trigger(ev.cooldown.time)
new CodeInstruction(OpCodes.Ldarg_0).WithLabels(allowed),
new(OpCodes.Ldloc, ev.LocalIndex),
new(OpCodes.Callvirt, PropertyGetter(typeof(FinishingSenseEventArgs), nameof(FinishingSenseEventArgs.CooldownTime))),
new(OpCodes.Callvirt, Method(typeof(AbilityCooldown), nameof(AbilityCooldown.Trigger), new[] { typeof(double) })),

new(OpCodes.Br_S, skipactivatingsense),
});

offset = -2;
index = newInstructions.FindIndex(i =>
i.opcode == OpCodes.Call &&
i.operand == (object)Method(typeof(SubroutineBase), nameof(SubroutineBase.ServerSendRpc), new[] { typeof(bool) })) + offset;

newInstructions[index].labels.Add(skipactivatingsense);

for (int i = 0; i < newInstructions.Count; i++)
yield return newInstructions[i];

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