0
0
mirror of https://github.com/sp-tarkov/modules.git synced 2025-02-13 03:30:44 -05:00

0.13.5.0 (!33)

Co-authored-by: Dev <dev@dev.sp-tarkov.com>
Co-authored-by: CWX <CWX@noreply.dev.sp-tarkov.com>
Co-authored-by: DrakiaXYZ <drakiaxyz@noreply.dev.sp-tarkov.com>
Co-authored-by: RaiRaiTheRaichu <rairaitheraichu@noreply.dev.sp-tarkov.com>
Co-authored-by: CWX <cwx@noreply.dev.sp-tarkov.com>
Co-authored-by: Kaeno <e>
Reviewed-on: SPT-AKI/Modules#33
This commit is contained in:
chomp 2023-10-10 10:58:33 +00:00
parent 5e881d263f
commit 1e238c426e
51 changed files with 919 additions and 260 deletions

4
.gitignore vendored
View File

@ -38,6 +38,10 @@ bld/
# Visual Studio 2015/2017 cache/options directory # Visual Studio 2015/2017 cache/options directory
.vs/ .vs/
# Rider directory
.idea/
# Uncomment if you have tasks that create the project's static files in wwwroot # Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/ #wwwroot/

View File

@ -23,8 +23,8 @@ git config --local user.email "USERNAME@SOMETHING.com"
## Requirements ## Requirements
- Escape From Tarkov 25206 - Escape From Tarkov 26282
- BepInEx 5.4.19 - BepInEx 5.4.21
- Visual Studio Code - Visual Studio Code
- .NET 6 SDK - .NET 6 SDK

View File

@ -24,6 +24,7 @@ namespace Aki.Core
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError($"A PATCH IN {GetType().Name} FAILED. SUBSEQUENT PATCHES HAVE NOT LOADED");
Logger.LogError($"{GetType().Name}: {ex}"); Logger.LogError($"{GetType().Name}: {ex}");
throw; throw;
} }

View File

@ -15,7 +15,14 @@ namespace Aki.Core.Patches
{ {
try 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<Dictionary<ETransportProtocolType, string>>(); var value = Traverse.Create(type).Field("TransportPrefixes").GetValue<Dictionary<ETransportProtocolType, string>>();
value[ETransportProtocolType.HTTPS] = "http://"; value[ETransportProtocolType.HTTPS] = "http://";
value[ETransportProtocolType.WSS] = "ws://"; value[ETransportProtocolType.WSS] = "ws://";

View File

@ -1,5 +1,4 @@
using Aki.Common.Http; using Aki.Custom.Airdrops.Models;
using Aki.Custom.Airdrops.Models;
using Aki.Custom.Airdrops.Utils; using Aki.Custom.Airdrops.Utils;
using Comfort.Common; using Comfort.Common;
using EFT; using EFT;
@ -12,11 +11,12 @@ namespace Aki.Custom.Airdrops
private AirdropPlane airdropPlane; private AirdropPlane airdropPlane;
private AirdropBox airdropBox; private AirdropBox airdropBox;
private ItemFactoryUtil factory; private ItemFactoryUtil factory;
public bool isFlareDrop; public bool isFlareDrop;
private AirdropParametersModel airdropParameters; private AirdropParametersModel airdropParameters;
public async void Start() public async void Awake()
{
try
{ {
var gameWorld = Singleton<GameWorld>.Instance; var gameWorld = Singleton<GameWorld>.Instance;
@ -32,6 +32,13 @@ namespace Aki.Custom.Airdrops
Destroy(this); Destroy(this);
return; return;
} }
}
catch
{
Debug.LogError("[AKI-AIRDROPS]: Unable to get config from server, airdrop won't occur");
Destroy(this);
throw;
}
try try
{ {
@ -54,6 +61,10 @@ namespace Aki.Custom.Airdrops
} }
public void FixedUpdate() public void FixedUpdate()
{
if (airdropParameters == null || airdropPlane == null || airdropBox == null) return;
try
{ {
airdropParameters.Timer += 0.02f; airdropParameters.Timer += 0.02f;
@ -85,6 +96,15 @@ namespace Aki.Custom.Airdrops
Destroy(this); Destroy(this);
} }
} }
catch
{
Debug.LogError("[AKI-AIRDROPS]: An error occurred during the airdrop FixedUpdate process");
Destroy(airdropBox.gameObject);
Destroy(airdropPlane.gameObject);
Destroy(this);
throw;
}
}
private void StartPlane() private void StartPlane()
{ {

View File

@ -43,12 +43,15 @@ namespace Aki.Custom.Airdrops.Utils
if (item.IsPreset) if (item.IsPreset)
{ {
actualItem = itemFactory.GetPresetItem(item.Tpl); 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(); resources = actualItem.GetAllItems().Select(x => x.Template).SelectMany(x => x.AllResources).ToArray();
} }
else else
{ {
actualItem = itemFactory.CreateItem(item.ID, item.Tpl, null); actualItem = itemFactory.CreateItem(item.ID, item.Tpl, null);
actualItem.StackObjectsCount = item.StackCount; actualItem.StackObjectsCount = item.StackCount;
actualItem.SpawnedInSession = true;
resources = actualItem.Template.AllResources.ToArray(); resources = actualItem.Template.AllResources.ToArray();
} }

View File

@ -31,6 +31,7 @@ namespace Aki.Custom
new SessionIdPatch().Enable(); new SessionIdPatch().Enable();
new VersionLabelPatch().Enable(); new VersionLabelPatch().Enable();
new IsEnemyPatch().Enable(); new IsEnemyPatch().Enable();
new LocationLootCacheBustingPatch().Enable();
//new AddSelfAsEnemyPatch().Enable(); //new AddSelfAsEnemyPatch().Enable();
new CheckAndAddEnemyPatch().Enable(); new CheckAndAddEnemyPatch().Enable();
new BotSelfEnemyPatch().Enable(); // needed new BotSelfEnemyPatch().Enable(); // needed
@ -42,9 +43,13 @@ namespace Aki.Custom
new ExitWhileLootingPatch().Enable(); new ExitWhileLootingPatch().Enable();
new QTEPatch().Enable(); new QTEPatch().Enable();
new PmcFirstAidPatch().Enable(); new PmcFirstAidPatch().Enable();
new SettingsLocationPatch().Enable();
//new RankPanelPatch().Enable();
new RagfairFeePatch().Enable();
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError($"A PATCH IN {GetType().Name} FAILED. SUBSEQUENT PATCHES HAVE NOT LOADED");
Logger.LogError($"{GetType().Name}: {ex}"); Logger.LogError($"{GetType().Name}: {ex}");
throw; throw;
} }

