Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Exiled::API] Adding a game's constant patching #2419

Merged
merged 19 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from 18 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
212 changes: 212 additions & 0 deletions Exiled.API/Features/Core/ConstProperty.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
// -----------------------------------------------------------------------
// <copyright file="ConstProperty.cs" company="Exiled Team">
// Copyright (c) Exiled Team. All rights reserved.
// Licensed under the CC BY-SA 3.0 license.
// </copyright>
// -----------------------------------------------------------------------

namespace Exiled.API.Features.Core
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;

using HarmonyLib;
using Mono.Cecil;

/// <summary>
/// A class to manipulate game's constants.
/// </summary>
/// <typeparam name="T">Constant type.</typeparam>
public class ConstProperty<T>
{
private bool patched = false;
private T value;

/// <summary>
/// Initializes a new instance of the <see cref="ConstProperty{T}"/> class.
/// </summary>
/// <param name="constantValue">An actual constant value.</param>
/// <param name="typesToPatch">A collection of types where this constant is used.</param>
/// <param name="skipMethods"><inheritdoc cref="SkipMethods"/></param>
public ConstProperty(T constantValue, Type[] typesToPatch, MethodInfo[] skipMethods = null)
{
ConstantValue = constantValue;
value = constantValue;
TypesToPatch = typesToPatch;
SkipMethods = skipMethods ?? Array.Empty<MethodInfo>();

List.Add(this);
}

/// <summary>
/// Finalizes an instance of the <see cref="ConstProperty{T}"/> class.
/// </summary>
~ConstProperty()
{
List.Remove(this);

foreach (MethodInfo methodInfo in TypesToPatch.SelectMany(x => x.GetMethods().Where(y => y.DeclaringType != null && y.DeclaringType == x)))
VALERA771 marked this conversation as resolved.
Show resolved Hide resolved
{
try
{
Harmony.Unpatch(methodInfo, AccessTools.Method(typeof(ConstProperty<T>), nameof(Transpiler)));
}
catch (Exception exception)
{
Log.Error(exception);
}
}
}

/// <summary>
/// Gets the value of game's constant.
/// </summary>
public T ConstantValue { get; }

/// <summary>
/// Gets or sets a value which will replace <see cref="ConstantValue"/> in types.
/// </summary>
public T Value
{
get => value;
set
{
this.value = value;

if (!patched && !EqualityComparer<T>.Default.Equals(value, ConstantValue))
Patch();
}
}

/// <summary>
/// Gets a collection of types where <see cref="ConstantValue"/> should be replaced with <see cref="Value"/>.
/// </summary>
public Type[] TypesToPatch { get; }

/// <summary>
/// Gets a collection of methods that should be skipped when patching.
/// </summary>
public MethodInfo[] SkipMethods { get; }

/// <summary>
/// Gets the <see cref="HarmonyLib.Harmony"/> instance for this constant property.
/// </summary>
internal static Harmony Harmony { get; } = new($"exiled.api-{typeof(T).FullName}");

/// <summary>
/// Gets the list of all <see cref="ConstProperty{T}"/>.
/// </summary>
internal static List<ConstProperty<T>> List { get; } = new();

/// <summary>
/// A converter to value's type.
/// </summary>
/// <param name="property">The <see cref="ConstProperty{T}"/> instance.</param>
/// <returns>A <see cref="ConstProperty{T}"/> value.</returns>
public static implicit operator T(ConstProperty<T> property) => property.Value;

/// <summary>
/// Gets the <see cref="ConstProperty{T}"/> by it's patched type and constant value.
/// </summary>
/// <param name="constValue">A game's constant value.</param>
/// <param name="type">Type where this constant is using.</param>
/// <returns>The <see cref="ConstProperty{T}"/> instance or <see langword="null"/>.</returns>
internal static ConstProperty<T> Get(T constValue, Type type) => List.Find(x => x.TypesToPatch.Contains(type) && typeof(T) == x.ConstantValue.GetType() && EqualityComparer<T>.Default.Equals(constValue, x.ConstantValue));

private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, MethodBase original)
{
Type currentType = original.DeclaringType;

foreach (CodeInstruction instruction in instructions)
{
if (instruction.operand == null || instruction.operand.GetType() != typeof(T))
{
yield return instruction;
continue;
}

ConstProperty<T> property = Get((T)instruction.operand, currentType);

if (property == null || !EqualityComparer<T>.Default.Equals((T)instruction.operand, property.ConstantValue))
{
yield return instruction;
continue;
}

switch (Type.GetTypeCode(typeof(T)))
{
case TypeCode.Int16 or TypeCode.Int32 or TypeCode.Int64 or TypeCode.Boolean or TypeCode.Byte or TypeCode.SByte or TypeCode.UInt16 or TypeCode.UInt32 or TypeCode.UInt64:
yield return new(OpCodes.Ldc_I4_S, Convert.ToInt32(property.Value));
continue;
case TypeCode.Char or TypeCode.String:
yield return new(OpCodes.Ldstr, property.Value);
continue;
case TypeCode.Single:
yield return new(OpCodes.Ldc_R4, property.Value);
continue;
case TypeCode.Double:
yield return new(OpCodes.Ldc_R8, property.Value);
continue;
case TypeCode.Empty:
yield return instruction;
continue;
case TypeCode.Object:
yield return new(instruction.opcode, property.Value);
continue;
}

yield return instruction;
}
}

