0
0
mirror of https://github.com/sp-tarkov/modules.git synced 2025-02-12 16:50:43 -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
.vs/
# Rider directory
.idea/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/

View File

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

View File

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

View File

@ -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<Dictionary<ETransportProtocolType, string>>();
value[ETransportProtocolType.HTTPS] = "http://";
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 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<GameWorld>.Instance;
if (gameWorld == null)
try
{
Destroy(this);
var gameWorld = Singleton<GameWorld>.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;
}
}

View File

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

View File

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

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()
{
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<BotOwner>)__instance.GetType().GetField("list_1", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance);
if (botOwners.Any(x => x.Id == person.Id))

View File

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

View File

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

View File

@ -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.
/// </summary>
[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

View File

@ -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)
{

View File

@ -41,7 +41,7 @@ namespace Aki.Custom.Patches
/// removes the !player.AIData.IsAI check
/// </summary>
[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

View File

@ -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<WildSpawnType, Dictionary<string, Dictionary<string, int>>> botTypeCache = new Dictionary<WildSpawnType, Dictionary<string, Dictionary<string, int>>>();
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);
}
/// <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
/// Postfix will adjust it back to original type
/// </summary>
/// <param name="__state">state to save for postfix to use later</param>
/// <param name="__instance"></param>
/// <param name="___botOwner_0">botOwner_0 property</param>
[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
}
/// <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>
/// Revert prefix change, get bots type back to what it was before changes
/// </summary>
@ -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<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));
_bundlesField = type.GetField($"{EasyBundleHelper.Type.Name.ToLowerInvariant()}_0", PatchConstants.PrivateFlags);
// DependencyGraph<IEasyBundle>
_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);
}

View File

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

View File

@ -39,22 +39,22 @@ namespace Aki.Custom.Patches
/// Needed to ensure bot checks the enemy side, not just its botType
/// </summary>
[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
}
/// <summary>
@ -104,7 +109,7 @@ namespace Aki.Custom.Patches
/// </summary>
/// <param name="requester"></param>
/// <returns>bool</returns>
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
/// </summary>
/// <param name="requester"></param>
/// <returns></returns>
private static bool ShouldAttackBear(IAIDetails requester)
private static bool ShouldAttackBear(IPlayer requester)
{
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.Utils;
using EFT;
using System;
using System.Linq;
using System.Reflection;
namespace Aki.Custom.Patches
@ -10,11 +13,30 @@ namespace Aki.Custom.Patches
/// </summary>
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);
}
/// <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]

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 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<PreloaderUI>.Instance).Field("_alphaVersionLabel").Property("LocalizationKey").SetValue("{0}");
Traverse.Create(MonoBehaviourSingleton<PreloaderUI>.Instance).Field("string_2").SetValue(_versionLabel);
Traverse.Create(Singleton<PreloaderUI>.Instance).Field("_alphaVersionLabel").Property("LocalizationKey").SetValue("{0}");
Traverse.Create(Singleton<PreloaderUI>.Instance).Field("string_2").SetValue(_versionLabel);
Traverse.Create(__result).Field("Major").SetValue(_versionLabel);
}
}

View File

@ -4,13 +4,14 @@ using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Aki.Reflection.Utils;
using UnityEngine;
using BindableState = BindableState<Diz.DependencyManager.ELoadState>;
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<IEasyBundle>
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<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>
<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="UnityEngine" HintPath="..\Shared\Managed\UnityEngine.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
{
// new CoordinatesPatch().Enable();
new EndRaidDebug().Enable();
}
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 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)
{

View File

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

View File

@ -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<MineDirectionalColliders> _bridgeMines;
private RecodableItemClass _transmitter;
private readonly List<IAIDetails> _zryachiyAndFollowers = new List<IAIDetails>();
private readonly List<IPlayer> _zryachiyAndFollowers = new List<IPlayer>();
private bool _aggressor;
private bool _isDoorDisabled;
private readonly string _transmitterId = "62e910aaf957f2915e0a5e36";
@ -26,34 +25,37 @@ namespace Aki.SinglePlayer.Models.Progression
_gameWorld = Singleton<GameWorld>.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<MineDirectionalColliders>().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();
}
}
/// <summary>
/// Gets transmitter from players inventory
/// </summary>
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>
@ -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;
}
/// <summary>
/// Set all brdige mines to desire state
/// </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()
{
// 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
}
}
/// <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>
/// Disable door + set transmitter to 'red'
/// </summary>

View File

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

View File

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

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
// in-game console shows newer messages at the top
ConsoleScreen consoleScreen = MonoBehaviourSingleton<PreloaderUI>.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);
}
}

View File

@ -17,12 +17,18 @@ namespace Aki.SinglePlayer.Patches.Progression
private static void PatchPostfix()
{
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;
}
gameWorld.GetOrAddComponent<LighthouseProgressionClass>();
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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()
/// </summary>
[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)

View File

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

View File

@ -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
{

View File

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

View File

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

View File

@ -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<GameWorld>.Instance.MainPlayer.ActiveHealthController, new object[] { EBodyPart.Common }) != null;

View File

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

View File

@ -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
{

Binary file not shown.