Files
Nitrox/NitroxPatcher/TranspilerHelper.cs
2025-07-06 00:23:46 +02:00

161 lines
6.2 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using HarmonyLib;
using NitroxClient.GameLogic;
using NitroxClient.MonoBehaviours;
using NitroxModel.Core;
using NitroxModel.Helper;
using NitroxPatcher.PatternMatching;
using UnityEngine;
namespace NitroxPatcher;
internal static class TranspilerHelper
{
private static readonly MethodInfo serviceLocator = typeof(NitroxServiceLocator)
.GetMethod(nameof(NitroxServiceLocator.LocateService), BindingFlags.Static | BindingFlags.Public, null, Array.Empty<Type>(), null);
private static readonly MethodInfo DELTA_TIME_INSERTED_METHOD = Reflect.Method(() => GetDeltaTime());
private static readonly MethodInfo DELTA_TIME_MATCHING_FIELD = Reflect.Property(() => Time.deltaTime).GetGetMethod();
public static CodeInstruction LocateService<T>()
{
return new CodeInstruction(OpCodes.Call, serviceLocator.MakeGenericMethod(typeof(T)));
}
private static IEnumerable<LocalVariableInfo> GetMatchingLocalVariables<T>(MethodBase method)
{
return method.GetMethodBody()?.LocalVariables.Where(v => v.LocalType == typeof(T)) ?? Array.Empty<LocalVariableInfo>();
}
/// <summary>
/// Outputs an If (Multiplayer.Active) check
/// </summary>
/// <param name="jmpLabel">Spot to jump to if Multiplayer.Active is false</param>
/// <param name="generator">The ILGenerator</param>
/// <returns></returns>
public static IEnumerable<CodeInstruction> IsMultiplayer(Label jmpLabel, ILGenerator generator)
{
yield return new CodeInstruction(OpCodes.Callvirt, Reflect.Property(() => Multiplayer.Active).GetMethod);
yield return new CodeInstruction(OpCodes.Brfalse, jmpLabel); // If false jump to the end of the code block
}
/// <summary>
/// Outputs an If (!Multiplayer.Active) check
/// </summary>
/// <param name="jmpLabel">Spot to jump to if Multiplayer.Active is true</param>
/// <param name="generator">The ILGenerator</param>
/// <returns></returns>
public static IEnumerable<CodeInstruction> IsNotMultiplayer(Label jmpLabel, ILGenerator generator)
{
yield return new CodeInstruction(OpCodes.Callvirt, Reflect.Property(() => Multiplayer.Active).GetMethod);
yield return new CodeInstruction(OpCodes.Brtrue, jmpLabel); // If true jump to the end of the code block
}
/// <summary>
/// Returns the one and only local variable of type <typeparamref name="T"/>. Throws <see cref="InvalidOperationException"/> if there is not exactly one local variable of that type.
/// </summary>
/// <exception cref="InvalidOperationException" />
public static int GetLocalVariableIndex<T>(this MethodBase method)
{
return GetMatchingLocalVariables<T>(method).Single().LocalIndex;
}
/// <summary>
/// Returns the index of the <paramref name="i"/>'th local variable of type <typeparamref name="T"/>.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException" />
public static int GetLocalVariableIndex<T>(this MethodBase method, int i)
{
return GetMatchingLocalVariables<T>(method).ElementAt(i).LocalIndex;
}
/// <summary>
/// Returns the shortest possible Ldloc instruction for the given local variable index <paramref name="i"/>.
/// </summary>
private static CodeInstruction Ldloc(int i)
{
switch (i)
{
case 0:
return new CodeInstruction(OpCodes.Ldloc_0);
case 1:
return new CodeInstruction(OpCodes.Ldloc_1);
case 2:
return new CodeInstruction(OpCodes.Ldloc_2);
case 3:
return new CodeInstruction(OpCodes.Ldloc_3);
default:
if (i <= 0xFF)
{
return new CodeInstruction(OpCodes.Ldloc_S, (byte)i);
}
else
{
return new CodeInstruction(OpCodes.Ldloc, (ushort)i);
}
}
}
/// <summary>
/// Returns an instruction that loads the one and only local variable of type <typeparamref name="T"/> in <paramref name="method"/> onto the evaluation stack.
/// </summary>
/// <typeparam name="T">The type to locate</typeparam>
/// <param name="method">The method in which to locate the local variable</param>
public static CodeInstruction Ldloc<T>(this MethodBase method)
{
return Ldloc(method.GetLocalVariableIndex<T>());
}
/// <summary>
/// Loads the <paramref name="i"/>'th occurence of a local variable of type <typeparamref name="T"/> in <paramref name="method"/>.
/// </summary>
/// <typeparam name="T">The type to locate</typeparam>
/// <param name="method">The method in which to locate the local variable</param>
/// <param name="i">Index of the local variable to load, in a list of only variables of type <typeparamref name="T"/></param>
public static CodeInstruction Ldloc<T>(this MethodBase method, int i)
{
if (method == null)
{
return new CodeInstruction(OpCodes.Nop);
}
return Ldloc(method.GetLocalVariableIndex<T>(i));
}
// ReSharper disable once UnusedMember.Global
public static CodeMatcher LogInstructions(this CodeMatcher matcher)
{
StringBuilder sb = new();
sb.AppendLine();
sb.Append("IL-Instructions Valid: ");
sb.AppendLine(matcher.IsValid.ToString());
sb.AppendLine(matcher.InstructionEnumeration().ToPrettyString());
Log.Info(sb.ToString());
return matcher;
}
/// <summary>
/// Replaces first use of <see cref="Time.deltaTime"/> by <see cref="TimeManager.DeltaTime"/>.
/// Moves to the said instruction.
/// </summary>
public static CodeMatcher ReplaceDeltaTime(this CodeMatcher codeMatcher)
{
return codeMatcher.MatchStartForward(new CodeMatch(OpCodes.Call, DELTA_TIME_MATCHING_FIELD))
.SetOperandAndAdvance(DELTA_TIME_INSERTED_METHOD);
}
/// <summary>
/// Wrapper for dependency resolving and variable querying
/// </summary>
private static float GetDeltaTime()
{
return NitroxServiceLocator.Cache<TimeManager>.Value.DeltaTime;
}
}