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
Terkoiz 337a0733ae Publicized assembly refactor (!58)
Depends on SPT-AKI/SPT-AssemblyTool#3

* Refactored Modules for better consistency and general readability, along with preparing the code for a publicized assembly
* Added `PublicDeclaredFlags` to `PatchConstants` to cover a set of commonly used flags to get methods post-publicizing
* Added a replacement to LINQ's `.Single()` - `.SingleCustom()` which has improved logging to help with debugging Module code. Replaced all `.Single()` usages where applicable
* Replaced most method info fetching with `AccessTools` for consistency and better readability, especially in places where methods were being retrieved by their name anyways

**NOTE:**
As a side effect of publicizing all properties, some property access code such as `Player.Position` will now show "ambiguous reference" errors during compile, due to there being multiple interfaces with the Property name being defined on the class. The way to get around this is to use a cast to an explicit interface
Example:
```cs
Singleton<GameWorld>.Instance.MainPlayer.Position
```
will now need to be
```cs
((IPlayer)Singleton<GameWorld>.Instance.MainPlayer).Position
```

Co-authored-by: Terkoiz <terkoiz@spt.dev>
Reviewed-on: SPT-AKI/Modules#58
Co-authored-by: Terkoiz <terkoiz@noreply.dev.sp-tarkov.com>
Co-committed-by: Terkoiz <terkoiz@noreply.dev.sp-tarkov.com>
2024-01-13 22:08:29 +00:00

152 lines
7.0 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);
_ = GClass3164.MAX_SCAV_COUNT; // UPDATE REFS TO THIS CLASS BELOW !!!
// `MatchmakerInsuranceScreen` OnShowNextScreen
_onReadyScreenMethod = AccessTools.Method(typeof(MainMenuController), nameof(MainMenuController.method_42));
_isLocalField = AccessTools.Field(typeof(MainMenuController), "bool_0");
_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()
{
// `MatchMakerSelectionLocationScreen` OnShowNextScreen
return AccessTools.Method(typeof(MainMenuController), nameof(MainMenuController.method_68));
}
[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();
// Get fields from MainMenuController.cs
var raidSettings = Traverse.Create(menuController).Field("raidSettings_0").GetValue<RaidSettings>();
var matchmakerPlayersController = Traverse.Create(menuController).Field($"{nameof(GClass3164).ToLowerInvariant()}_0").GetValue<GClass3164>();
var gclass = new MatchmakerOfflineRaidScreen.GClass3153(profile?.Info, ref raidSettings, matchmakerPlayersController);
gclass.OnShowNextScreen += LoadOfflineRaidNextScreen;
// `MatchmakerOfflineRaidScreen` OnShowReadyScreen
gclass.OnShowReadyScreen += (OfflineRaidAction)Delegate.CreateDelegate(typeof(OfflineRaidAction), menuController, nameof(MainMenuController.method_72));
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;
}
}
}