View File

@ -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<WildSpawnType> playerScavTypes = new List<WildSpawnType>() { 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<AIBrains>(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<WildSpawnType, Dictionary<string, Dictionary<string, int>>> pmc { get; set; }
public Dictionary<string, Dictionary<string, int>> assault { get; set; }
}
/// <summary>
/// Choose a value from a choice of values with weightings
/// </summary>
/// <param name="botTypes"></param>
/// <param name="weights"></param>
/// <returns></returns>
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";
}
}
}

View File

@ -0,0 +1,47 @@
using Aki.PrePatch;
using EFT;
namespace Aki.Custom.CustomAI
{
public static class AiHelpers
{
/// <summary>
/// Bot is a PMC when it has IsStreamerModeAvailable flagged and has a wildspawn type of 'sptBear' or 'sptUsec'
/// </summary>
/// <param name="botRoleToCheck">Bots role</param>
/// <param name="___botOwner_0">Bot details</param>
/// <returns></returns>
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;
}
}
}

View File

@ -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<string> nonFiRItems = new List<string>() { 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<string> weaponTypeIds = new List<string>() { 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<Slot> 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<string> { 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;
}
}
}
}

View File

@ -16,11 +16,11 @@ namespace Aki.Custom.Patches
protected override MethodBase GetTargetMethod() protected override MethodBase GetTargetMethod()
{ {
return typeof(BotGroupClass).GetMethod(methodName); return typeof(BotZoneGroupsDictionary).GetMethod(methodName);
} }
[PatchPrefix] [PatchPrefix]
private static bool PatchPrefix(BotGroupClass __instance, IAIDetails person) private static bool PatchPrefix(BotZoneGroupsDictionary __instance, IPlayer person)
{ {
var botOwners = (List<BotOwner>)__instance.GetType().GetField("list_1", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance); var botOwners = (List<BotOwner>)__instance.GetType().GetField("list_1", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance);
if (botOwners.Any(x => x.Id == person.Id)) if (botOwners.Any(x => x.Id == person.Id))

View File

@ -19,7 +19,7 @@ namespace Aki.Custom.Patches
private bool IsTargetType(Type type) 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; return true;
} }
@ -42,7 +42,7 @@ namespace Aki.Custom.Patches
/// This should fix that. /// This should fix that.
/// </summary> /// </summary>
[PatchPrefix] [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; BotZone botZone = groupOwner.AIData.BotOwner.BotsGroup.BotZone;
foreach (var item in __instance.Groups()) foreach (var item in __instance.Groups())
@ -63,7 +63,7 @@ namespace Aki.Custom.Patches
&& group.ShallRevengeFor(target) && group.ShallRevengeFor(target)
) )
{ {
group.AddEnemy(aggressor); group.AddEnemy(aggressor, EBotEnemyCause.AddEnemyToAllGroupsInBotZone);
} }
} }
} }

View File

@ -1,7 +1,8 @@
using Aki.Common.Http;
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using Aki.Reflection.Utils; using Aki.Reflection.Utils;
using Aki.Common.Http;
using EFT; using EFT;
using EFT.UI;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
@ -22,7 +23,13 @@ namespace Aki.Custom.Patches
private static bool PatchPrefix(ref string __result, BotDifficulty botDifficulty, WildSpawnType role) private static bool PatchPrefix(ref string __result, BotDifficulty botDifficulty, WildSpawnType role)
{ {
__result = RequestHandler.GetJson($"/singleplayer/settings/bot/difficulty/{role}/{botDifficulty}"); __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
} }
} }
} }

View File

@ -19,7 +19,7 @@ namespace Aki.Custom.Patches
private bool IsTargetType(Type type) 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}"); Logger.LogInfo($"{methodName}: {type.FullName}");
return true; return true;
@ -40,7 +40,7 @@ namespace Aki.Custom.Patches
/// This should fix that. /// This should fix that.
/// </summary> /// </summary>
[PatchPrefix] [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; BotZone botZone = groupOwner.AIData.BotOwner.BotsGroup.BotZone;
foreach (var item in __instance.Groups()) foreach (var item in __instance.Groups())
@ -54,14 +54,14 @@ namespace Aki.Custom.Patches
{ {
if (!group.Enemies.ContainsKey(aggressor) && ShouldAttack(aggressor, target, group)) if (!group.Enemies.ContainsKey(aggressor) && ShouldAttack(aggressor, target, group))
{ {
group.AddEnemy(aggressor); group.AddEnemy(aggressor, EBotEnemyCause.AddEnemyToAllGroupsInBotZone);
} }
} }
} }
return false; 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. // 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 bool shouldAttack = attacker.Side != groupToCheck.Side

View File

