0
0
mirror of https://github.com/sp-tarkov/modules.git synced 2025-02-13 09:50:43 -05:00
modules/project/Aki.SinglePlayer/Patches/ScavMode/LoadOfflineRaidScreenPatch.cs
2023-03-03 18:52:31 +00:00

147 lines
6.7 KiB
C#

using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using EFT;
using EFT.Bots;
using EFT.UI.Matchmaker;
using EFT.UI.Screens;
using HarmonyLib;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using OfflineRaidAction = System.Action;
// DON'T FORGET TO UPDATE REFERENCES IN CONSTRUCTOR
// AND IN THE LoadOfflineRaidScreenForScavs METHOD AS WELL
namespace Aki.SinglePlayer.Patches.ScavMode
{
public class LoadOfflineRaidScreenPatch : ModulePatch
{
private static readonly MethodInfo _onReadyScreenMethod;
private static readonly FieldInfo _isLocalField;
private static readonly FieldInfo _menuControllerField;
static LoadOfflineRaidScreenPatch()
{
_ = nameof(MainMenuController.InventoryController);
_ = nameof(TimeAndWeatherSettings.IsRandomWeather);
_ = nameof(BotControllerSettings.IsScavWars);
_ = nameof(WavesSettings.IsBosses);
var menuControllerType = typeof(MainMenuController);
_onReadyScreenMethod = menuControllerType.GetMethod("method_39", PatchConstants.PrivateFlags);
_isLocalField = menuControllerType.GetField("bool_0", PatchConstants.PrivateFlags);
_menuControllerField = typeof(TarkovApplication).GetFields(PatchConstants.PrivateFlags).FirstOrDefault(x => x.FieldType == typeof(MainMenuController));
if (_menuControllerField == null)
{
Logger.LogError($"LoadOfflineRaidScreenPatch() menuControllerField is null and could not be found in {nameof(TarkovApplication)} class");
}
}
protected override MethodBase GetTargetMethod()
{
return typeof(MainMenuController).GetMethod("method_63", PatchConstants.PrivateFlags);
}
[PatchTranspiler]
private static IEnumerable<CodeInstruction> PatchTranspiler(ILGenerator generator, IEnumerable<CodeInstruction> instructions)
{
var codes = new List<CodeInstruction>(instructions);
// The original method call that we want to replace
var onReadyScreenMethodIndex = -1;
var onReadyScreenMethodCode = new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(MainMenuController), _onReadyScreenMethod.Name));
// We additionally need to replace an instruction that jumps to a label on certain conditions, since we change the jump target instruction
var jumpWhenFalse_Index = -1;
for (var i = 0; i < codes.Count; i++)
{
if (codes[i].opcode == onReadyScreenMethodCode.opcode && codes[i].operand == onReadyScreenMethodCode.operand)
{
onReadyScreenMethodIndex = i;
continue;
}
if (codes[i].opcode == OpCodes.Brfalse)
{
if (jumpWhenFalse_Index != -1)
{
// If this warning is ever logged, the condition for locating the exact brfalse instruction will have to be updated
Logger.LogWarning($"[{nameof(LoadOfflineRaidScreenPatch)}] Found extra instructions with the brfalse opcode! " +
"This breaks an old assumption that there is only one such instruction in the method body and is now very likely to cause bugs!");
}
jumpWhenFalse_Index = i;
}
}
if (onReadyScreenMethodIndex == -1)
{
throw new Exception($"{nameof(LoadOfflineRaidScreenPatch)} failed: Could not find {nameof(_onReadyScreenMethod)} reference code.");
}
if (jumpWhenFalse_Index == -1)
{
throw new Exception($"{nameof(LoadOfflineRaidScreenPatch)} failed: Could not find jump (brfalse) reference code.");
}
// Define the new jump label
var brFalseLabel = generator.DefineLabel();
// We build the method call for our substituted method and replace the initial method call with our own, also adding our new label
var callCode = new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(LoadOfflineRaidScreenPatch), nameof(LoadOfflineRaidScreenForScav))) { labels = { brFalseLabel }};
codes[onReadyScreenMethodIndex] = callCode;
// We build a new brfalse instruction and give it our new label, then replace the original brfalse instruction
var newBrFalseCode = new CodeInstruction(OpCodes.Brfalse, brFalseLabel);
codes[jumpWhenFalse_Index] = newBrFalseCode;
// This will remove a stray ldarg.0 instruction. It's only needed if we wanted to reference something from `this` in the method body.
// This is done last to ensure that previous instruction indexes don't shift around (probably why this used to just turn it into a Nop OpCode)
codes.RemoveAt(onReadyScreenMethodIndex - 1);
return codes.AsEnumerable();
}
private static void LoadOfflineRaidScreenForScav()
{
var profile = PatchConstants.BackEndSession.Profile;
var menuController = (object)GetMenuController();
var raidSettings = Traverse.Create(menuController).Field("raidSettings_0").GetValue<RaidSettings>();
var matchmakerPlayersController = Traverse.Create(menuController).Field("gclass2780_0").GetValue<GClass2780>();
var gclass = new MatchmakerOfflineRaidScreen.GClass2769(profile?.Info, ref raidSettings, matchmakerPlayersController);
gclass.OnShowNextScreen += LoadOfflineRaidNextScreen;
// ready method
gclass.OnShowReadyScreen += (OfflineRaidAction)Delegate.CreateDelegate(typeof(OfflineRaidAction), menuController, "method_67");
gclass.ShowScreen(EScreenState.Queued);
}
private static void LoadOfflineRaidNextScreen()
{
var menuController = GetMenuController();
var raidSettings = Traverse.Create(menuController).Field("raidSettings_0").GetValue<RaidSettings>();
if (raidSettings.SelectedLocation.Id == "laboratory")
{
raidSettings.WavesSettings.IsBosses = true;
}
// set offline raid values
_isLocalField.SetValue(menuController, raidSettings.Local);
// load ready screen method
_onReadyScreenMethod.Invoke(menuController, null);
}
private static MainMenuController GetMenuController()
{
return _menuControllerField.GetValue(ClientAppUtils.GetMainApp()) as MainMenuController;
}
}
}