private void Patch()
{
AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(Path.Combine(Paths.ManagedAssemblies, "Assembly-CSharp.dll"));

foreach (MethodInfo methodInfo in TypesToPatch.SelectMany(x => x.GetProperties().Where(y => y.SetMethod != null && y.DeclaringType != null && y.DeclaringType == x)
.Select(y => y.SetMethod)))
{
if (Array.Exists(SkipMethods, x => x == methodInfo))
continue;

MethodReference methodReference = assembly.MainModule.ImportReference(methodInfo);
if (!methodReference.Resolve().Body.Instructions.Any(x => x.Operand is T t && EqualityComparer<T>.Default.Equals(t, ConstantValue)))
continue;

try
{
Harmony.Patch(methodInfo, transpiler: new HarmonyMethod(typeof(ConstProperty<T>), nameof(Transpiler)));
}
catch (Exception exception)
{
Log.Error(exception);
}
}

foreach (MethodInfo methodInfo in TypesToPatch.SelectMany(x => x.GetMethods().Where(y => y.DeclaringType != null && y.DeclaringType == x)))
{
if (Array.Exists(SkipMethods, x => x == methodInfo))
continue;

MethodReference methodReference = assembly.MainModule.ImportReference(methodInfo);
if (!methodReference.Resolve().Body.Instructions.Any(x => x.Operand is T t && EqualityComparer<T>.Default.Equals(t, ConstantValue)))
continue;

try
{
Harmony.Patch(methodInfo, transpiler: new HarmonyMethod(typeof(ConstProperty<T>), nameof(Transpiler)));
}
catch (Exception exception)
{
Log.Error(exception);
}
}

patched = true;
}
}
}
12 changes: 12 additions & 0 deletions Exiled.API/Features/Items/Jailbird.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace Exiled.API.Features.Items
{
using System;

using Exiled.API.Features.Core;
using Exiled.API.Features.Pickups;
using Exiled.API.Interfaces;
using InventorySystem.Items.Autosync;
Expand All @@ -23,6 +24,8 @@ namespace Exiled.API.Features.Items
/// </summary>
public class Jailbird : Item, IWrapper<JailbirdItem>
{
private readonly ConstProperty<double> chargeTolerance = new(-0.4000000059604645, new[] { typeof(JailbirdItem) });

/// <summary>
/// Initializes a new instance of the <see cref="Jailbird"/> class.
/// </summary>
Expand Down Expand Up @@ -114,6 +117,15 @@ public JailbirdWearState WearState
}
}

/// <summary>
/// Gets or sets amount of time when Jailbird is charging.
/// </summary>
public double ChargeTolerance
{
get => chargeTolerance;
set => chargeTolerance.Value = value;
}

/// <summary>
/// Calculates the damage corresponding to a given <see cref="JailbirdWearState"/>.
/// </summary>
Expand Down
33 changes: 32 additions & 1 deletion Exiled.API/Features/Items/MicroHid.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@