@ -17,9 +17,9 @@ namespace Aki.Custom.Patches
} }
[PatchPrefix] [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) foreach (var enemy in group.Enemies)
{ {

View File

@ -41,7 +41,7 @@ namespace Aki.Custom.Patches
/// removes the !player.AIData.IsAI check /// removes the !player.AIData.IsAI check
/// </summary> /// </summary>
[PatchPrefix] [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 // 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) if (__instance.InitialBotType == WildSpawnType.bossZryachiy || __instance.InitialBotType == WildSpawnType.followerZryachiy)
@ -56,7 +56,7 @@ namespace Aki.Custom.Patches
if (!__instance.Enemies.ContainsKey(player)) if (!__instance.Enemies.ContainsKey(player))
{ {
__instance.AddEnemy(player); __instance.AddEnemy(player, EBotEnemyCause.checkAddTODO);
} }
return false; // Skip original return false; // Skip original

View File

@ -1,79 +1,91 @@
using Aki.Common.Http; using Aki.Reflection.Patching;
using Aki.Reflection.Patching;
using EFT; using EFT;
using Newtonsoft.Json;
using System; using System;
using Comfort.Common; using Comfort.Common;
using System.Collections.Generic;
using System.Linq;
using System.Reflection; using System.Reflection;
using Aki.PrePatch; using Aki.PrePatch;
using Random = System.Random; using Aki.Custom.CustomAI;
namespace Aki.Custom.Patches namespace Aki.Custom.Patches
{ {
public class CustomAiPatch : ModulePatch public class CustomAiPatch : ModulePatch
{ {
private static readonly Random random = new Random(); private static readonly PmcFoundInRaidEquipment pmcFoundInRaidEquipment = new PmcFoundInRaidEquipment(Logger);
private static Dictionary<WildSpawnType, Dictionary<string, Dictionary<string, int>>> botTypeCache = new Dictionary<WildSpawnType, Dictionary<string, Dictionary<string, int>>>(); private static readonly AIBrainSpawnWeightAdjustment aIBrainSpawnWeightAdjustment = new AIBrainSpawnWeightAdjustment(Logger);
private static DateTime cacheDate = new DateTime();
protected override MethodBase GetTargetMethod() protected override MethodBase GetTargetMethod()
{ {
return typeof(BotBrainClass).GetMethod("Activate", BindingFlags.Public | BindingFlags.Instance); return typeof(StandartBotBrain).GetMethod("Activate", BindingFlags.Public | BindingFlags.Instance);
} }
/// <summary> /// <summary>
/// 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 /// 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
/// </summary> /// </summary>
/// <param name="__state">state to save for postfix to use later</param> /// <param name="__state">state to save for postfix to use later</param>
/// <param name="__instance"></param> /// <param name="__instance"></param>
/// <param name="___botOwner_0">botOwner_0 property</param> /// <param name="___botOwner_0">botOwner_0 property</param>
[PatchPrefix] [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 ___botOwner_0.Profile.Info.Settings.Role = FixAssaultGroupPmcsRole(___botOwner_0);
__state = ___botOwner_0.Profile.Info.Settings.Role; __state = ___botOwner_0.Profile.Info.Settings.Role; // Store original type in state param to allow access in PatchPostFix()
//Console.WriteLine($"Processing bot {___botOwner_0.Profile.Info.Nickname} with role {___botOwner_0.Profile.Info.Settings.Role}");
try try
{ {
if (BotIsSptPmc(___botOwner_0.Profile.Info.Settings.Role)) if (AiHelpers.BotIsPlayerScav(__state, ___botOwner_0))
{ {
___botOwner_0.Profile.Info.Settings.Role = aIBrainSpawnWeightAdjustment.GetRandomisedPlayerScavType();
return true; // Do original
}
string currentMapName = GetCurrentMap(); string currentMapName = GetCurrentMap();
if (AiHelpers.BotIsNormalAssaultScav(__state, ___botOwner_0))
if (!botTypeCache.TryGetValue(___botOwner_0.Profile.Info.Settings.Role, out var botSettings) || CacheIsStale())
{ {
ResetCacheDate(); ___botOwner_0.Profile.Info.Settings.Role = aIBrainSpawnWeightAdjustment.GetAssaultScavWildSpawnType(___botOwner_0, currentMapName);
HydrateCacheWithServerData();
if (!botTypeCache.TryGetValue(___botOwner_0.Profile.Info.Settings.Role, out botSettings)) return true; // Do original
{
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()]; if (AiHelpers.BotIsSptPmc(__state, ___botOwner_0))
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"); // Bot has inventory equipment
___botOwner_0.Profile.Info.Settings.Role = newAiType; if (___botOwner_0.Profile?.Inventory?.Equipment != null)
}
else
{ {
Logger.LogError($"Couldnt not update spt bot {___botOwner_0.Profile.Info.Nickname} to random type {randomType}, does not exist for WildSpawnType enum"); pmcFoundInRaidEquipment.ConfigurePMCFindInRaidStatus(___botOwner_0);
} }
___botOwner_0.Profile.Info.Settings.Role = aIBrainSpawnWeightAdjustment.GetPmcWildSpawnType(___botOwner_0, ___botOwner_0.Profile.Info.Settings.Role, currentMapName);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError($"Error processing log: {ex.Message}"); Logger.LogError($"Error running CustomAiPatch PatchPrefix(): {ex.Message}");
Logger.LogError(ex.StackTrace); Logger.LogError(ex.StackTrace);
} }
return true; // Do original return true; // Do original
} }
/// <summary>
/// the client sometimes replaces PMC roles with 'assaultGroup', give PMCs their original role back (sptBear/sptUsec)
/// </summary>
/// <returns>WildSpawnType</returns>
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;
}
/// <summary> /// <summary>
/// Revert prefix change, get bots type back to what it was before changes /// Revert prefix change, get bots type back to what it was before changes
/// </summary> /// </summary>
@ -82,16 +94,16 @@ namespace Aki.Custom.Patches
[PatchPostfix] [PatchPostfix]
private static void PatchPostFix(WildSpawnType __state, BotOwner ___botOwner_0) 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 // Set spt bot bot back to original type
___botOwner_0.Profile.Info.Settings.Role = __state; ___botOwner_0.Profile.Info.Settings.Role = __state;
} }
} else if (AiHelpers.BotIsPlayerScav(__state, ___botOwner_0))
private static bool BotIsSptPmc(WildSpawnType role)
{ {
return (int)role == AkiBotsPrePatcher.sptBearValue || (int)role == AkiBotsPrePatcher.sptUsecValue; // Set pscav back to original type
___botOwner_0.Profile.Info.Settings.Role = __state;
}
} }
private static string GetCurrentMap() private static string GetCurrentMap()
@ -100,50 +112,5 @@ namespace Aki.Custom.Patches
return gameWorld.MainPlayer.Location; 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<Dictionary<WildSpawnType, Dictionary<string, Dictionary<string, int>>>>(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";
}
} }
} }

View File

@ -30,6 +30,8 @@ namespace Aki.Custom.Patches
_manifestField = type.GetField(nameof(EasyAssets.Manifest)); _manifestField = type.GetField(nameof(EasyAssets.Manifest));
_bundlesField = type.GetField($"{EasyBundleHelper.Type.Name.ToLowerInvariant()}_0", PatchConstants.PrivateFlags); _bundlesField = type.GetField($"{EasyBundleHelper.Type.Name.ToLowerInvariant()}_0", PatchConstants.PrivateFlags);
// DependencyGraph<IEasyBundle>
_systemProperty = type.GetProperty("System"); _systemProperty = type.GetProperty("System");
} }
@ -82,7 +84,15 @@ namespace Aki.Custom.Patches
for (var i = 0; i < bundleNames.Length; i++) 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); await JobScheduler.Yield(EJobPriority.Immediate);
} }

