diff --git a/.gitignore b/.gitignore index c66d895..3f40ac1 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,10 @@ bld/ # Visual Studio 2015/2017 cache/options directory .vs/ + +# Rider directory +.idea/ + # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ diff --git a/README.md b/README.md index b51decd..e2983e0 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,8 @@ git config --local user.email "USERNAME@SOMETHING.com" ## Requirements -- Escape From Tarkov 25206 -- BepInEx 5.4.19 +- Escape From Tarkov 26282 +- BepInEx 5.4.21 - Visual Studio Code - .NET 6 SDK diff --git a/project/Aki.Core/AkiCorePlugin.cs b/project/Aki.Core/AkiCorePlugin.cs index 85d2b9f..7324aa4 100644 --- a/project/Aki.Core/AkiCorePlugin.cs +++ b/project/Aki.Core/AkiCorePlugin.cs @@ -24,6 +24,7 @@ namespace Aki.Core } catch (Exception ex) { + Logger.LogError($"A PATCH IN {GetType().Name} FAILED. SUBSEQUENT PATCHES HAVE NOT LOADED"); Logger.LogError($"{GetType().Name}: {ex}"); throw; } diff --git a/project/Aki.Core/Patches/TransportPrefixPatch.cs b/project/Aki.Core/Patches/TransportPrefixPatch.cs index e9fef70..208b0c6 100644 --- a/project/Aki.Core/Patches/TransportPrefixPatch.cs +++ b/project/Aki.Core/Patches/TransportPrefixPatch.cs @@ -15,7 +15,14 @@ namespace Aki.Core.Patches { try { - var type = PatchConstants.EftTypes.Single(t => t.Name == "Class227"); + _ = GClass239.DEBUG_LOGIC; // UPDATE BELOW LINE TOO + var type = PatchConstants.EftTypes.Single(t => t.Name == "Class239"); + + if (type == null) + { + throw new Exception($"{nameof(TransportPrefixPatch)} failed: Could not find type to patch."); + } + var value = Traverse.Create(type).Field("TransportPrefixes").GetValue>(); value[ETransportProtocolType.HTTPS] = "http://"; value[ETransportProtocolType.WSS] = "ws://"; diff --git a/project/Aki.Custom/Airdrops/AirdropsManager.cs b/project/Aki.Custom/Airdrops/AirdropsManager.cs index 60ad401..0911c45 100644 --- a/project/Aki.Custom/Airdrops/AirdropsManager.cs +++ b/project/Aki.Custom/Airdrops/AirdropsManager.cs @@ -1,5 +1,4 @@ -using Aki.Common.Http; -using Aki.Custom.Airdrops.Models; +using Aki.Custom.Airdrops.Models; using Aki.Custom.Airdrops.Utils; using Comfort.Common; using EFT; @@ -12,25 +11,33 @@ namespace Aki.Custom.Airdrops private AirdropPlane airdropPlane; private AirdropBox airdropBox; private ItemFactoryUtil factory; - public bool isFlareDrop; private AirdropParametersModel airdropParameters; - public async void Start() + public async void Awake() { - var gameWorld = Singleton.Instance; - - if (gameWorld == null) + try { - Destroy(this); + var gameWorld = Singleton.Instance; + + if (gameWorld == null) + { + Destroy(this); + } + + airdropParameters = AirdropUtil.InitAirdropParams(gameWorld, isFlareDrop); + + if (!airdropParameters.AirdropAvailable) + { + Destroy(this); + return; + } } - - airdropParameters = AirdropUtil.InitAirdropParams(gameWorld, isFlareDrop); - - if (!airdropParameters.AirdropAvailable) + catch { + Debug.LogError("[AKI-AIRDROPS]: Unable to get config from server, airdrop won't occur"); Destroy(this); - return; + throw; } try @@ -55,34 +62,47 @@ namespace Aki.Custom.Airdrops public void FixedUpdate() { - airdropParameters.Timer += 0.02f; + if (airdropParameters == null || airdropPlane == null || airdropBox == null) return; - if (airdropParameters.Timer >= airdropParameters.TimeToStart && !airdropParameters.PlaneSpawned) + try { - StartPlane(); - } + airdropParameters.Timer += 0.02f; - if (!airdropParameters.PlaneSpawned) - { - return; - } + if (airdropParameters.Timer >= airdropParameters.TimeToStart && !airdropParameters.PlaneSpawned) + { + StartPlane(); + } - if (airdropParameters.DistanceTraveled >= airdropParameters.DistanceToDrop && !airdropParameters.BoxSpawned) - { - StartBox(); - BuildLootContainer(airdropParameters.Config); - } + if (!airdropParameters.PlaneSpawned) + { + return; + } - if (airdropParameters.DistanceTraveled < airdropParameters.DistanceToTravel) - { - airdropParameters.DistanceTraveled += Time.deltaTime * airdropParameters.Config.PlaneSpeed; - var distanceToDrop = airdropParameters.DistanceToDrop - airdropParameters.DistanceTraveled; - airdropPlane.ManualUpdate(distanceToDrop); + if (airdropParameters.DistanceTraveled >= airdropParameters.DistanceToDrop && !airdropParameters.BoxSpawned) + { + StartBox(); + BuildLootContainer(airdropParameters.Config); + } + + if (airdropParameters.DistanceTraveled < airdropParameters.DistanceToTravel) + { + airdropParameters.DistanceTraveled += Time.deltaTime * airdropParameters.Config.PlaneSpeed; + var distanceToDrop = airdropParameters.DistanceToDrop - airdropParameters.DistanceTraveled; + airdropPlane.ManualUpdate(distanceToDrop); + } + else + { + Destroy(airdropPlane.gameObject); + Destroy(this); + } } - else + catch { + Debug.LogError("[AKI-AIRDROPS]: An error occurred during the airdrop FixedUpdate process"); + Destroy(airdropBox.gameObject); Destroy(airdropPlane.gameObject); Destroy(this); + throw; } } diff --git a/project/Aki.Custom/Airdrops/Utils/ItemFactoryUtil.cs b/project/Aki.Custom/Airdrops/Utils/ItemFactoryUtil.cs index 2ea4e9f..1ea01da 100644 --- a/project/Aki.Custom/Airdrops/Utils/ItemFactoryUtil.cs +++ b/project/Aki.Custom/Airdrops/Utils/ItemFactoryUtil.cs @@ -43,12 +43,15 @@ namespace Aki.Custom.Airdrops.Utils if (item.IsPreset) { actualItem = itemFactory.GetPresetItem(item.Tpl); + actualItem.SpawnedInSession = true; + actualItem.GetAllItems().ExecuteForEach(x => x.SpawnedInSession = true); resources = actualItem.GetAllItems().Select(x => x.Template).SelectMany(x => x.AllResources).ToArray(); } else { actualItem = itemFactory.CreateItem(item.ID, item.Tpl, null); actualItem.StackObjectsCount = item.StackCount; + actualItem.SpawnedInSession = true; resources = actualItem.Template.AllResources.ToArray(); } diff --git a/project/Aki.Custom/AkiCustomPlugin.cs b/project/Aki.Custom/AkiCustomPlugin.cs index 20d01a4..9fc137c 100644 --- a/project/Aki.Custom/AkiCustomPlugin.cs +++ b/project/Aki.Custom/AkiCustomPlugin.cs @@ -31,6 +31,7 @@ namespace Aki.Custom new SessionIdPatch().Enable(); new VersionLabelPatch().Enable(); new IsEnemyPatch().Enable(); + new LocationLootCacheBustingPatch().Enable(); //new AddSelfAsEnemyPatch().Enable(); new CheckAndAddEnemyPatch().Enable(); new BotSelfEnemyPatch().Enable(); // needed @@ -42,9 +43,13 @@ namespace Aki.Custom new ExitWhileLootingPatch().Enable(); new QTEPatch().Enable(); new PmcFirstAidPatch().Enable(); + new SettingsLocationPatch().Enable(); + //new RankPanelPatch().Enable(); + new RagfairFeePatch().Enable(); } catch (Exception ex) { + Logger.LogError($"A PATCH IN {GetType().Name} FAILED. SUBSEQUENT PATCHES HAVE NOT LOADED"); Logger.LogError($"{GetType().Name}: {ex}"); throw; } diff --git a/project/Aki.Custom/CustomAI/AIBrainSpawnWeightAdjustment.cs b/project/Aki.Custom/CustomAI/AIBrainSpawnWeightAdjustment.cs new file mode 100644 index 0000000..42d4fda --- /dev/null +++ b/project/Aki.Custom/CustomAI/AIBrainSpawnWeightAdjustment.cs @@ -0,0 +1,146 @@ +using Aki.Common.Http; +using BepInEx.Logging; +using EFT; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Aki.Custom.CustomAI +{ + public class AIBrainSpawnWeightAdjustment + { + private static AIBrains aiBrainsCache = null; + private static DateTime aiBrainCacheDate = new DateTime(); + private static readonly Random random = new Random(); + private static readonly List playerScavTypes = new List() { WildSpawnType.bossKilla, WildSpawnType.pmcBot, WildSpawnType.bossGluhar }; + private readonly ManualLogSource logger; + + public AIBrainSpawnWeightAdjustment(ManualLogSource logger) + { + this.logger = logger; + } + + public WildSpawnType GetRandomisedPlayerScavType() + { + return playerScavTypes.Random(); + } + + public WildSpawnType GetAssaultScavWildSpawnType(BotOwner botOwner, string currentMapName) + { + // Get map brain weights from server and cache + if (aiBrainsCache == null || CacheIsStale()) + { + ResetCacheDate(); + HydrateCacheWithServerData(); + + if (!aiBrainsCache.assault.TryGetValue(currentMapName.ToLower(), out _)) + { + throw new Exception($"Bots were refreshed from the server but the assault cache still doesnt contain data"); + } + } + + // Choose random weighted brain + var randomType = WeightedRandom(aiBrainsCache.assault[currentMapName.ToLower()].Keys.ToArray(), aiBrainsCache.assault[currentMapName.ToLower()].Values.ToArray()); + if (Enum.TryParse(randomType, out WildSpawnType newAiType)) + { + logger.LogWarning($"Updated assault bot to use: {newAiType} brain"); + return newAiType; + } + else + { + logger.LogWarning($"Updated assault bot {botOwner.Profile.Info.Nickname}: {botOwner.Profile.Info.Settings.Role} to use: {newAiType} brain"); + + return newAiType; + } + } + + public WildSpawnType GetPmcWildSpawnType(BotOwner botOwner_0, WildSpawnType pmcType, string currentMapName) + { + if (aiBrainsCache == null || !aiBrainsCache.pmc.TryGetValue(pmcType, out var botSettings) || CacheIsStale()) + { + ResetCacheDate(); + HydrateCacheWithServerData(); + + if (!aiBrainsCache.pmc.TryGetValue(pmcType, out botSettings)) + { + throw new Exception($"Bots were refreshed from the server but the cache still doesnt contain an appropriate bot for type {botOwner_0.Profile.Info.Settings.Role}"); + } + } + + var mapSettings = botSettings[currentMapName.ToLower()]; + var randomType = WeightedRandom(mapSettings.Keys.ToArray(), mapSettings.Values.ToArray()); + if (Enum.TryParse(randomType, out WildSpawnType newAiType)) + { + logger.LogWarning($"Updated spt bot {botOwner_0.Profile.Info.Nickname}: {botOwner_0.Profile.Info.Settings.Role} to use: {newAiType} brain"); + + return newAiType; + } + else + { + logger.LogError($"Couldnt not update spt bot {botOwner_0.Profile.Info.Nickname} to random type {randomType}, does not exist for WildSpawnType enum, defaulting to 'assault'"); + + return WildSpawnType.assault; + } + } + + private void HydrateCacheWithServerData() + { + // Get weightings for PMCs from server and store in dict + var result = RequestHandler.GetJson($"/singleplayer/settings/bot/getBotBehaviours/"); + aiBrainsCache = JsonConvert.DeserializeObject(result); + logger.LogWarning($"Cached ai brain weights in client"); + } + + private void ResetCacheDate() + { + aiBrainCacheDate = DateTime.Now; + aiBrainsCache?.pmc?.Clear(); + aiBrainsCache?.assault?.Clear(); + } + + private static bool CacheIsStale() + { + TimeSpan cacheAge = DateTime.Now - aiBrainCacheDate; + + return cacheAge.Minutes > 15; + } + + public class AIBrains + { + public Dictionary>> pmc { get; set; } + public Dictionary> assault { get; set; } + } + + /// + /// Choose a value from a choice of values with weightings + /// + /// + /// + /// + private string WeightedRandom(string[] botTypes, int[] weights) + { + var cumulativeWeights = new int[botTypes.Length]; + + for (int i = 0; i < weights.Length; i++) + { + cumulativeWeights[i] = weights[i] + (i == 0 ? 0 : cumulativeWeights[i - 1]); + } + + var maxCumulativeWeight = cumulativeWeights[cumulativeWeights.Length - 1]; + var randomNumber = maxCumulativeWeight * random.NextDouble(); + + for (var itemIndex = 0; itemIndex < botTypes.Length; itemIndex++) + { + if (cumulativeWeights[itemIndex] >= randomNumber) + { + return botTypes[itemIndex]; + } + } + + logger.LogError("failed to get random bot weighting, returned assault"); + + return "assault"; + } + } +} diff --git a/project/Aki.Custom/CustomAI/AiHelpers.cs b/project/Aki.Custom/CustomAI/AiHelpers.cs new file mode 100644 index 0000000..98484fe --- /dev/null +++ b/project/Aki.Custom/CustomAI/AiHelpers.cs @@ -0,0 +1,47 @@ +using Aki.PrePatch; +using EFT; + +namespace Aki.Custom.CustomAI +{ + public static class AiHelpers + { + /// + /// Bot is a PMC when it has IsStreamerModeAvailable flagged and has a wildspawn type of 'sptBear' or 'sptUsec' + /// + /// Bots role + /// Bot details + /// + public static bool BotIsSptPmc(WildSpawnType botRoleToCheck, BotOwner ___botOwner_0) + { + if (___botOwner_0.Profile.Info.IsStreamerModeAvailable) + { + // PMCs can sometimes have thier role changed to 'assaultGroup' by the client, we need a alternate way to figure out if they're a spt pmc + return true; + } + + return (int)botRoleToCheck == AkiBotsPrePatcher.sptBearValue || (int)botRoleToCheck == AkiBotsPrePatcher.sptUsecValue; + } + + public static bool BotIsPlayerScav(WildSpawnType role, BotOwner ___botOwner_0) + { + if (___botOwner_0.Profile.Info.Nickname.Contains("(") && role == WildSpawnType.assault) + { + // Check bot is pscav by looking for the opening parentheses of their nickname e.g. scavname (pmc name) + return true; + } + + return false; + } + + public static bool BotIsNormalAssaultScav(WildSpawnType role, BotOwner ___botOwner_0) + { + // Is assault + no ( + if (!___botOwner_0.Profile.Info.Nickname.Contains("(") && role == WildSpawnType.assault) + { + return true; + } + + return false; + } + } +} diff --git a/project/Aki.Custom/CustomAI/PmcFoundInRaidEquipment.cs b/project/Aki.Custom/CustomAI/PmcFoundInRaidEquipment.cs new file mode 100644 index 0000000..65b6f10 --- /dev/null +++ b/project/Aki.Custom/CustomAI/PmcFoundInRaidEquipment.cs @@ -0,0 +1,122 @@ +using EFT.InventoryLogic; +using EFT; +using System.Collections.Generic; +using System.Linq; +using BepInEx.Logging; + +namespace Aki.Custom.CustomAI +{ + public class PmcFoundInRaidEquipment + { + private static readonly string magazineId = "5448bc234bdc2d3c308b4569"; + private static readonly string drugId = "5448f3a14bdc2d27728b4569"; + private static readonly string mediKitItem = "5448f39d4bdc2d0a728b4568"; + private static readonly string medicalItemId = "5448f3ac4bdc2dce718b4569"; + private static readonly string injectorItemId = "5448f3a64bdc2d60728b456a"; + private static readonly string throwableItemId = "543be6564bdc2df4348b4568"; + private static readonly string ammoItemId = "5485a8684bdc2da71d8b4567"; + private static readonly string weaponId = "5422acb9af1c889c16000029"; + private static readonly List nonFiRItems = new List() { magazineId, drugId, mediKitItem, medicalItemId, injectorItemId, throwableItemId, ammoItemId }; + + private static readonly string pistolId = "5447b5cf4bdc2d65278b4567"; + private static readonly string smgId = "5447b5e04bdc2d62278b4567"; + private static readonly string assaultRifleId = "5447b5f14bdc2d61278b4567"; + private static readonly string assaultCarbineId = "5447b5fc4bdc2d87278b4567"; + private static readonly string shotgunId = "5447b6094bdc2dc3278b4567"; + private static readonly string marksmanRifleId = "5447b6194bdc2d67278b4567"; + private static readonly string sniperRifleId = "5447b6254bdc2dc3278b4568"; + private static readonly string machinegunId = "5447bed64bdc2d97278b4568"; + private static readonly string grenadeLauncherId = "5447bedf4bdc2d87278b4568"; + private static readonly string knifeId = "5447e1d04bdc2dff2f8b4567"; + + private static readonly List weaponTypeIds = new List() { pistolId, smgId, assaultRifleId, assaultCarbineId, shotgunId, marksmanRifleId, sniperRifleId, machinegunId, grenadeLauncherId, knifeId }; + private readonly ManualLogSource logger; + + public PmcFoundInRaidEquipment(ManualLogSource logger) + { + this.logger = logger; + } + + public void ConfigurePMCFindInRaidStatus(BotOwner ___botOwner_0) + { + // Must run before the container loot code, otherwise backpack loot is not FiR + MakeEquipmentNotFiR(___botOwner_0); + + // Get inventory items that hold other items (backpack/rig/pockets) + List containerGear = ___botOwner_0.Profile.Inventory.Equipment.GetContainerSlots(); + foreach (var container in containerGear) + { + foreach (var item in container.ContainedItem.GetAllItems()) + { + // Skip items that match container (array has itself as an item) + if (item.Id == container.Items.FirstOrDefault().Id) + { + //this.logger.LogError($"Skipping item {item.Id} {item.Name} as its same as container {container.FullId}"); + continue; + } + + // Dont add FiR to tacvest items PMC usually brings into raid (meds/mags etc) + if (container.Name == "TacticalVest" && nonFiRItems.Any(item.Template._parent.Contains)) + { + //this.logger.LogError($"Skipping item {item.Id} {item.Name} as its on the item type blacklist"); + continue; + } + + // Don't add FiR to weapons in backpack (server sometimes adds pre-made weapons to backpack to simulate PMCs looting bodies) + if (container.Name == "Backpack" && weaponTypeIds.Any(item.Template._parent.Contains)) + { + //this.logger.LogError($"Skipping item {item.Id} {item.Name} as its on the item type blacklist"); + continue; + } + + // Don't add FiR to grenades/mags/ammo in pockets + if (container.Name == "Pockets" && new List { throwableItemId, ammoItemId, magazineId, medicalItemId }.Any(item.Template._parent.Contains)) + { + //this.logger.LogError($"Skipping item {item.Id} {item.Name} as its on the item type blacklist"); + continue; + } + + //Logger.LogError($"flagging item FiR: {item.Id} {item.Name} _parent: {item.Template._parent}"); + item.SpawnedInSession = true; + } + } + + // Set dogtag as FiR + var dogtag = ___botOwner_0.Profile.Inventory.GetItemsInSlots(new EquipmentSlot[] { EquipmentSlot.Dogtag }); + dogtag.FirstOrDefault().SpawnedInSession = true; + } + + + private void MakeEquipmentNotFiR(BotOwner ___botOwner_0) + { + var additionalItems = ___botOwner_0.Profile.Inventory.GetItemsInSlots(new EquipmentSlot[] + { EquipmentSlot.Backpack, + EquipmentSlot.FirstPrimaryWeapon, + EquipmentSlot.SecondPrimaryWeapon, + EquipmentSlot.TacticalVest, + EquipmentSlot.ArmorVest, + EquipmentSlot.Scabbard, + EquipmentSlot.Eyewear, + EquipmentSlot.Headwear, + EquipmentSlot.Earpiece, + EquipmentSlot.ArmBand, + EquipmentSlot.FaceCover, + EquipmentSlot.Holster, + EquipmentSlot.SecuredContainer + }); + + foreach (var item in additionalItems) + { + // Some items are null, probably because bot doesnt have that particular slot on them + if (item == null) + { + continue; + } + + //Logger.LogError($"flagging item FiR: {item.Id} {item.Name} _parent: {item.Template._parent}"); + item.SpawnedInSession = false; + } + } + + } +} diff --git a/project/Aki.Custom/Patches/AddEnemyPatch.cs b/project/Aki.Custom/Patches/AddEnemyPatch.cs index 4fad0dd..7fbd8e9 100644 --- a/project/Aki.Custom/Patches/AddEnemyPatch.cs +++ b/project/Aki.Custom/Patches/AddEnemyPatch.cs @@ -16,11 +16,11 @@ namespace Aki.Custom.Patches protected override MethodBase GetTargetMethod() { - return typeof(BotGroupClass).GetMethod(methodName); + return typeof(BotZoneGroupsDictionary).GetMethod(methodName); } [PatchPrefix] - private static bool PatchPrefix(BotGroupClass __instance, IAIDetails person) + private static bool PatchPrefix(BotZoneGroupsDictionary __instance, IPlayer person) { var botOwners = (List)__instance.GetType().GetField("list_1", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance); if (botOwners.Any(x => x.Id == person.Id)) diff --git a/project/Aki.Custom/Patches/AddEnemyToAllGroupsInBotZonePatch.cs b/project/Aki.Custom/Patches/AddEnemyToAllGroupsInBotZonePatch.cs index 6705d36..304d18f 100644 --- a/project/Aki.Custom/Patches/AddEnemyToAllGroupsInBotZonePatch.cs +++ b/project/Aki.Custom/Patches/AddEnemyToAllGroupsInBotZonePatch.cs @@ -19,7 +19,7 @@ namespace Aki.Custom.Patches private bool IsTargetType(Type type) { - if (type.Name == nameof(BotControllerClass) && type.GetMethod(methodName) != null) + if (type.Name == nameof(BotsController) && type.GetMethod(methodName) != null) { return true; } @@ -42,7 +42,7 @@ namespace Aki.Custom.Patches /// This should fix that. /// [PatchPrefix] - private static bool PatchPrefix(BotControllerClass __instance, IAIDetails aggressor, IAIDetails groupOwner, IAIDetails target) + private static bool PatchPrefix(BotsController __instance, IPlayer aggressor, IPlayer groupOwner, IPlayer target) { BotZone botZone = groupOwner.AIData.BotOwner.BotsGroup.BotZone; foreach (var item in __instance.Groups()) @@ -63,7 +63,7 @@ namespace Aki.Custom.Patches && group.ShallRevengeFor(target) ) { - group.AddEnemy(aggressor); + group.AddEnemy(aggressor, EBotEnemyCause.AddEnemyToAllGroupsInBotZone); } } } diff --git a/project/Aki.Custom/Patches/BotDifficultyPatch.cs b/project/Aki.Custom/Patches/BotDifficultyPatch.cs index d1cb06f..a684ed6 100644 --- a/project/Aki.Custom/Patches/BotDifficultyPatch.cs +++ b/project/Aki.Custom/Patches/BotDifficultyPatch.cs @@ -1,7 +1,8 @@ +using Aki.Common.Http; using Aki.Reflection.Patching; using Aki.Reflection.Utils; -using Aki.Common.Http; using EFT; +using EFT.UI; using System.Linq; using System.Reflection; @@ -22,7 +23,13 @@ namespace Aki.Custom.Patches private static bool PatchPrefix(ref string __result, BotDifficulty botDifficulty, WildSpawnType role) { __result = RequestHandler.GetJson($"/singleplayer/settings/bot/difficulty/{role}/{botDifficulty}"); - return string.IsNullOrWhiteSpace(__result); + var resultIsNullEmpty = string.IsNullOrWhiteSpace(__result); + if (resultIsNullEmpty) + { + ConsoleScreen.LogError($"Unable to get difficulty settings for {role} {botDifficulty}"); + } + + return resultIsNullEmpty; // Server data returned = false = skip original method } } } diff --git a/project/Aki.Custom/Patches/BotEnemyTargetPatch.cs b/project/Aki.Custom/Patches/BotEnemyTargetPatch.cs index 7245cb5..6524d70 100644 --- a/project/Aki.Custom/Patches/BotEnemyTargetPatch.cs +++ b/project/Aki.Custom/Patches/BotEnemyTargetPatch.cs @@ -19,7 +19,7 @@ namespace Aki.Custom.Patches private bool IsTargetType(Type type) { - if (type.Name == nameof(BotControllerClass) && type.GetMethod(methodName) != null) + if (type.Name == nameof(BotsController) && type.GetMethod(methodName) != null) { Logger.LogInfo($"{methodName}: {type.FullName}"); return true; @@ -40,7 +40,7 @@ namespace Aki.Custom.Patches /// This should fix that. /// [PatchPrefix] - private static bool PatchPrefix(BotControllerClass __instance, IAIDetails aggressor, IAIDetails groupOwner, IAIDetails target) + private static bool PatchPrefix(BotsController __instance, IPlayer aggressor, IPlayer groupOwner, IPlayer target) { BotZone botZone = groupOwner.AIData.BotOwner.BotsGroup.BotZone; foreach (var item in __instance.Groups()) @@ -54,14 +54,14 @@ namespace Aki.Custom.Patches { if (!group.Enemies.ContainsKey(aggressor) && ShouldAttack(aggressor, target, group)) { - group.AddEnemy(aggressor); + group.AddEnemy(aggressor, EBotEnemyCause.AddEnemyToAllGroupsInBotZone); } } } return false; } - private static bool ShouldAttack(IAIDetails attacker, IAIDetails victim, BotGroupClass groupToCheck) + private static bool ShouldAttack(IPlayer attacker, IPlayer victim, BotsGroup groupToCheck) { // Group should target if player attack a victim on the same side or if the group is not on the same side as the player. bool shouldAttack = attacker.Side != groupToCheck.Side diff --git a/project/Aki.Custom/Patches/BotSelfEnemyPatch.cs b/project/Aki.Custom/Patches/BotSelfEnemyPatch.cs index 50481da..cc7ca9e 100644 --- a/project/Aki.Custom/Patches/BotSelfEnemyPatch.cs +++ b/project/Aki.Custom/Patches/BotSelfEnemyPatch.cs @@ -17,9 +17,9 @@ namespace Aki.Custom.Patches } [PatchPrefix] - private static bool PatchPrefix(BotOwner __instance, BotGroupClass group) + private static bool PatchPrefix(BotOwner __instance, BotsGroup group) { - IAIDetails selfToRemove = null; + IPlayer selfToRemove = null; foreach (var enemy in group.Enemies) { diff --git a/project/Aki.Custom/Patches/CheckAndAddEnemyPatch.cs b/project/Aki.Custom/Patches/CheckAndAddEnemyPatch.cs index b181647..54dd730 100644 --- a/project/Aki.Custom/Patches/CheckAndAddEnemyPatch.cs +++ b/project/Aki.Custom/Patches/CheckAndAddEnemyPatch.cs @@ -41,7 +41,7 @@ namespace Aki.Custom.Patches /// removes the !player.AIData.IsAI check /// [PatchPrefix] - private static bool PatchPrefix(BotGroupClass __instance, IAIDetails player, ref bool ignoreAI) + private static bool PatchPrefix(BotsGroup __instance, IPlayer player, ref bool ignoreAI) { // Z already has player as enemy BUT Enemies dict is empty, adding them again causes 'existing key' errors if (__instance.InitialBotType == WildSpawnType.bossZryachiy || __instance.InitialBotType == WildSpawnType.followerZryachiy) @@ -56,7 +56,7 @@ namespace Aki.Custom.Patches if (!__instance.Enemies.ContainsKey(player)) { - __instance.AddEnemy(player); + __instance.AddEnemy(player, EBotEnemyCause.checkAddTODO); } return false; // Skip original diff --git a/project/Aki.Custom/Patches/CustomAiPatch.cs b/project/Aki.Custom/Patches/CustomAiPatch.cs index dcd21d1..b28639e 100644 --- a/project/Aki.Custom/Patches/CustomAiPatch.cs +++ b/project/Aki.Custom/Patches/CustomAiPatch.cs @@ -1,79 +1,91 @@ -using Aki.Common.Http; -using Aki.Reflection.Patching; +using Aki.Reflection.Patching; using EFT; -using Newtonsoft.Json; using System; using Comfort.Common; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using Aki.PrePatch; -using Random = System.Random; +using Aki.Custom.CustomAI; namespace Aki.Custom.Patches { public class CustomAiPatch : ModulePatch { - private static readonly Random random = new Random(); - private static Dictionary>> botTypeCache = new Dictionary>>(); - private static DateTime cacheDate = new DateTime(); + private static readonly PmcFoundInRaidEquipment pmcFoundInRaidEquipment = new PmcFoundInRaidEquipment(Logger); + private static readonly AIBrainSpawnWeightAdjustment aIBrainSpawnWeightAdjustment = new AIBrainSpawnWeightAdjustment(Logger); protected override MethodBase GetTargetMethod() { - return typeof(BotBrainClass).GetMethod("Activate", BindingFlags.Public | BindingFlags.Instance); + return typeof(StandartBotBrain).GetMethod("Activate", BindingFlags.Public | BindingFlags.Instance); } /// /// Get a randomly picked wildspawntype from server and change PMC bot to use it, this ensures the bot is generated with that random type altering its behaviour + /// Postfix will adjust it back to original type /// /// state to save for postfix to use later /// /// botOwner_0 property [PatchPrefix] - private static bool PatchPrefix(out WildSpawnType __state, object __instance, BotOwner ___botOwner_0) + private static bool PatchPrefix(out WildSpawnType __state, StandartBotBrain __instance, BotOwner ___botOwner_0) { - // Store original type in state param - __state = ___botOwner_0.Profile.Info.Settings.Role; - //Console.WriteLine($"Processing bot {___botOwner_0.Profile.Info.Nickname} with role {___botOwner_0.Profile.Info.Settings.Role}"); + ___botOwner_0.Profile.Info.Settings.Role = FixAssaultGroupPmcsRole(___botOwner_0); + __state = ___botOwner_0.Profile.Info.Settings.Role; // Store original type in state param to allow access in PatchPostFix() try { - if (BotIsSptPmc(___botOwner_0.Profile.Info.Settings.Role)) + if (AiHelpers.BotIsPlayerScav(__state, ___botOwner_0)) { - string currentMapName = GetCurrentMap(); + ___botOwner_0.Profile.Info.Settings.Role = aIBrainSpawnWeightAdjustment.GetRandomisedPlayerScavType(); - if (!botTypeCache.TryGetValue(___botOwner_0.Profile.Info.Settings.Role, out var botSettings) || CacheIsStale()) + return true; // Do original + } + string currentMapName = GetCurrentMap(); + if (AiHelpers.BotIsNormalAssaultScav(__state, ___botOwner_0)) + { + ___botOwner_0.Profile.Info.Settings.Role = aIBrainSpawnWeightAdjustment.GetAssaultScavWildSpawnType(___botOwner_0, currentMapName); + + return true; // Do original + } + + if (AiHelpers.BotIsSptPmc(__state, ___botOwner_0)) + { + // Bot has inventory equipment + if (___botOwner_0.Profile?.Inventory?.Equipment != null) { - ResetCacheDate(); - HydrateCacheWithServerData(); - - if (!botTypeCache.TryGetValue(___botOwner_0.Profile.Info.Settings.Role, out botSettings)) - { - throw new Exception($"Bots were refreshed from the server but the cache still doesnt contain an appropriate bot for type {___botOwner_0.Profile.Info.Settings.Role}"); - } + pmcFoundInRaidEquipment.ConfigurePMCFindInRaidStatus(___botOwner_0); } - var mapSettings = botSettings[currentMapName.ToLower()]; - var randomType = WeightedRandom(mapSettings.Keys.ToArray(), mapSettings.Values.ToArray()); - if (Enum.TryParse(randomType, out WildSpawnType newAiType)) - { - Logger.LogWarning($"Updated spt bot {___botOwner_0.Profile.Info.Nickname}: {___botOwner_0.Profile.Info.Settings.Role} to use: {newAiType} brain"); - ___botOwner_0.Profile.Info.Settings.Role = newAiType; - } - else - { - Logger.LogError($"Couldnt not update spt bot {___botOwner_0.Profile.Info.Nickname} to random type {randomType}, does not exist for WildSpawnType enum"); - } + ___botOwner_0.Profile.Info.Settings.Role = aIBrainSpawnWeightAdjustment.GetPmcWildSpawnType(___botOwner_0, ___botOwner_0.Profile.Info.Settings.Role, currentMapName); } } catch (Exception ex) { - Logger.LogError($"Error processing log: {ex.Message}"); + Logger.LogError($"Error running CustomAiPatch PatchPrefix(): {ex.Message}"); Logger.LogError(ex.StackTrace); } return true; // Do original } + /// + /// the client sometimes replaces PMC roles with 'assaultGroup', give PMCs their original role back (sptBear/sptUsec) + /// + /// WildSpawnType + private static WildSpawnType FixAssaultGroupPmcsRole(BotOwner botOwner) + { + if (botOwner.Profile.Info.IsStreamerModeAvailable && botOwner.Profile.Info.Settings.Role == WildSpawnType.assaultGroup) + { + Logger.LogError($"Broken PMC found: {botOwner.Profile.Nickname}, was {botOwner.Profile.Info.Settings.Role}"); + + // Its a PMC, figure out what the bot originally was and return it + return botOwner.Profile.Info.Side == EPlayerSide.Bear + ? (WildSpawnType)AkiBotsPrePatcher.sptBearValue + : (WildSpawnType)AkiBotsPrePatcher.sptUsecValue; + } + + // Not broken pmc, return original role + return botOwner.Profile.Info.Settings.Role; + } + /// /// Revert prefix change, get bots type back to what it was before changes /// @@ -82,16 +94,16 @@ namespace Aki.Custom.Patches [PatchPostfix] private static void PatchPostFix(WildSpawnType __state, BotOwner ___botOwner_0) { - if (BotIsSptPmc(__state)) + if (AiHelpers.BotIsSptPmc(__state, ___botOwner_0)) { // Set spt bot bot back to original type ___botOwner_0.Profile.Info.Settings.Role = __state; } - } - - private static bool BotIsSptPmc(WildSpawnType role) - { - return (int)role == AkiBotsPrePatcher.sptBearValue || (int)role == AkiBotsPrePatcher.sptUsecValue; + else if (AiHelpers.BotIsPlayerScav(__state, ___botOwner_0)) + { + // Set pscav back to original type + ___botOwner_0.Profile.Info.Settings.Role = __state; + } } private static string GetCurrentMap() @@ -100,50 +112,5 @@ namespace Aki.Custom.Patches return gameWorld.MainPlayer.Location; } - - private static bool CacheIsStale() - { - TimeSpan cacheAge = DateTime.Now - cacheDate; - - return cacheAge.Minutes > 20; - } - - private static void ResetCacheDate() - { - cacheDate = DateTime.Now; - } - - private static void HydrateCacheWithServerData() - { - // Get weightings for PMCs from server and store in dict - var result = RequestHandler.GetJson($"/singleplayer/settings/bot/getBotBehaviours/"); - botTypeCache = JsonConvert.DeserializeObject>>>(result); - Logger.LogWarning($"Cached bot.json/pmcType PMC brain weights in client"); - } - - private static string WeightedRandom(string[] botTypes, int[] weights) - { - var cumulativeWeights = new int[botTypes.Length]; - - for (int i = 0; i < weights.Length; i++) - { - cumulativeWeights[i] = weights[i] + (i == 0 ? 0 : cumulativeWeights[i - 1]); - } - - var maxCumulativeWeight = cumulativeWeights[cumulativeWeights.Length - 1]; - var randomNumber = maxCumulativeWeight * random.NextDouble(); - - for (var itemIndex = 0; itemIndex < botTypes.Length; itemIndex++) - { - if (cumulativeWeights[itemIndex] >= randomNumber) - { - return botTypes[itemIndex]; - } - } - - Logger.LogError("failed to get random bot weighting, returned assault"); - - return "assault"; - } } } diff --git a/project/Aki.Custom/Patches/EasyAssetsPatch.cs b/project/Aki.Custom/Patches/EasyAssetsPatch.cs index d04172a..d77d9c3 100644 --- a/project/Aki.Custom/Patches/EasyAssetsPatch.cs +++ b/project/Aki.Custom/Patches/EasyAssetsPatch.cs @@ -30,6 +30,8 @@ namespace Aki.Custom.Patches _manifestField = type.GetField(nameof(EasyAssets.Manifest)); _bundlesField = type.GetField($"{EasyBundleHelper.Type.Name.ToLowerInvariant()}_0", PatchConstants.PrivateFlags); + + // DependencyGraph _systemProperty = type.GetProperty("System"); } @@ -82,7 +84,15 @@ namespace Aki.Custom.Patches for (var i = 0; i < bundleNames.Length; i++) { - bundles[i] = (IEasyBundle)Activator.CreateInstance(EasyBundleHelper.Type, new object[] { bundleNames[i], path, manifest, bundleLock, bundleCheck }); + bundles[i] = (IEasyBundle)Activator.CreateInstance(EasyBundleHelper.Type, new object[] + { + bundleNames[i], + path, + manifest, + bundleLock, + bundleCheck + }); + await JobScheduler.Yield(EJobPriority.Immediate); } diff --git a/project/Aki.Custom/Patches/ExitWhileLootingPatch.cs b/project/Aki.Custom/Patches/ExitWhileLootingPatch.cs index 84b88ec..9ea2c1e 100644 --- a/project/Aki.Custom/Patches/ExitWhileLootingPatch.cs +++ b/project/Aki.Custom/Patches/ExitWhileLootingPatch.cs @@ -33,7 +33,7 @@ namespace Aki.Custom.Patches var player = Singleton.Instance.MainPlayer; if (profileId == player?.Profile.Id) { - GClass2977.Instance.CloseAllScreensForced(); + GClass2897.Instance.CloseAllScreensForced(); } return true; diff --git a/project/Aki.Custom/Patches/IsEnemyPatch.cs b/project/Aki.Custom/Patches/IsEnemyPatch.cs index 273ed3d..584d469 100644 --- a/project/Aki.Custom/Patches/IsEnemyPatch.cs +++ b/project/Aki.Custom/Patches/IsEnemyPatch.cs @@ -39,22 +39,22 @@ namespace Aki.Custom.Patches /// Needed to ensure bot checks the enemy side, not just its botType /// [PatchPrefix] - private static bool PatchPrefix(ref bool __result, BotGroupClass __instance, IAIDetails requester) + private static bool PatchPrefix(ref bool __result, BotsGroup __instance, IPlayer requester) { var isEnemy = false; // default not an enemy if (requester == null) { __result = isEnemy; - return true; // Skip original + return false; // Skip original } // Check existing enemies list // Could also check x.Value.Player?.Id - BSG do it this way - if (!__instance.Enemies.IsNullOrEmpty() && __instance.Enemies.Any(x=> x.Key.Id == requester.Id)) + if (!__instance.Enemies.IsNullOrEmpty() && __instance.Enemies.Any(x => x.Key.Id == requester.Id)) { __result = true; - return true; + return false; // Skip original } else { @@ -62,7 +62,7 @@ namespace Aki.Custom.Patches // Make zryachiy use existing isEnemy() code if (__instance.InitialBotType == WildSpawnType.bossZryachiy) { - return false; // do original method + return false; // Skip original } if (__instance.Side == EPlayerSide.Usec) @@ -71,7 +71,7 @@ namespace Aki.Custom.Patches ShouldAttackUsec(requester)) { isEnemy = true; - __instance.AddEnemy(requester); + __instance.AddEnemy(requester, EBotEnemyCause.checkAddTODO); } } else if (__instance.Side == EPlayerSide.Bear) @@ -80,23 +80,28 @@ namespace Aki.Custom.Patches ShouldAttackBear(requester)) { isEnemy = true; - __instance.AddEnemy(requester); + __instance.AddEnemy(requester, EBotEnemyCause.checkAddTODO); } } else if (__instance.Side == EPlayerSide.Savage) { if (requester.Side != EPlayerSide.Savage) { + //Lets exUsec warn Usecs and fire at will at Bears + if (__instance.InitialBotType == WildSpawnType.exUsec) + { + return true; // Let BSG handle things + } // everyone else is an enemy to savage (scavs) isEnemy = true; - __instance.AddEnemy(requester); + __instance.AddEnemy(requester, EBotEnemyCause.checkAddTODO); } } } __result = isEnemy; - return true; // Skip original + return false; // Skip original } /// @@ -104,7 +109,7 @@ namespace Aki.Custom.Patches /// /// /// bool - private static bool ShouldAttackUsec(IAIDetails requester) + private static bool ShouldAttackUsec(IPlayer requester) { var requesterMind = requester?.AIData?.BotOwner?.Settings?.FileSettings?.Mind; @@ -121,7 +126,7 @@ namespace Aki.Custom.Patches /// /// /// - private static bool ShouldAttackBear(IAIDetails requester) + private static bool ShouldAttackBear(IPlayer requester) { var requesterMind = requester.AIData?.BotOwner?.Settings?.FileSettings?.Mind; diff --git a/project/Aki.Custom/Patches/LocationLootCacheBustingPatch.cs b/project/Aki.Custom/Patches/LocationLootCacheBustingPatch.cs new file mode 100644 index 0000000..4c68fd0 --- /dev/null +++ b/project/Aki.Custom/Patches/LocationLootCacheBustingPatch.cs @@ -0,0 +1,40 @@ +using Aki.Reflection.Patching; +using Aki.Reflection.Utils; +using System; +using System.Linq; +using System.Reflection; + +namespace Aki.Custom.Patches +{ + /// + /// BaseLocalGame appears to cache a maps loot data and reuse it when the variantId from method_6 is the same, this patch exits the method early, never caching the data + /// + public class LocationLootCacheBustingPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + var desiredType = PatchConstants.EftTypes.Single(x => x.Name == "LocalGame").BaseType; // BaseLocalGame + var desiredMethod = desiredType.GetMethods(PatchConstants.PrivateFlags).Single(x => IsTargetMethod(x)); // method_6 + + Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); + Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}"); + + return desiredMethod; + } + + private static bool IsTargetMethod(MethodInfo mi) + { + var parameters = mi.GetParameters(); + return parameters.Length == 3 + && parameters[0].Name == "backendUrl" + && parameters[1].Name == "locationId" + && parameters[2].Name == "variantId"; + } + + [PatchPrefix] + private static bool PatchPrefix() + { + return false; // skip original + } + } +} diff --git a/project/Aki.Custom/Patches/PmcFirstAidPatch.cs b/project/Aki.Custom/Patches/PmcFirstAidPatch.cs index 5299a1b..0dde0c8 100644 --- a/project/Aki.Custom/Patches/PmcFirstAidPatch.cs +++ b/project/Aki.Custom/Patches/PmcFirstAidPatch.cs @@ -1,5 +1,8 @@ using Aki.Reflection.Patching; +using Aki.Reflection.Utils; using EFT; +using System; +using System.Linq; using System.Reflection; namespace Aki.Custom.Patches @@ -10,11 +13,30 @@ namespace Aki.Custom.Patches /// public class PmcFirstAidPatch : ModulePatch { + private static Type _targetType; private static readonly string methodName = "FirstAidApplied"; + public PmcFirstAidPatch() + { + _targetType = PatchConstants.EftTypes.FirstOrDefault(IsTargetType); + } + protected override MethodBase GetTargetMethod() { - return typeof(GClass399).GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic); + return _targetType.GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic); + } + + /// + /// GCLass350 for client version 25782 + /// + private bool IsTargetType(Type type) + { + if (type.GetMethod("GetHpPercent") != null && type.GetMethod("TryApplyToCurrentPart") != null) + { + return true; + } + + return false; } [PatchPrefix] diff --git a/project/Aki.Custom/Patches/RagfairFeePatch.cs b/project/Aki.Custom/Patches/RagfairFeePatch.cs new file mode 100644 index 0000000..2efd181 --- /dev/null +++ b/project/Aki.Custom/Patches/RagfairFeePatch.cs @@ -0,0 +1,49 @@ +using Aki.Common.Http; +using Aki.Reflection.Patching; +using Aki.Reflection.Utils; +using EFT.InventoryLogic; +using EFT.UI.Ragfair; +using System.Reflection; +using UnityEngine; + +namespace Aki.Custom.Patches +{ + /// + /// Send the tax amount for listing an item for sale on flea by player to server for later use when charging player + /// Client doesnt send this data and calculating it server-side isn't accurate + /// + public class RagfairFeePatch : ModulePatch + { + public RagfairFeePatch() + { + // Remember to update prefix parameter if below lines are broken + _ = nameof(GClass2859.IsAllSelectedItemSame); + _ = nameof(GClass2859.AutoSelectSimilar); + } + + protected override MethodBase GetTargetMethod() + { + return typeof(AddOfferWindow).GetMethod("method_1", PatchConstants.PrivateFlags); + } + + /// + /// Calculate tax to charge player and send to server before the offer is sent + /// + /// Item sold + /// OfferItemCount + /// RequirementsPrice + /// SellInOnePiece + [PatchPrefix] + private static void PatchPrefix(ref Item ___item_0, ref GClass2859 ___gclass2859_0, ref double ___double_0, ref bool ___bool_0) + { + RequestHandler.PutJson("/client/ragfair/offerfees", new + { + id = ___item_0.Id, + tpl = ___item_0.TemplateId, + count = ___gclass2859_0.OfferItemCount, + fee = Mathf.CeilToInt((float)GClass1940.CalculateTaxPrice(___item_0, ___gclass2859_0.OfferItemCount, ___double_0, ___bool_0)) + } + .ToJson()); + } + } +} \ No newline at end of file diff --git a/project/Aki.Custom/Patches/RankPanelPatch.cs b/project/Aki.Custom/Patches/RankPanelPatch.cs new file mode 100644 index 0000000..2a445e1 --- /dev/null +++ b/project/Aki.Custom/Patches/RankPanelPatch.cs @@ -0,0 +1,37 @@ +using Aki.Reflection.Patching; +using Aki.Reflection.Utils; +using Comfort.Common; +using EFT; +using EFT.UI; +using System.Reflection; + +namespace Aki.Custom.Patches +{ + public class RankPanelPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + var desiredType = typeof(RankPanel); + var desiredMethod = desiredType.GetMethod("Show", PatchConstants.PublicFlags); + + Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); + Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}"); + + return desiredMethod; + } + + [PatchPrefix] + private static bool PatchPreFix(ref int rankLevel, ref int maxRank) + { + if (Singleton.Instance != null) + { + Logger.LogWarning("Rank Level: " + rankLevel.ToString() + " Max Rank Level: " + maxRank.ToString()); + ConsoleScreen.LogError("Rank Level: " + rankLevel.ToString() + " Max Rank Level: " + maxRank.ToString()); + ConsoleScreen.LogError("Game Broke!"); + Logger.LogWarning("This Shouldn't happen!! Please report this in discord"); + ConsoleScreen.LogError("This Shouldn't happen!! Please report this in discord"); + } + return true; + } + } +} \ No newline at end of file diff --git a/project/Aki.Custom/Patches/SettingsLocationPatch.cs b/project/Aki.Custom/Patches/SettingsLocationPatch.cs new file mode 100644 index 0000000..1d9f08f --- /dev/null +++ b/project/Aki.Custom/Patches/SettingsLocationPatch.cs @@ -0,0 +1,30 @@ +using Aki.Reflection.Patching; +using HarmonyLib; +using System; +using System.IO; +using System.Reflection; + +namespace Aki.Custom.Patches +{ + public class SettingsLocationPatch : ModulePatch + { + private static string _sptPath = Path.Combine(Environment.CurrentDirectory, "user", "sptSettings"); + + protected override MethodBase GetTargetMethod() + { + return AccessTools.Constructor(typeof(SharedGameSettingsClass)); + } + + [PatchPrefix] + internal static void PatchPrefix(ref string ___string_0, ref string ___string_1) + { + if (!Directory.Exists(_sptPath)) + { + Directory.CreateDirectory(_sptPath); + } + + ___string_0 = _sptPath; + ___string_1 = _sptPath; + } + } +} diff --git a/project/Aki.Custom/Patches/VersionLabelPatch.cs b/project/Aki.Custom/Patches/VersionLabelPatch.cs index 169275d..9feaba3 100644 --- a/project/Aki.Custom/Patches/VersionLabelPatch.cs +++ b/project/Aki.Custom/Patches/VersionLabelPatch.cs @@ -7,6 +7,7 @@ using EFT.UI; using HarmonyLib; using System.Linq; using System.Reflection; +using Comfort.Common; namespace Aki.Custom.Patches { @@ -40,8 +41,8 @@ namespace Aki.Custom.Patches Logger.LogInfo($"Server version: {_versionLabel}"); } - Traverse.Create(MonoBehaviourSingleton.Instance).Field("_alphaVersionLabel").Property("LocalizationKey").SetValue("{0}"); - Traverse.Create(MonoBehaviourSingleton.Instance).Field("string_2").SetValue(_versionLabel); + Traverse.Create(Singleton.Instance).Field("_alphaVersionLabel").Property("LocalizationKey").SetValue("{0}"); + Traverse.Create(Singleton.Instance).Field("string_2").SetValue(_versionLabel); Traverse.Create(__result).Field("Major").SetValue(_versionLabel); } } diff --git a/project/Aki.Custom/Utils/EasyBundleHelper.cs b/project/Aki.Custom/Utils/EasyBundleHelper.cs index 7a8b0b6..f3fdf40 100644 --- a/project/Aki.Custom/Utils/EasyBundleHelper.cs +++ b/project/Aki.Custom/Utils/EasyBundleHelper.cs @@ -4,13 +4,14 @@ using System.Linq; using System.Reflection; using System.Threading.Tasks; using Aki.Reflection.Utils; +using UnityEngine; using BindableState = BindableState; namespace Aki.Custom.Utils { public class EasyBundleHelper { - private const BindingFlags _flags = BindingFlags.Instance | BindingFlags.NonPublic; + private const BindingFlags _NonPublicInstanceflags = BindingFlags.Instance | BindingFlags.NonPublic; private static readonly FieldInfo _pathField; private static readonly FieldInfo _keyWithoutExtensionField; private static readonly FieldInfo _bundleLockField; @@ -26,14 +27,24 @@ namespace Aki.Custom.Utils _ = nameof(IBundleLock.IsLocked); _ = nameof(BindableState.Bind); - Type = PatchConstants.EftTypes.Single(x => x.GetMethod("set_SameNameAsset", _flags) != null); - _pathField = Type.GetField("string_1", _flags); - _keyWithoutExtensionField = Type.GetField("string_0", _flags); - _bundleLockField = Type.GetFields(_flags).FirstOrDefault(x => x.FieldType == typeof(IBundleLock)); + // Class can be found as a private array inside EasyAssets.cs, next to DependencyGraph + Type = PatchConstants.EftTypes.Single(x => x.GetMethod("set_SameNameAsset", _NonPublicInstanceflags) != null); + + _pathField = Type.GetField("string_1", _NonPublicInstanceflags); + _keyWithoutExtensionField = Type.GetField("string_0", _NonPublicInstanceflags); + _bundleLockField = Type.GetFields(_NonPublicInstanceflags).FirstOrDefault(x => x.FieldType == typeof(IBundleLock)); _dependencyKeysProperty = Type.GetProperty("DependencyKeys"); _keyProperty = Type.GetProperty("Key"); _loadStateProperty = Type.GetProperty("LoadState"); - _loadingCoroutineMethod = Type.GetMethods(_flags).Single(x => x.GetParameters().Length == 0 && x.ReturnType == typeof(Task)); + + // Function with 0 params and returns task (usually method_0()) + var possibleMethods = Type.GetMethods(_NonPublicInstanceflags).Where(x => x.GetParameters().Length == 0 && x.ReturnType == typeof(Task)); + if (possibleMethods.Count() > 1) + { + Console.WriteLine($"Unable to find desired method as there are multiple possible matches: {string.Join(",", possibleMethods.Select(x => x.Name))}"); + } + + _loadingCoroutineMethod = possibleMethods.SingleOrDefault(); } public EasyBundleHelper(object easyBundle) @@ -113,9 +124,9 @@ namespace Aki.Custom.Utils } } - public Task LoadingCoroutine() + public Task LoadingCoroutine(Dictionary bundles) { - return (Task)_loadingCoroutineMethod.Invoke(_instance, new object[] { }); + return (Task)_loadingCoroutineMethod.Invoke(_instance, new object[] { bundles }); } } } diff --git a/project/Aki.Debugging/Aki.Debugging.csproj b/project/Aki.Debugging/Aki.Debugging.csproj index 0f516c8..3958538 100644 --- a/project/Aki.Debugging/Aki.Debugging.csproj +++ b/project/Aki.Debugging/Aki.Debugging.csproj @@ -12,6 +12,7 @@ + diff --git a/project/Aki.Debugging/AkiDebuggingPlugin.cs b/project/Aki.Debugging/AkiDebuggingPlugin.cs index 9267f2a..02fe9b2 100644 --- a/project/Aki.Debugging/AkiDebuggingPlugin.cs +++ b/project/Aki.Debugging/AkiDebuggingPlugin.cs @@ -15,6 +15,7 @@ namespace Aki.Debugging try { // new CoordinatesPatch().Enable(); + new EndRaidDebug().Enable(); } catch (Exception ex) { diff --git a/project/Aki.Debugging/Patches/EndRaidDebug.cs b/project/Aki.Debugging/Patches/EndRaidDebug.cs new file mode 100644 index 0000000..909695c --- /dev/null +++ b/project/Aki.Debugging/Patches/EndRaidDebug.cs @@ -0,0 +1,57 @@ +using Aki.Reflection.Patching; +using System.Reflection; +using Aki.Reflection.Utils; +using BepInEx.Logging; +using EFT; +using EFT.UI; +using TMPro; + +namespace Aki.Debugging.Patches +{ + public class EndRaidDebug : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return typeof(TraderCard).GetMethod("method_0", PatchConstants.PrivateFlags); + } + + [PatchPrefix] + private static bool PatchPreFix(ref LocalizedText ____nickName, ref TMP_Text ____standing, + ref RankPanel ____rankPanel, ref Profile.GClass1625 ___gclass1625_0) + { + if (____nickName.LocalizationKey == null) + { + ConsoleScreen.LogError("This Shouldn't happen!! Please report this in discord"); + Logger.Log(LogLevel.Error, "[AKI] _nickName.LocalizationKey was null"); + } + + if (____standing.text == null) + { + ConsoleScreen.LogError("This Shouldn't happen!! Please report this in discord"); + Logger.Log(LogLevel.Error, "[AKI] _standing.text was null"); + } + + if (____rankPanel == null) + { + ConsoleScreen.LogError("This Shouldn't happen!! Please report this in discord"); + Logger.Log(LogLevel.Error, "[AKI] _rankPanel was null, skipping method_0"); + + return false; // skip original + } + + if (___gclass1625_0?.LoyaltyLevel == null) + { + ConsoleScreen.LogError("This Shouldn't happen!! Please report this in discord"); + Logger.Log(LogLevel.Error, "[AKI] _gclass1618_0 or _gclass1618_0.LoyaltyLevel was null"); + } + + if (___gclass1625_0?.MaxLoyaltyLevel == null) + { + ConsoleScreen.LogError("This Shouldn't happen!! Please report this in discord"); + Logger.Log(LogLevel.Error, "[AKI] _gclass1618_0 or _gclass1618_0.MaxLoyaltyLevel was null"); + } + + return true; + } + } +} \ No newline at end of file diff --git a/project/Aki.PrePatch/AkiBotsPrePatcher.cs b/project/Aki.PrePatch/AkiBotsPrePatcher.cs index cc88f99..d2b6d71 100644 --- a/project/Aki.PrePatch/AkiBotsPrePatcher.cs +++ b/project/Aki.PrePatch/AkiBotsPrePatcher.cs @@ -7,8 +7,8 @@ namespace Aki.PrePatch { public static IEnumerable TargetDLLs { get; } = new[] { "Assembly-CSharp.dll" }; - public static int sptUsecValue = 34; - public static int sptBearValue = 35; + public static int sptUsecValue = 38; + public static int sptBearValue = 39; public static void Patch(ref AssemblyDefinition assembly) { diff --git a/project/Aki.SinglePlayer/AkiSingleplayerPlugin.cs b/project/Aki.SinglePlayer/AkiSingleplayerPlugin.cs index 9e2a0b1..8bd770d 100644 --- a/project/Aki.SinglePlayer/AkiSingleplayerPlugin.cs +++ b/project/Aki.SinglePlayer/AkiSingleplayerPlugin.cs @@ -53,9 +53,11 @@ namespace Aki.SinglePlayer new PluginErrorNotifierPatch().Enable(); new SpawnProcessNegativeValuePatch().Enable(); new InsuredItemManagerStartPatch().Enable(); + new MapReadyButtonPatch().Enable(); } catch (Exception ex) { + Logger.LogError($"A PATCH IN {GetType().Name} FAILED. SUBSEQUENT PATCHES HAVE NOT LOADED"); Logger.LogError($"{GetType().Name}: {ex}"); throw; } diff --git a/project/Aki.SinglePlayer/Models/Progression/LighthouseProgressionClass.cs b/project/Aki.SinglePlayer/Models/Progression/LighthouseProgressionClass.cs index 65ca0cd..32811c1 100644 --- a/project/Aki.SinglePlayer/Models/Progression/LighthouseProgressionClass.cs +++ b/project/Aki.SinglePlayer/Models/Progression/LighthouseProgressionClass.cs @@ -8,14 +8,13 @@ namespace Aki.SinglePlayer.Models.Progression { public class LighthouseProgressionClass : MonoBehaviour { - private bool _isScav; private GameWorld _gameWorld; private Player _player; private float _timer; private bool _playerFlaggedAsEnemyToBosses; private List _bridgeMines; private RecodableItemClass _transmitter; - private readonly List _zryachiyAndFollowers = new List(); + private readonly List _zryachiyAndFollowers = new List(); private bool _aggressor; private bool _isDoorDisabled; private readonly string _transmitterId = "62e910aaf957f2915e0a5e36"; @@ -26,34 +25,37 @@ namespace Aki.SinglePlayer.Models.Progression _gameWorld = Singleton.Instance; _player = _gameWorld?.MainPlayer; - // Exit if not on lighthouse - if (_gameWorld == null || !string.Equals(_player.Location, "lighthouse", System.StringComparison.OrdinalIgnoreCase)) + if (_gameWorld == null || _player == null) { + Destroy(this); + return; } + // Get transmitter from players inventory + _transmitter = GetTransmitterFromInventory(); + + // Exit if transmitter does not exist and isnt green + if (!PlayerHasActiveTransmitterInInventory()) + { + Destroy(this); + + return; + } + + GameObject.Find("Attack").SetActive(false); + + // Zone was added in a newer version and the gameObject actually has a \ + GameObject.Find("CloseZone\\").SetActive(false); + + // Give access to Lightkeepers door + _gameWorld.BufferZoneController.SetPlayerAccessStatus(_player.ProfileId, true); + // Expensive, run after gameworld / lighthouse checks above _bridgeMines = FindObjectsOfType().ToList(); - // Player is a scav, exit - if (_player.Side == EPlayerSide.Savage) - { - _isScav = true; - - return; - } - - _transmitter = GetTransmitterFromInventory(); - if (PlayerHasTransmitterInInventory()) - { - GameObject.Find("Attack").SetActive(false); - - // Zone was added in a newer version and the gameObject actually has a \ - GameObject.Find("CloseZone\\").SetActive(false); - - // Give access to Lightkeepers door - _gameWorld.BufferZoneController.SetPlayerAccessStatus(_player.ProfileId, true); - } + // Set mines to be non-active + SetBridgeMinesStatus(false); } public void Update() @@ -82,37 +84,28 @@ namespace Aki.SinglePlayer.Models.Progression SetupZryachiyAndFollowerHostility(); } - if (_isScav) - { - MakeZryachiyAndFollowersHostileToPlayer(); - - return; - } - - // (active/green) - if (PlayerHasActiveTransmitterInHands()) - { - SetBridgeMinesStatus(false); - } - else - { - SetBridgeMinesStatus(true); - } - + // If player becomes aggressor, block access to LK if (_aggressor) { DisableAccessToLightKeeper(); } } + /// + /// Gets transmitter from players inventory + /// private RecodableItemClass GetTransmitterFromInventory() { - return (RecodableItemClass)_player.Profile.Inventory.AllRealPlayerItems.FirstOrDefault(x => x.TemplateId == _transmitterId); + return (RecodableItemClass) _player.Profile.Inventory.AllRealPlayerItems.FirstOrDefault(x => x.TemplateId == _transmitterId); } - private bool PlayerHasTransmitterInInventory() + /// + /// Checks for transmitter status and exists in players inventory + /// + private bool PlayerHasActiveTransmitterInInventory() { - return _transmitter != null; + return _transmitter != null && + _transmitter?.RecodableComponent?.Status == RadioTransmitterStatus.Green; } /// @@ -123,12 +116,6 @@ namespace Aki.SinglePlayer.Models.Progression _timer += Time.deltaTime; } - private bool PlayerHasActiveTransmitterInHands() - { - return _gameWorld?.MainPlayer?.HandsController?.Item?.TemplateId == _transmitterId - && _transmitter?.RecodableComponent?.Status == RadioTransmitterStatus.Green; - } - /// /// Set all brdige mines to desire state /// @@ -142,11 +129,21 @@ namespace Aki.SinglePlayer.Models.Progression } } + /// + /// Put Zryachiy and followers into a list and sub to their death event + /// Make player agressor if player kills them. + /// private void SetupZryachiyAndFollowerHostility() { // only process non-players (ai) foreach (var aiBot in _gameWorld.AllAlivePlayersList.Where(x => !x.IsYourPlayer)) { + // Bots that die on mounted guns get stuck in AllAlivePlayersList, need to check health + if (!aiBot.HealthController.IsAlive) + { + continue; + } + // Edge case of bossZryachiy not being hostile to player if (aiBot.AIData.BotOwner.IsRole(WildSpawnType.bossZryachiy) || aiBot.AIData.BotOwner.IsRole(WildSpawnType.followerZryachiy)) { @@ -171,21 +168,6 @@ namespace Aki.SinglePlayer.Models.Progression } } - /// - /// Iterate over bots gathered from SetupZryachiyHostility() - /// - private void MakeZryachiyAndFollowersHostileToPlayer() - { - // If player is a scav, they must be added to the bosses enemy list otherwise they wont kill them - foreach (var bot in _zryachiyAndFollowers) - { - bot.AIData.BotOwner.BotsGroup.CheckAndAddEnemy(_player); - } - - // Flag player was added to enemy list - _playerFlaggedAsEnemyToBosses = true; - } - /// /// Disable door + set transmitter to 'red' /// diff --git a/project/Aki.SinglePlayer/Patches/Healing/MainMenuControllerPatch.cs b/project/Aki.SinglePlayer/Patches/Healing/MainMenuControllerPatch.cs index 3643e68..1e801d3 100644 --- a/project/Aki.SinglePlayer/Patches/Healing/MainMenuControllerPatch.cs +++ b/project/Aki.SinglePlayer/Patches/Healing/MainMenuControllerPatch.cs @@ -1,5 +1,6 @@ using Aki.Reflection.Patching; using Aki.Reflection.Utils; +using EFT.HealthSystem; using System.Reflection; namespace Aki.SinglePlayer.Patches.Healing diff --git a/project/Aki.SinglePlayer/Patches/MainMenu/InsuranceScreenPatch.cs b/project/Aki.SinglePlayer/Patches/MainMenu/InsuranceScreenPatch.cs index 1ce655f..84bb6dd 100644 --- a/project/Aki.SinglePlayer/Patches/MainMenu/InsuranceScreenPatch.cs +++ b/project/Aki.SinglePlayer/Patches/MainMenu/InsuranceScreenPatch.cs @@ -16,8 +16,23 @@ namespace Aki.SinglePlayer.Patches.MainMenu protected override MethodBase GetTargetMethod() { + //[CompilerGenerated] + //private void method_67() + //{ + // if (this.raidSettings_0.SelectedLocation.Id == "laboratory") + // { + // this.raidSettings_0.WavesSettings.IsBosses = true; + // } + // if (this.raidSettings_0.RaidMode == ERaidMode.Online) + // { + // this.method_40(); + // return; + // } + // this.method_41(); + //} + var desiredType = typeof(MainMenuController); - var desiredMethod = desiredType.GetMethod("method_66", BindingFlags.NonPublic | BindingFlags.Instance); + var desiredMethod = desiredType.GetMethod("method_69", BindingFlags.NonPublic | BindingFlags.Instance); Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}"); diff --git a/project/Aki.SinglePlayer/Patches/MainMenu/MapReadyButtonPatch.cs b/project/Aki.SinglePlayer/Patches/MainMenu/MapReadyButtonPatch.cs new file mode 100644 index 0000000..de2094f --- /dev/null +++ b/project/Aki.SinglePlayer/Patches/MainMenu/MapReadyButtonPatch.cs @@ -0,0 +1,26 @@ +using System.Reflection; +using Aki.Reflection.Patching; +using Aki.Reflection.Utils; +using EFT.UI; +using EFT.UI.Matchmaker; + +namespace Aki.SinglePlayer.Patches.MainMenu +{ + /// + /// Removes the 'ready' button from the map preview screen - accessible by choosing map to deply to > clicking 'map' bottom left of screen + /// Clicking the ready button makes a call to client/match/available, something we dont want + /// + public class MapReadyButtonPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return typeof(MatchmakerMapPointsScreen).GetMethod("Show", PatchConstants.PrivateFlags); + } + + [PatchPostfix] + private static void PatchPostFix(ref DefaultUIButton ____readyButton) + { + ____readyButton?.GameObject?.SetActive(false); + } + } +} \ No newline at end of file diff --git a/project/Aki.SinglePlayer/Patches/MainMenu/PluginErrorNotifierPatch.cs b/project/Aki.SinglePlayer/Patches/MainMenu/PluginErrorNotifierPatch.cs index e4a2547..c8ea38b 100644 --- a/project/Aki.SinglePlayer/Patches/MainMenu/PluginErrorNotifierPatch.cs +++ b/project/Aki.SinglePlayer/Patches/MainMenu/PluginErrorNotifierPatch.cs @@ -61,14 +61,13 @@ namespace Aki.SinglePlayer.Patches.MainMenu // Show an error in the in-game console, we have to write this in reverse order because the // in-game console shows newer messages at the top - ConsoleScreen consoleScreen = MonoBehaviourSingleton.Instance.Console; foreach (string line in errorMessage.Split('\n').Reverse()) { if (line.Length > 0) { // Note: We directly call the internal Log method to work around a bug in 'LogError' that passes an empty string // as the StackTrace parameter, which results in extra newlines being added to the console logs - _directLogMethod.Invoke(consoleScreen, new object[] { line, null, LogType.Error }); + ConsoleScreen.LogError(line); } } diff --git a/project/Aki.SinglePlayer/Patches/Progression/LighthouseBridgePatch.cs b/project/Aki.SinglePlayer/Patches/Progression/LighthouseBridgePatch.cs index 5b4ae15..157a145 100644 --- a/project/Aki.SinglePlayer/Patches/Progression/LighthouseBridgePatch.cs +++ b/project/Aki.SinglePlayer/Patches/Progression/LighthouseBridgePatch.cs @@ -17,12 +17,18 @@ namespace Aki.SinglePlayer.Patches.Progression private static void PatchPostfix() { var gameWorld = Singleton.Instance; - if (gameWorld == null || gameWorld.MainPlayer.Location.ToLower() != "lighthouse") + + if (gameWorld == null) + { + return; + } + + if (gameWorld.MainPlayer.Location.ToLower() != "lighthouse" || gameWorld.MainPlayer.Side == EPlayerSide.Savage) { return; } gameWorld.GetOrAddComponent(); - } + } } } \ No newline at end of file diff --git a/project/Aki.SinglePlayer/Patches/Progression/OfflineSaveProfilePatch.cs b/project/Aki.SinglePlayer/Patches/Progression/OfflineSaveProfilePatch.cs index 79fb0ec..2bb23d4 100644 --- a/project/Aki.SinglePlayer/Patches/Progression/OfflineSaveProfilePatch.cs +++ b/project/Aki.SinglePlayer/Patches/Progression/OfflineSaveProfilePatch.cs @@ -7,6 +7,7 @@ using Comfort.Common; using EFT; using HarmonyLib; using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; using System; using System.Linq; using System.Reflection; @@ -67,7 +68,7 @@ namespace Aki.SinglePlayer.Patches.Progression IsPlayerScav = ____raidSettings.IsScav }; - RequestHandler.PutJson("/raid/profile/save", request.ToJson(_defaultJsonConverters.AddItem(new NotesJsonConverter()).ToArray())); + RequestHandler.PutJson("/raid/profile/save", request.ToJson(_defaultJsonConverters.AddItem(new NotesJsonConverter()).ToArray())); } } } diff --git a/project/Aki.SinglePlayer/Patches/Progression/OfflineSpawnPointPatch.cs b/project/Aki.SinglePlayer/Patches/Progression/OfflineSpawnPointPatch.cs index ab8eb32..32d75e4 100644 --- a/project/Aki.SinglePlayer/Patches/Progression/OfflineSpawnPointPatch.cs +++ b/project/Aki.SinglePlayer/Patches/Progression/OfflineSpawnPointPatch.cs @@ -45,7 +45,7 @@ namespace Aki.SinglePlayer.Patches.Progression ESpawnCategory category, EPlayerSide side, string groupId, - IAIDetails person, + IPlayer person, string infiltration) { var spawnPointsField = (ISpawnPoints)__instance.GetType().GetFields(PatchConstants.PrivateFlags).SingleOrDefault(f => f.FieldType == typeof(ISpawnPoints))?.GetValue(__instance); @@ -69,8 +69,9 @@ namespace Aki.SinglePlayer.Patches.Progression ? GetFallBackSpawnPoint(unfilteredFilteredSpawnPoints, category, side, infiltration) : mapSpawnPoints.RandomElement(); - Logger.LogInfo($"Desired spawnpoint: [{category}] [{side}] [{infiltration}]"); - Logger.LogInfo($"PatchPrefix SelectSpawnPoint: [{__result.Id}] [{__result.Name}] [{__result.Categories}] [{__result.Sides}] [{__result.Infiltration}]"); + Logger.LogInfo($"Desired spawnpoint: [category:{category}] [side:{side}] [infil:{infiltration}] [{mapSpawnPoints.Count} total spawn points]"); + Logger.LogInfo($"Selected SpawnPoint: [id:{__result.Id}] [name:{__result.Name}] [category:{__result.Categories}] [side:{__result.Sides}] [infil:{__result.Infiltration}]"); + return false; } diff --git a/project/Aki.SinglePlayer/Patches/Progression/ScavExperienceGainPatch.cs b/project/Aki.SinglePlayer/Patches/Progression/ScavExperienceGainPatch.cs index 0dd1076..8c1f2ff 100644 --- a/project/Aki.SinglePlayer/Patches/Progression/ScavExperienceGainPatch.cs +++ b/project/Aki.SinglePlayer/Patches/Progression/ScavExperienceGainPatch.cs @@ -45,8 +45,8 @@ namespace Aki.SinglePlayer.Patches.Progression if (activeProfile.Side == EPlayerSide.Savage) { side = EPlayerSide.Savage; // Also set side to correct value (defaults to usec/bear when playing as scav) - int xpGainedInSession = activeProfile.Stats.SessionCounters.GetAllInt(new object[] { CounterTag.Exp }); - activeProfile.Stats.TotalSessionExperience = (int)(xpGainedInSession * activeProfile.Stats.SessionExperienceMult * activeProfile.Stats.ExperienceBonusMult); + int xpGainedInSession = activeProfile.Stats.Eft.SessionCounters.GetAllInt(new object[] { CounterTag.Exp }); + activeProfile.Stats.Eft.TotalSessionExperience = (int)(xpGainedInSession * activeProfile.Stats.Eft.SessionExperienceMult * activeProfile.Stats.Eft.ExperienceBonusMult); } return true; // Always do original method diff --git a/project/Aki.SinglePlayer/Patches/Quests/SpawnPmcPatch.cs b/project/Aki.SinglePlayer/Patches/Quests/SpawnPmcPatch.cs index b78cbad..f425067 100644 --- a/project/Aki.SinglePlayer/Patches/Quests/SpawnPmcPatch.cs +++ b/project/Aki.SinglePlayer/Patches/Quests/SpawnPmcPatch.cs @@ -22,7 +22,7 @@ namespace Aki.SinglePlayer.Patches.Quests private static bool IsTargetType(Type type) { - if (!typeof(IBotData).IsAssignableFrom(type) || type.GetMethod("method_1", PatchConstants.PrivateFlags) == null) + if (!typeof(IGetProfileData).IsAssignableFrom(type) || type.GetMethod("method_1", PatchConstants.PrivateFlags) == null) { return false; } diff --git a/project/Aki.SinglePlayer/Patches/RaidFix/GetNewBotTemplatesPatch.cs b/project/Aki.SinglePlayer/Patches/RaidFix/GetNewBotTemplatesPatch.cs index 8b0cd34..5b798d9 100644 --- a/project/Aki.SinglePlayer/Patches/RaidFix/GetNewBotTemplatesPatch.cs +++ b/project/Aki.SinglePlayer/Patches/RaidFix/GetNewBotTemplatesPatch.cs @@ -16,7 +16,7 @@ namespace Aki.SinglePlayer.Patches.RaidFix static GetNewBotTemplatesPatch() { - _ = nameof(IBotData.PrepareToLoadBackend); + _ = nameof(IGetProfileData.PrepareToLoadBackend); _ = nameof(BotsPresets.GetNewProfile); _ = nameof(PoolManager.LoadBundlesAndCreatePools); _ = nameof(JobPriority.General); @@ -58,7 +58,7 @@ namespace Aki.SinglePlayer.Patches.RaidFix /// BotsPresets.GetNewProfile() /// [PatchPrefix] - private static bool PatchPrefix(ref Task __result, BotsPresets __instance, List ___list_0, GClass628 data, ref bool withDelete) + private static bool PatchPrefix(ref Task __result, BotsPresets __instance, List ___list_0, GClass513 data, ref bool withDelete) { /* When client wants new bot and GetNewProfile() return null (if not more available templates or they don't satisfy by Role and Difficulty condition) diff --git a/project/Aki.SinglePlayer/Patches/RaidFix/InsuredItemManagerStartPatch.cs b/project/Aki.SinglePlayer/Patches/RaidFix/InsuredItemManagerStartPatch.cs index 2df1b2c..2e1c8c0 100644 --- a/project/Aki.SinglePlayer/Patches/RaidFix/InsuredItemManagerStartPatch.cs +++ b/project/Aki.SinglePlayer/Patches/RaidFix/InsuredItemManagerStartPatch.cs @@ -2,6 +2,7 @@ using EFT; using System.Reflection; using Aki.SinglePlayer.Utils.Insurance; +using Comfort.Common; namespace Aki.SinglePlayer.Patches.RaidFix { @@ -15,7 +16,26 @@ namespace Aki.SinglePlayer.Patches.RaidFix [PatchPostfix] public static void PatchPostFix() { + var gameWorld = Singleton.Instance; + + // Starts tracking of insured items manager InsuredItemManager.Instance.Init(); + + // Sets PlayerScavs items to FoundInRaid + ConfigurePlayerScavFindInRaidStatus(gameWorld.MainPlayer); + } + + private static void ConfigurePlayerScavFindInRaidStatus(Player player) + { + if (player == null || player.Profile.Side != EPlayerSide.Savage) + { + return; + } + + foreach (var item in player.Profile.Inventory.AllRealPlayerItems) + { + item.SpawnedInSession = true; + } } } } diff --git a/project/Aki.SinglePlayer/Patches/RaidFix/PostRaidHealingPricePatch.cs b/project/Aki.SinglePlayer/Patches/RaidFix/PostRaidHealingPricePatch.cs index a9f9364..53fc6dc 100644 --- a/project/Aki.SinglePlayer/Patches/RaidFix/PostRaidHealingPricePatch.cs +++ b/project/Aki.SinglePlayer/Patches/RaidFix/PostRaidHealingPricePatch.cs @@ -2,7 +2,7 @@ using HarmonyLib; using System; using System.Reflection; -using TraderInfo = EFT.Profile.GClass1729; +using TraderInfo = EFT.Profile.GClass1625; namespace Aki.SinglePlayer.Patches.RaidFix { diff --git a/project/Aki.SinglePlayer/Patches/RaidFix/RemoveUsedBotProfilePatch.cs b/project/Aki.SinglePlayer/Patches/RaidFix/RemoveUsedBotProfilePatch.cs index 0b060bd..57396a8 100644 --- a/project/Aki.SinglePlayer/Patches/RaidFix/RemoveUsedBotProfilePatch.cs +++ b/project/Aki.SinglePlayer/Patches/RaidFix/RemoveUsedBotProfilePatch.cs @@ -16,7 +16,7 @@ namespace Aki.SinglePlayer.Patches.RaidFix static RemoveUsedBotProfilePatch() { - _ = nameof(IBotData.ChooseProfile); + _ = nameof(IGetProfileData.ChooseProfile); _flags = BindingFlags.Instance | BindingFlags.NonPublic; _targetInterface = PatchConstants.EftTypes.Single(IsTargetInterface); @@ -42,7 +42,7 @@ namespace Aki.SinglePlayer.Patches.RaidFix /// /// BotsPresets.GetNewProfile() [PatchPrefix] - private static bool PatchPrefix(ref Profile __result, object __instance, GClass628 data, ref bool withDelete) + private static bool PatchPrefix(ref Profile __result, object __instance, GClass513 data, ref bool withDelete) { withDelete = true; diff --git a/project/Aki.SinglePlayer/Patches/RaidFix/SpawnProcessNegativeValuePatch.cs b/project/Aki.SinglePlayer/Patches/RaidFix/SpawnProcessNegativeValuePatch.cs index dec3ec5..33d5b32 100644 --- a/project/Aki.SinglePlayer/Patches/RaidFix/SpawnProcessNegativeValuePatch.cs +++ b/project/Aki.SinglePlayer/Patches/RaidFix/SpawnProcessNegativeValuePatch.cs @@ -1,7 +1,7 @@ -using Aki.Common.Http; using Aki.Reflection.Patching; using Aki.Reflection.Utils; -using System.Linq; +using EFT; +using System; using System.Reflection; namespace Aki.SinglePlayer.Patches.RaidFix @@ -19,7 +19,7 @@ namespace Aki.SinglePlayer.Patches.RaidFix { protected override MethodBase GetTargetMethod() { - var desiredType = typeof(BotSpawnerClass); + var desiredType = typeof(BotSpawner); var desiredMethod = desiredType.GetMethod("CheckOnMax", PatchConstants.PublicFlags); Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); @@ -29,13 +29,20 @@ namespace Aki.SinglePlayer.Patches.RaidFix } [PatchPrefix] - private static void PatchPreFix(ref int ___int_3) + private static bool PatchPreFix(int wantSpawn, ref int toDelay, ref int toSpawn, ref int ____maxBots, int ____allBotsCount, int ____inSpawnProcess) { - // Spawn process - if (___int_3 < 0) + + // Set bots to delay if alive bots + spawning bots count > maxbots + // ____inSpawnProcess can be negative, don't go below 0 when calculating + if ((____allBotsCount + Math.Max(____inSpawnProcess, 0)) > ____maxBots) { - ___int_3 = 0; + toDelay += wantSpawn; + toSpawn = 0; + + return false; // Skip original } + + return true; // Do original } } } \ No newline at end of file diff --git a/project/Aki.SinglePlayer/Patches/RaidFix/TinnitusFixPatch.cs b/project/Aki.SinglePlayer/Patches/RaidFix/TinnitusFixPatch.cs index 96141a5..240dd2b 100644 --- a/project/Aki.SinglePlayer/Patches/RaidFix/TinnitusFixPatch.cs +++ b/project/Aki.SinglePlayer/Patches/RaidFix/TinnitusFixPatch.cs @@ -3,6 +3,7 @@ using Comfort.Common; using System.Reflection; using Aki.Reflection.Patching; using System.Collections; +using EFT.HealthSystem; namespace Aki.SinglePlayer.Patches.RaidFix { @@ -17,9 +18,9 @@ namespace Aki.SinglePlayer.Patches.RaidFix [PatchPrefix] static bool PatchPrefix() { - bool shouldInvoke = typeof(ActiveHealthControllerClass) + bool shouldInvoke = typeof(ActiveHealthController) .GetMethod("FindActiveEffect", BindingFlags.Instance | BindingFlags.Public) - .MakeGenericMethod(typeof(ActiveHealthControllerClass) + .MakeGenericMethod(typeof(ActiveHealthController) .GetNestedType("Stun", BindingFlags.Instance | BindingFlags.NonPublic)) .Invoke(Singleton.Instance.MainPlayer.ActiveHealthController, new object[] { EBodyPart.Common }) != null; diff --git a/project/Aki.SinglePlayer/Patches/ScavMode/LoadOfflineRaidScreenPatch.cs b/project/Aki.SinglePlayer/Patches/ScavMode/LoadOfflineRaidScreenPatch.cs index db6ca08..680ae7a 100644 --- a/project/Aki.SinglePlayer/Patches/ScavMode/LoadOfflineRaidScreenPatch.cs +++ b/project/Aki.SinglePlayer/Patches/ScavMode/LoadOfflineRaidScreenPatch.cs @@ -29,10 +29,13 @@ namespace Aki.SinglePlayer.Patches.ScavMode _ = nameof(TimeAndWeatherSettings.IsRandomWeather); _ = nameof(BotControllerSettings.IsScavWars); _ = nameof(WavesSettings.IsBosses); + _ = GClass2952.MAX_SCAV_COUNT; // UPDATE REFS TO THIS CLASS BELOW !!! var menuControllerType = typeof(MainMenuController); - _onReadyScreenMethod = menuControllerType.GetMethod("method_39", PatchConstants.PrivateFlags); + // `MatchmakerInsuranceScreen` OnShowNextScreen + _onReadyScreenMethod = menuControllerType.GetMethod("method_42", PatchConstants.PrivateFlags); + _isLocalField = menuControllerType.GetField("bool_0", PatchConstants.PrivateFlags); _menuControllerField = typeof(TarkovApplication).GetFields(PatchConstants.PrivateFlags).FirstOrDefault(x => x.FieldType == typeof(MainMenuController)); @@ -44,7 +47,8 @@ namespace Aki.SinglePlayer.Patches.ScavMode protected override MethodBase GetTargetMethod() { - return typeof(MainMenuController).GetMethod("method_63", PatchConstants.PrivateFlags); + // `MatchMakerSelectionLocationScreen` OnShowNextScreen + return typeof(MainMenuController).GetMethod("method_66", PatchConstants.PrivateFlags); } [PatchTranspiler] @@ -93,7 +97,7 @@ namespace Aki.SinglePlayer.Patches.ScavMode 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 }}; + 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 @@ -114,14 +118,14 @@ namespace Aki.SinglePlayer.Patches.ScavMode // Get fields from MainMenuController.cs var raidSettings = Traverse.Create(menuController).Field("raidSettings_0").GetValue(); - var matchmakerPlayersController = Traverse.Create(menuController).Field("gclass3030_0").GetValue(); + var matchmakerPlayersController = Traverse.Create(menuController).Field($"{nameof(GClass2952).ToLowerInvariant()}_0").GetValue(); - var gclass = new MatchmakerOfflineRaidScreen.GClass3019(profile?.Info, ref raidSettings, matchmakerPlayersController); + var gclass = new MatchmakerOfflineRaidScreen.GClass2941(profile?.Info, ref raidSettings, matchmakerPlayersController); gclass.OnShowNextScreen += LoadOfflineRaidNextScreen; - // Ready method - gclass.OnShowReadyScreen += (OfflineRaidAction)Delegate.CreateDelegate(typeof(OfflineRaidAction), menuController, "method_67"); + // `MatchmakerOfflineRaidScreen` OnShowReadyScreen + gclass.OnShowReadyScreen += (OfflineRaidAction)Delegate.CreateDelegate(typeof(OfflineRaidAction), menuController, "method_70"); gclass.ShowScreen(EScreenState.Queued); } diff --git a/project/Aki.SinglePlayer/Utils/Healing/HealthListener.cs b/project/Aki.SinglePlayer/Utils/Healing/HealthListener.cs index 02b0539..18e77ea 100644 --- a/project/Aki.SinglePlayer/Utils/Healing/HealthListener.cs +++ b/project/Aki.SinglePlayer/Utils/Healing/HealthListener.cs @@ -6,6 +6,7 @@ using Aki.Reflection.Utils; using Aki.SinglePlayer.Models.Healing; using System.Linq; using BepInEx.Logging; +using EFT.HealthSystem; namespace Aki.SinglePlayer.Utils.Healing { diff --git a/project/Shared/Hollowed/hollowed.dll b/project/Shared/Hollowed/hollowed.dll index 299f6c2..2e26950 100644 Binary files a/project/Shared/Hollowed/hollowed.dll and b/project/Shared/Hollowed/hollowed.dll differ