namespace Exiled.API.Features.Items
{
using Exiled.API.Features.Core;
using Exiled.API.Interfaces;

using InventorySystem.Items.MicroHID;

/// <summary>
/// A wrapper class for <see cref="MicroHIDItem"/>.
/// </summary>
public class MicroHid : Item, IWrapper<MicroHIDItem>
{
private readonly ConstProperty<double> preFire = new(1.7000000476837158, new[] { typeof(MicroHIDItem) });
private readonly ConstProperty<double> minTimeToSwitch = new(0.3499999940395355, new[] { typeof(MicroHIDItem) });
private readonly ConstProperty<double> powerdownTime = new(3.0999999046325684, new[] { typeof(MicroHIDItem) });

/// <summary>
/// Initializes a new instance of the <see cref="MicroHid"/> class.
/// </summary>
Expand Down Expand Up @@ -57,6 +61,33 @@ public HidState State
set => Base.State = value;
}

/// <summary>
/// Gets or sets the time it takes to start firing from the MicroHID.
/// </summary>
public double PreFireTime
{
get => preFire.Value;
set => preFire.Value = value;
}

/// <summary>
/// Gets or sets the time it takes to switch the MicroHID to other mode.
/// </summary>
public double MinTimeToSwitch
{
get => minTimeToSwitch.Value;
set => minTimeToSwitch.Value = value;
}

/// <summary>
/// Gets or sets the time it takes to power down the MicroHID.
/// </summary>
public double PowerDownTime
{
get => powerdownTime.Value;
set => powerdownTime.Value = value;
}

/// <summary>
/// Starts firing the MicroHID.
/// </summary>
Expand Down
15 changes: 12 additions & 3 deletions Exiled.API/Features/Items/Radio.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@
namespace Exiled.API.Features.Items
{
using Enums;
using Exiled.API.Features.Core;
using Exiled.API.Interfaces;

using InventorySystem.Items.Radio;

using Structs;

using VoiceChat.Playbacks;

/// <summary>
/// A wrapper class for <see cref="RadioItem"/>.
/// </summary>
public class Radio : Item, IWrapper<RadioItem>
{
private readonly ConstProperty<float> drainMultiplier = new(RadioItem.DrainMultiplier, new[] { typeof(RadioItem) });

/// <summary>
/// Initializes a new instance of the <see cref="Radio"/> class.
/// </summary>
Expand Down Expand Up @@ -97,6 +97,15 @@ public bool IsEnabled
/// </summary>
public bool IsTransmitting => PersonalRadioPlayback.IsTransmitting(Owner.ReferenceHub);

/// <summary>
/// Gets or sets a multiplier for draining radio.
/// </summary>
public float DrainMultiplier
{
get => drainMultiplier;
set => drainMultiplier.Value = value;
}

/// <summary>
/// Sets the <see cref="RadioRangeSettings"/> of the given <paramref name="range"/>.
/// </summary>
Expand Down
23 changes: 22 additions & 1 deletion Exiled.API/Features/Items/Scp1576.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

namespace Exiled.API.Features.Items
{
using Exiled.API.Features.Core;
using Exiled.API.Interfaces;

using InventorySystem.Items.Usables;
using InventorySystem.Items.Usables.Scp1576;

Expand All @@ -17,6 +17,9 @@ namespace Exiled.API.Features.Items
/// </summary>
public class Scp1576 : Usable, IWrapper<Scp1576Item>
{
private readonly ConstProperty<float> cooldown = new(Scp1576Item.UseCooldown, new[] { typeof(Scp1576Item) });
private readonly ConstProperty<double> warningDuration = new(Scp1576Item.WarningDuration, new[] { typeof(Scp1576Item) });

/// <summary>
/// Initializes a new instance of the <see cref="Scp1576"/> class.
/// </summary>
Expand Down Expand Up @@ -45,6 +48,24 @@ internal Scp1576()
/// </summary>
public Scp1576Playback PlaybackTemplate => Base.PlaybackTemplate;

/// <summary>
/// Gets or sets the cooldown for using the SCP-1576.
/// </summary>
public float Cooldown
{
get => cooldown;
set => cooldown.Value = value;
}

/// <summary>
/// Gets or sets the warning duration.
/// </summary>
public double WarningDuration
{
get => warningDuration;
set => warningDuration.Value = value;
}

/// <summary>
/// Forcefully stops the transmission of SCP-1576.
/// </summary>
Expand Down
Loading
Loading