View File

@ -33,7 +33,7 @@ namespace Aki.Custom.Patches
var player = Singleton<GameWorld>.Instance.MainPlayer; var player = Singleton<GameWorld>.Instance.MainPlayer;
if (profileId == player?.Profile.Id) if (profileId == player?.Profile.Id)
{ {
GClass2977.Instance.CloseAllScreensForced(); GClass2897.Instance.CloseAllScreensForced();
} }
return true; return true;

View File

@ -39,22 +39,22 @@ namespace Aki.Custom.Patches
/// Needed to ensure bot checks the enemy side, not just its botType /// Needed to ensure bot checks the enemy side, not just its botType
/// </summary> /// </summary>
[PatchPrefix] [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 var isEnemy = false; // default not an enemy
if (requester == null) if (requester == null)
{ {
__result = isEnemy; __result = isEnemy;
return true; // Skip original return false; // Skip original
} }
// Check existing enemies list // Check existing enemies list
// Could also check x.Value.Player?.Id - BSG do it this way // 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; __result = true;
return true; return false; // Skip original
} }
else else
{ {
@ -62,7 +62,7 @@ namespace Aki.Custom.Patches
// Make zryachiy use existing isEnemy() code // Make zryachiy use existing isEnemy() code
if (__instance.InitialBotType == WildSpawnType.bossZryachiy) if (__instance.InitialBotType == WildSpawnType.bossZryachiy)
{ {
return false; // do original method return false; // Skip original
} }
if (__instance.Side == EPlayerSide.Usec) if (__instance.Side == EPlayerSide.Usec)
@ -71,7 +71,7 @@ namespace Aki.Custom.Patches
ShouldAttackUsec(requester)) ShouldAttackUsec(requester))
{ {
isEnemy = true; isEnemy = true;
__instance.AddEnemy(requester); __instance.AddEnemy(requester, EBotEnemyCause.checkAddTODO);
} }
} }
else if (__instance.Side == EPlayerSide.Bear) else if (__instance.Side == EPlayerSide.Bear)
@ -80,23 +80,28 @@ namespace Aki.Custom.Patches
ShouldAttackBear(requester)) ShouldAttackBear(requester))
{ {
isEnemy = true; isEnemy = true;
__instance.AddEnemy(requester); __instance.AddEnemy(requester, EBotEnemyCause.checkAddTODO);
} }
} }
else if (__instance.Side == EPlayerSide.Savage) else if (__instance.Side == EPlayerSide.Savage)
{ {
if (requester.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) // everyone else is an enemy to savage (scavs)
isEnemy = true; isEnemy = true;
__instance.AddEnemy(requester); __instance.AddEnemy(requester, EBotEnemyCause.checkAddTODO);
} }
} }
} }
__result = isEnemy; __result = isEnemy;
return true; // Skip original return false; // Skip original
} }
/// <summary> /// <summary>
@ -104,7 +109,7 @@ namespace Aki.Custom.Patches
/// </summary> /// </summary>
/// <param name="requester"></param> /// <param name="requester"></param>
/// <returns>bool</returns> /// <returns>bool</returns>
private static bool ShouldAttackUsec(IAIDetails requester) private static bool ShouldAttackUsec(IPlayer requester)
{ {
var requesterMind = requester?.AIData?.BotOwner?.Settings?.FileSettings?.Mind; var requesterMind = requester?.AIData?.BotOwner?.Settings?.FileSettings?.Mind;
@ -121,7 +126,7 @@ namespace Aki.Custom.Patches
/// </summary> /// </summary>
/// <param name="requester"></param> /// <param name="requester"></param>
/// <returns></returns> /// <returns></returns>
private static bool ShouldAttackBear(IAIDetails requester) private static bool ShouldAttackBear(IPlayer requester)
{ {
var requesterMind = requester.AIData?.BotOwner?.Settings?.FileSettings?.Mind; var requesterMind = requester.AIData?.BotOwner?.Settings?.FileSettings?.Mind;

View File

@ -0,0 +1,40 @@
using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using System;
using System.Linq;
using System.Reflection;
namespace Aki.Custom.Patches
{
/// <summary>
/// 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
/// </summary>
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
}
}
}

View File

@ -1,5 +1,8 @@
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using Aki.Reflection.Utils;
using EFT; using EFT;
using System;
using System.Linq;
using System.Reflection; using System.Reflection;
namespace Aki.Custom.Patches namespace Aki.Custom.Patches
@ -10,11 +13,30 @@ namespace Aki.Custom.Patches
/// </summary> /// </summary>
public class PmcFirstAidPatch : ModulePatch public class PmcFirstAidPatch : ModulePatch
{ {
private static Type _targetType;
private static readonly string methodName = "FirstAidApplied"; private static readonly string methodName = "FirstAidApplied";
public PmcFirstAidPatch()
{
_targetType = PatchConstants.EftTypes.FirstOrDefault(IsTargetType);
}
protected override MethodBase GetTargetMethod() protected override MethodBase GetTargetMethod()
{ {
return typeof(GClass399).GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic); return _targetType.GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic);
}
/// <summary>
/// GCLass350 for client version 25782
/// </summary>
private bool IsTargetType(Type type)
{
if (type.GetMethod("GetHpPercent") != null && type.GetMethod("TryApplyToCurrentPart") != null)
{
return true;
}
return false;
} }
[PatchPrefix] [PatchPrefix]

View File

@ -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
{
/// <summary>
/// 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
/// </summary>
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);
}
/// <summary>
/// Calculate tax to charge player and send to server before the offer is sent
/// </summary>
/// <param name="___item_0">Item sold</param>
/// <param name="___gclass2859_0">OfferItemCount</param>
/// <param name="___double_0">RequirementsPrice</param>
/// <param name="___bool_0">SellInOnePiece</param>
[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());
}
}
}

View File

@ -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<GameWorld>.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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -7,6 +7,7 @@ using EFT.UI;
using HarmonyLib; using HarmonyLib;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Comfort.Common;
namespace Aki.Custom.Patches namespace Aki.Custom.Patches
{ {
@ -40,8 +41,8 @@ namespace Aki.Custom.Patches
Logger.LogInfo($"Server version: {_versionLabel}"); Logger.LogInfo($"Server version: {_versionLabel}");
} }
Traverse.Create(MonoBehaviourSingleton<PreloaderUI>.Instance).Field("_alphaVersionLabel").Property("LocalizationKey").SetValue("{0}"); Traverse.Create(Singleton<PreloaderUI>.Instance).Field("_alphaVersionLabel").Property("LocalizationKey").SetValue("{0}");
Traverse.Create(MonoBehaviourSingleton<PreloaderUI>.Instance).Field("string_2").SetValue(_versionLabel); Traverse.Create(Singleton<PreloaderUI>.Instance).Field("string_2").SetValue(_versionLabel);
Traverse.Create(__result).Field("Major").SetValue(_versionLabel); Traverse.Create(__result).Field("Major").SetValue(_versionLabel);
} }
} }

View File

@ -4,13 +4,14 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Aki.Reflection.Utils; using Aki.Reflection.Utils;
using UnityEngine;
using BindableState = BindableState<Diz.DependencyManager.ELoadState>; using BindableState = BindableState<Diz.DependencyManager.ELoadState>;
namespace Aki.Custom.Utils namespace Aki.Custom.Utils
{ {
public class EasyBundleHelper 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 _pathField;
private static readonly FieldInfo _keyWithoutExtensionField; private static readonly FieldInfo _keyWithoutExtensionField;
private static readonly FieldInfo _bundleLockField; private static readonly FieldInfo _bundleLockField;
@ -26,14 +27,24 @@ namespace Aki.Custom.Utils
_ = nameof(IBundleLock.IsLocked); _ = nameof(IBundleLock.IsLocked);
_ = nameof(BindableState.Bind); _ = nameof(BindableState.Bind);
Type = PatchConstants.EftTypes.Single(x => x.GetMethod("set_SameNameAsset", _flags) != null); // Class can be found as a private array inside EasyAssets.cs, next to DependencyGraph<IEasyBundle>
_pathField = Type.GetField("string_1", _flags); Type = PatchConstants.EftTypes.Single(x => x.GetMethod("set_SameNameAsset", _NonPublicInstanceflags) != null);
_keyWithoutExtensionField = Type.GetField("string_0", _flags);
_bundleLockField = Type.GetFields(_flags).FirstOrDefault(x => x.FieldType == typeof(IBundleLock)); _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"); _dependencyKeysProperty = Type.GetProperty("DependencyKeys");
_keyProperty = Type.GetProperty("Key"); _keyProperty = Type.GetProperty("Key");
_loadStateProperty = Type.GetProperty("LoadState"); _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) public EasyBundleHelper(object easyBundle)
@ -113,9 +124,9 @@ namespace Aki.Custom.Utils
} }
} }
public Task LoadingCoroutine() public Task LoadingCoroutine(Dictionary<string, AssetBundle> bundles)
{ {
return (Task)_loadingCoroutineMethod.Invoke(_instance, new object[] { }); return (Task)_loadingCoroutineMethod.Invoke(_instance, new object[] { bundles });
} }
} }
} }

View File

@ -12,6 +12,7 @@
<ItemGroup> <ItemGroup>
<Reference Include="Assembly-CSharp" HintPath="..\Shared\Hollowed\hollowed.dll" Private="False" /> <Reference Include="Assembly-CSharp" HintPath="..\Shared\Hollowed\hollowed.dll" Private="False" />
<Reference Include="Sirenix.Serialization" HintPath="..\Shared\Managed\Sirenix.Serialization.dll" Private="False" />
<Reference Include="Unity.TextMeshPro" HintPath="..\Shared\Managed\Unity.TextMeshPro.dll" Private="False" /> <Reference Include="Unity.TextMeshPro" HintPath="..\Shared\Managed\Unity.TextMeshPro.dll" Private="False" />
<Reference Include="UnityEngine" HintPath="..\Shared\Managed\UnityEngine.dll" Private="False" /> <Reference Include="UnityEngine" HintPath="..\Shared\Managed\UnityEngine.dll" Private="False" />
<Reference Include="UnityEngine.CoreModule" HintPath="..\Shared\Managed\UnityEngine.CoreModule.dll" Private="False" /> <Reference Include="UnityEngine.CoreModule" HintPath="..\Shared\Managed\UnityEngine.CoreModule.dll" Private="False" />

View File

@ -15,6 +15,7 @@ namespace Aki.Debugging
try try
{ {
// new CoordinatesPatch().Enable(); // new CoordinatesPatch().Enable();
new EndRaidDebug().Enable();
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -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;
}
}
}

View File

@ -7,8 +7,8 @@ namespace Aki.PrePatch
{ {
public static IEnumerable<string> TargetDLLs { get; } = new[] { "Assembly-CSharp.dll" }; public static IEnumerable<string> TargetDLLs { get; } = new[] { "Assembly-CSharp.dll" };
public static int sptUsecValue = 34; public static int sptUsecValue = 38;
public static int sptBearValue = 35; public static int sptBearValue = 39;
public static void Patch(ref AssemblyDefinition assembly) public static void Patch(ref AssemblyDefinition assembly)
{ {

View File

@ -53,9 +53,11 @@ namespace Aki.SinglePlayer
new PluginErrorNotifierPatch().Enable(); new PluginErrorNotifierPatch().Enable();
new SpawnProcessNegativeValuePatch().Enable(); new SpawnProcessNegativeValuePatch().Enable();
new InsuredItemManagerStartPatch().Enable(); new InsuredItemManagerStartPatch().Enable();
new MapReadyButtonPatch().Enable();
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError($"A PATCH IN {GetType().Name} FAILED. SUBSEQUENT PATCHES HAVE NOT LOADED");
Logger.LogError($"{GetType().Name}: {ex}"); Logger.LogError($"{GetType().Name}: {ex}");
throw; throw;
} }

View File

@ -8,14 +8,13 @@ namespace Aki.SinglePlayer.Models.Progression
{ {
public class LighthouseProgressionClass : MonoBehaviour public class LighthouseProgressionClass : MonoBehaviour
{ {
private bool _isScav;
private GameWorld _gameWorld; private GameWorld _gameWorld;
private Player _player; private Player _player;
private float _timer; private float _timer;
private bool _playerFlaggedAsEnemyToBosses; private bool _playerFlaggedAsEnemyToBosses;
private List<MineDirectionalColliders> _bridgeMines; private List<MineDirectionalColliders> _bridgeMines;
private RecodableItemClass _transmitter; private RecodableItemClass _transmitter;
private readonly List<IAIDetails> _zryachiyAndFollowers = new List<IAIDetails>(); private readonly List<IPlayer> _zryachiyAndFollowers = new List<IPlayer>();
private bool _aggressor; private bool _aggressor;
private bool _isDoorDisabled; private bool _isDoorDisabled;
private readonly string _transmitterId = "62e910aaf957f2915e0a5e36"; private readonly string _transmitterId = "62e910aaf957f2915e0a5e36";
@ -26,26 +25,24 @@ namespace Aki.SinglePlayer.Models.Progression
_gameWorld = Singleton<GameWorld>.Instance; _gameWorld = Singleton<GameWorld>.Instance;
_player = _gameWorld?.MainPlayer; _player = _gameWorld?.MainPlayer;
// Exit if not on lighthouse if (_gameWorld == null || _player == null)
if (_gameWorld == null || !string.Equals(_player.Location, "lighthouse", System.StringComparison.OrdinalIgnoreCase))
{ {
return; Destroy(this);
}
// Expensive, run after gameworld / lighthouse checks above
_bridgeMines = FindObjectsOfType<MineDirectionalColliders>().ToList();
// Player is a scav, exit
if (_player.Side == EPlayerSide.Savage)
{
_isScav = true;
return; return;
} }
// Get transmitter from players inventory
_transmitter = GetTransmitterFromInventory(); _transmitter = GetTransmitterFromInventory();
if (PlayerHasTransmitterInInventory())
// Exit if transmitter does not exist and isnt green
if (!PlayerHasActiveTransmitterInInventory())
{ {
Destroy(this);
return;
}
GameObject.Find("Attack").SetActive(false); GameObject.Find("Attack").SetActive(false);
// Zone was added in a newer version and the gameObject actually has a \ // Zone was added in a newer version and the gameObject actually has a \
@ -53,7 +50,12 @@ namespace Aki.SinglePlayer.Models.Progression
// Give access to Lightkeepers door // Give access to Lightkeepers door
_gameWorld.BufferZoneController.SetPlayerAccessStatus(_player.ProfileId, true); _gameWorld.BufferZoneController.SetPlayerAccessStatus(_player.ProfileId, true);
}
// Expensive, run after gameworld / lighthouse checks above
_bridgeMines = FindObjectsOfType<MineDirectionalColliders>().ToList();
// Set mines to be non-active
SetBridgeMinesStatus(false);
} }
public void Update() public void Update()
@ -82,37 +84,28 @@ namespace Aki.SinglePlayer.Models.Progression
SetupZryachiyAndFollowerHostility(); SetupZryachiyAndFollowerHostility();
} }
if (_isScav) // If player becomes aggressor, block access to LK
{
MakeZryachiyAndFollowersHostileToPlayer();
return;
}
// (active/green)
if (PlayerHasActiveTransmitterInHands())
{
SetBridgeMinesStatus(false);
}
else
{
SetBridgeMinesStatus(true);
}
if (_aggressor) if (_aggressor)
{ {
DisableAccessToLightKeeper(); DisableAccessToLightKeeper();
} }
} }
/// <summary>
/// Gets transmitter from players inventory
/// </summary>
private RecodableItemClass GetTransmitterFromInventory() 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() /// <summary>
/// Checks for transmitter status and exists in players inventory
/// </summary>
private bool PlayerHasActiveTransmitterInInventory()
{ {
return _transmitter != null; return _transmitter != null &&
_transmitter?.RecodableComponent?.Status == RadioTransmitterStatus.Green;
} }
/// <summary> /// <summary>
@ -123,12 +116,6 @@ namespace Aki.SinglePlayer.Models.Progression
_timer += Time.deltaTime; _timer += Time.deltaTime;
} }
private bool PlayerHasActiveTransmitterInHands()
{
return _gameWorld?.MainPlayer?.HandsController?.Item?.TemplateId == _transmitterId
&& _transmitter?.RecodableComponent?.Status == RadioTransmitterStatus.Green;
}
/// <summary> /// <summary>
/// Set all brdige mines to desire state /// Set all brdige mines to desire state
/// </summary> /// </summary>
@ -142,11 +129,21 @@ namespace Aki.SinglePlayer.Models.Progression
} }
} }
/// <summary>
/// Put Zryachiy and followers into a list and sub to their death event
/// Make player agressor if player kills them.
/// </summary>
private void SetupZryachiyAndFollowerHostility() private void SetupZryachiyAndFollowerHostility()
{ {
// only process non-players (ai) // only process non-players (ai)
foreach (var aiBot in _gameWorld.AllAlivePlayersList.Where(x => !x.IsYourPlayer)) 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 // Edge case of bossZryachiy not being hostile to player
if (aiBot.AIData.BotOwner.IsRole(WildSpawnType.bossZryachiy) || aiBot.AIData.BotOwner.IsRole(WildSpawnType.followerZryachiy)) if (aiBot.AIData.BotOwner.IsRole(WildSpawnType.bossZryachiy) || aiBot.AIData.BotOwner.IsRole(WildSpawnType.followerZryachiy))
{ {
@ -171,21 +168,6 @@ namespace Aki.SinglePlayer.Models.Progression
} }
} }
/// <summary>
/// Iterate over bots gathered from SetupZryachiyHostility()
/// </summary>
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;
}
/// <summary> /// <summary>
/// Disable door + set transmitter to 'red' /// Disable door + set transmitter to 'red'
/// </summary> /// </summary>

View File

@ -1,5 +1,6 @@
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using Aki.Reflection.Utils; using Aki.Reflection.Utils;
using EFT.HealthSystem;
using System.Reflection; using System.Reflection;
namespace Aki.SinglePlayer.Patches.Healing namespace Aki.SinglePlayer.Patches.Healing

View File

@ -16,8 +16,23 @@ namespace Aki.SinglePlayer.Patches.MainMenu
protected override MethodBase GetTargetMethod() 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 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} Type: {desiredType?.Name}");
Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}"); Logger.LogDebug($"{this.GetType().Name} Method: {desiredMethod?.Name}");

View File

@ -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
{
/// <summary>
/// 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
/// </summary>
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);
}
}
}

View File

@ -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 // 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 // in-game console shows newer messages at the top
ConsoleScreen consoleScreen = MonoBehaviourSingleton<PreloaderUI>.Instance.Console;
foreach (string line in errorMessage.Split('\n').Reverse()) foreach (string line in errorMessage.Split('\n').Reverse())
{ {
if (line.Length > 0) if (line.Length > 0)
{ {
// Note: We directly call the internal Log method to work around a bug in 'LogError' that passes an empty string // 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 // 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);
} }
} }

View File

@ -17,7 +17,13 @@ namespace Aki.SinglePlayer.Patches.Progression
private static void PatchPostfix() private static void PatchPostfix()
{ {
var gameWorld = Singleton<GameWorld>.Instance; var gameWorld = Singleton<GameWorld>.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; return;
} }

View File

@ -7,6 +7,7 @@ using Comfort.Common;
using EFT; using EFT;
using HarmonyLib; using HarmonyLib;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System; using System;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;

View File

@ -45,7 +45,7 @@ namespace Aki.SinglePlayer.Patches.Progression
ESpawnCategory category, ESpawnCategory category,
EPlayerSide side, EPlayerSide side,
string groupId, string groupId,
IAIDetails person, IPlayer person,
string infiltration) string infiltration)
{ {
var spawnPointsField = (ISpawnPoints)__instance.GetType().GetFields(PatchConstants.PrivateFlags).SingleOrDefault(f => f.FieldType == typeof(ISpawnPoints))?.GetValue(__instance); 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) ? GetFallBackSpawnPoint(unfilteredFilteredSpawnPoints, category, side, infiltration)
: mapSpawnPoints.RandomElement(); : mapSpawnPoints.RandomElement();
Logger.LogInfo($"Desired spawnpoint: [{category}] [{side}] [{infiltration}]"); Logger.LogInfo($"Desired spawnpoint: [category:{category}] [side:{side}] [infil:{infiltration}] [{mapSpawnPoints.Count} total spawn points]");
Logger.LogInfo($"PatchPrefix SelectSpawnPoint: [{__result.Id}] [{__result.Name}] [{__result.Categories}] [{__result.Sides}] [{__result.Infiltration}]"); Logger.LogInfo($"Selected SpawnPoint: [id:{__result.Id}] [name:{__result.Name}] [category:{__result.Categories}] [side:{__result.Sides}] [infil:{__result.Infiltration}]");
return false; return false;
} }

View File

@ -45,8 +45,8 @@ namespace Aki.SinglePlayer.Patches.Progression
if (activeProfile.Side == EPlayerSide.Savage) if (activeProfile.Side == EPlayerSide.Savage)
{ {
side = EPlayerSide.Savage; // Also set side to correct value (defaults to usec/bear when playing as scav) 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 }); int xpGainedInSession = activeProfile.Stats.Eft.SessionCounters.GetAllInt(new object[] { CounterTag.Exp });
activeProfile.Stats.TotalSessionExperience = (int)(xpGainedInSession * activeProfile.Stats.SessionExperienceMult * activeProfile.Stats.ExperienceBonusMult); activeProfile.Stats.Eft.TotalSessionExperience = (int)(xpGainedInSession * activeProfile.Stats.Eft.SessionExperienceMult * activeProfile.Stats.Eft.ExperienceBonusMult);
} }
return true; // Always do original method return true; // Always do original method

View File

@ -22,7 +22,7 @@ namespace Aki.SinglePlayer.Patches.Quests
private static bool IsTargetType(Type type) 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; return false;
} }

View File

@ -16,7 +16,7 @@ namespace Aki.SinglePlayer.Patches.RaidFix
static GetNewBotTemplatesPatch() static GetNewBotTemplatesPatch()
{ {
_ = nameof(IBotData.PrepareToLoadBackend); _ = nameof(IGetProfileData.PrepareToLoadBackend);
_ = nameof(BotsPresets.GetNewProfile); _ = nameof(BotsPresets.GetNewProfile);
_ = nameof(PoolManager.LoadBundlesAndCreatePools); _ = nameof(PoolManager.LoadBundlesAndCreatePools);
_ = nameof(JobPriority.General); _ = nameof(JobPriority.General);
@ -58,7 +58,7 @@ namespace Aki.SinglePlayer.Patches.RaidFix
/// BotsPresets.GetNewProfile() /// BotsPresets.GetNewProfile()
/// </summary> /// </summary>
[PatchPrefix] [PatchPrefix]
private static bool PatchPrefix(ref Task<Profile> __result, BotsPresets __instance, List<Profile> ___list_0, GClass628 data, ref bool withDelete) private static bool PatchPrefix(ref Task<Profile> __result, BotsPresets __instance, List<Profile> ___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) When client wants new bot and GetNewProfile() return null (if not more available templates or they don't satisfy by Role and Difficulty condition)

View File

@ -2,6 +2,7 @@
using EFT; using EFT;
using System.Reflection; using System.Reflection;
using Aki.SinglePlayer.Utils.Insurance; using Aki.SinglePlayer.Utils.Insurance;
using Comfort.Common;
namespace Aki.SinglePlayer.Patches.RaidFix namespace Aki.SinglePlayer.Patches.RaidFix
{ {
@ -15,7 +16,26 @@ namespace Aki.SinglePlayer.Patches.RaidFix
[PatchPostfix] [PatchPostfix]
public static void PatchPostFix() public static void PatchPostFix()
{ {
var gameWorld = Singleton<GameWorld>.Instance;
// Starts tracking of insured items manager
InsuredItemManager.Instance.Init(); 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;
}
} }
} }
} }

View File

@ -2,7 +2,7 @@
using HarmonyLib; using HarmonyLib;
using System; using System;
using System.Reflection; using System.Reflection;
using TraderInfo = EFT.Profile.GClass1729; using TraderInfo = EFT.Profile.GClass1625;
namespace Aki.SinglePlayer.Patches.RaidFix namespace Aki.SinglePlayer.Patches.RaidFix
{ {

View File

@ -16,7 +16,7 @@ namespace Aki.SinglePlayer.Patches.RaidFix
static RemoveUsedBotProfilePatch() static RemoveUsedBotProfilePatch()
{ {
_ = nameof(IBotData.ChooseProfile); _ = nameof(IGetProfileData.ChooseProfile);
_flags = BindingFlags.Instance | BindingFlags.NonPublic; _flags = BindingFlags.Instance | BindingFlags.NonPublic;
_targetInterface = PatchConstants.EftTypes.Single(IsTargetInterface); _targetInterface = PatchConstants.EftTypes.Single(IsTargetInterface);
@ -42,7 +42,7 @@ namespace Aki.SinglePlayer.Patches.RaidFix
/// <summary> /// <summary>
/// BotsPresets.GetNewProfile() /// BotsPresets.GetNewProfile()
[PatchPrefix] [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; withDelete = true;

View File

@ -1,7 +1,7 @@
using Aki.Common.Http;
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using Aki.Reflection.Utils; using Aki.Reflection.Utils;
using System.Linq; using EFT;
using System;
using System.Reflection; using System.Reflection;
namespace Aki.SinglePlayer.Patches.RaidFix namespace Aki.SinglePlayer.Patches.RaidFix
@ -19,7 +19,7 @@ namespace Aki.SinglePlayer.Patches.RaidFix
{ {
protected override MethodBase GetTargetMethod() protected override MethodBase GetTargetMethod()
{ {
var desiredType = typeof(BotSpawnerClass); var desiredType = typeof(BotSpawner);
var desiredMethod = desiredType.GetMethod("CheckOnMax", PatchConstants.PublicFlags); var desiredMethod = desiredType.GetMethod("CheckOnMax", PatchConstants.PublicFlags);
Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}"); Logger.LogDebug($"{this.GetType().Name} Type: {desiredType?.Name}");
@ -29,13 +29,20 @@ namespace Aki.SinglePlayer.Patches.RaidFix
} }
[PatchPrefix] [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
} }
} }
} }

View File

@ -3,6 +3,7 @@ using Comfort.Common;
using System.Reflection; using System.Reflection;
using Aki.Reflection.Patching; using Aki.Reflection.Patching;
using System.Collections; using System.Collections;
using EFT.HealthSystem;
namespace Aki.SinglePlayer.Patches.RaidFix namespace Aki.SinglePlayer.Patches.RaidFix
{ {
@ -17,9 +18,9 @@ namespace Aki.SinglePlayer.Patches.RaidFix
[PatchPrefix] [PatchPrefix]
static bool PatchPrefix() static bool PatchPrefix()
{ {
bool shouldInvoke = typeof(ActiveHealthControllerClass) bool shouldInvoke = typeof(ActiveHealthController)
.GetMethod("FindActiveEffect", BindingFlags.Instance | BindingFlags.Public) .GetMethod("FindActiveEffect", BindingFlags.Instance | BindingFlags.Public)
.MakeGenericMethod(typeof(ActiveHealthControllerClass) .MakeGenericMethod(typeof(ActiveHealthController)
.GetNestedType("Stun", BindingFlags.Instance | BindingFlags.NonPublic)) .GetNestedType("Stun", BindingFlags.Instance | BindingFlags.NonPublic))
.Invoke(Singleton<GameWorld>.Instance.MainPlayer.ActiveHealthController, new object[] { EBodyPart.Common }) != null; .Invoke(Singleton<GameWorld>.Instance.MainPlayer.ActiveHealthController, new object[] { EBodyPart.Common }) != null;

View File

@ -29,10 +29,13 @@ namespace Aki.SinglePlayer.Patches.ScavMode
_ = nameof(TimeAndWeatherSettings.IsRandomWeather); _ = nameof(TimeAndWeatherSettings.IsRandomWeather);
_ = nameof(BotControllerSettings.IsScavWars); _ = nameof(BotControllerSettings.IsScavWars);
_ = nameof(WavesSettings.IsBosses); _ = nameof(WavesSettings.IsBosses);
_ = GClass2952.MAX_SCAV_COUNT; // UPDATE REFS TO THIS CLASS BELOW !!!
var menuControllerType = typeof(MainMenuController); 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); _isLocalField = menuControllerType.GetField("bool_0", PatchConstants.PrivateFlags);
_menuControllerField = typeof(TarkovApplication).GetFields(PatchConstants.PrivateFlags).FirstOrDefault(x => x.FieldType == typeof(MainMenuController)); _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() protected override MethodBase GetTargetMethod()
{ {
return typeof(MainMenuController).GetMethod("method_63", PatchConstants.PrivateFlags); // `MatchMakerSelectionLocationScreen` OnShowNextScreen
return typeof(MainMenuController).GetMethod("method_66", PatchConstants.PrivateFlags);
} }
[PatchTranspiler] [PatchTranspiler]
@ -93,7 +97,7 @@ namespace Aki.SinglePlayer.Patches.ScavMode
var brFalseLabel = generator.DefineLabel(); 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 // 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; codes[onReadyScreenMethodIndex] = callCode;
// We build a new brfalse instruction and give it our new label, then replace the original brfalse instruction // 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 // Get fields from MainMenuController.cs
var raidSettings = Traverse.Create(menuController).Field("raidSettings_0").GetValue<RaidSettings>(); var raidSettings = Traverse.Create(menuController).Field("raidSettings_0").GetValue<RaidSettings>();
var matchmakerPlayersController = Traverse.Create(menuController).Field("gclass3030_0").GetValue<GClass3030>(); var matchmakerPlayersController = Traverse.Create(menuController).Field($"{nameof(GClass2952).ToLowerInvariant()}_0").GetValue<GClass2952>();
var gclass = new MatchmakerOfflineRaidScreen.GClass3019(profile?.Info, ref raidSettings, matchmakerPlayersController); var gclass = new MatchmakerOfflineRaidScreen.GClass2941(profile?.Info, ref raidSettings, matchmakerPlayersController);
gclass.OnShowNextScreen += LoadOfflineRaidNextScreen; gclass.OnShowNextScreen += LoadOfflineRaidNextScreen;
// Ready method // `MatchmakerOfflineRaidScreen` OnShowReadyScreen
gclass.OnShowReadyScreen += (OfflineRaidAction)Delegate.CreateDelegate(typeof(OfflineRaidAction), menuController, "method_67"); gclass.OnShowReadyScreen += (OfflineRaidAction)Delegate.CreateDelegate(typeof(OfflineRaidAction), menuController, "method_70");
gclass.ShowScreen(EScreenState.Queued); gclass.ShowScreen(EScreenState.Queued);
} }

View File

@ -6,6 +6,7 @@ using Aki.Reflection.Utils;
using Aki.SinglePlayer.Models.Healing; using Aki.SinglePlayer.Models.Healing;
using System.Linq; using System.Linq;
using BepInEx.Logging; using BepInEx.Logging;
using EFT.HealthSystem;
namespace Aki.SinglePlayer.Utils.Healing namespace Aki.SinglePlayer.Utils.Healing
{ {

Binary file not shown.