├── Models ├── FightLocations.cs ├── Presets.cs ├── Position.cs ├── StartingBots.cs ├── PrepBotInfo.cs ├── BotSpawnInfo.cs ├── GetGroupWrapper.cs ├── CreateBotCallbackWrapper.cs ├── Folder.cs ├── SCAVBotLimitPresets.cs ├── PMCBotLimitPresets.cs ├── Entry.cs ├── HotspotTimer.cs ├── ZoneSpawnPoints.cs ├── BotWaves.cs └── Setting.cs ├── Properties └── AssemblyInfo.cs ├── Patches ├── BotProfilePreparationHook.cs ├── BotMemoryAddEnemyPatch.cs ├── BotGroupAddEnemyPatch.cs ├── CoverPointMasterNullRef.cs ├── PatchStandbyTeleport.cs ├── ShootDataNullRefPatch.cs ├── MatchEndPlayerDisposePatch.cs ├── PlayerFireControlPatch.cs ├── AddEnemyPatch.cs └── DelayedGameStartPatch.cs ├── Patterns ├── CustomsScav.json ├── ReservePMC.json └── CustomsPMC.json ├── PluginGUI ├── DrawDebugging.cs ├── DrawSpawnSettings.cs ├── DrawAdvancedSettings.cs ├── DrawSpawnPointMaker.cs ├── IMGUIToolkit.cs ├── DrawMainSettings.cs └── PluginGUIHelper.cs ├── LICENSE.txt ├── README.md ├── Donuts.sln ├── Bots ├── BotCountManager.cs ├── Initialization.cs ├── SpawnChecks.cs └── ProfilePreparationComponent.cs ├── .gitattributes ├── Tools ├── DependencyChecker.cs ├── ConfigurationManagerAttributes.cs ├── Gizmos.cs └── EditorFunctions.cs ├── Donuts - Backup.csproj ├── Donuts.csproj ├── .gitignore └── Plugin.cs /Models/FightLocations.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Donuts.Models 4 | { 5 | internal class FightLocations 6 | { 7 | public List Locations 8 | { 9 | get; set; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Models/Presets.cs: -------------------------------------------------------------------------------- 1 | namespace Donuts.Models 2 | { 3 | internal class Presets 4 | { 5 | public string Name 6 | { 7 | get; set; 8 | } 9 | 10 | public int Weight 11 | { 12 | get; set; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Models/Position.cs: -------------------------------------------------------------------------------- 1 | namespace Donuts.Models 2 | { 3 | internal class Position 4 | { 5 | public float x 6 | { 7 | get; set; 8 | } 9 | public float y 10 | { 11 | get; set; 12 | } 13 | public float z 14 | { 15 | get; set; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | // General Information about an assembly is controlled through the following 4 | // set of attributes. Change these attribute values to modify the information 5 | // associated with an assembly. 6 | [assembly: ComVisible(false)] 7 | 8 | // The following GUID is for the ID of the typelib if this project is exposed to COM 9 | [assembly: Guid("967e5737-8917-4c2b-a0a4-b2b553498462")] 10 | -------------------------------------------------------------------------------- /Patches/BotProfilePreparationHook.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using SPT.Reflection.Patching; 3 | using EFT; 4 | 5 | namespace Donuts.Patches 6 | { 7 | internal class BotProfilePreparationHook : ModulePatch 8 | { 9 | protected override MethodBase GetTargetMethod() => typeof(BotsController).GetMethod(nameof(BotsController.AddActivePLayer)); 10 | 11 | [PatchPrefix] 12 | public static void PatchPrefix() => DonutsBotPrep.Enable(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Patterns/CustomsScav.json: -------------------------------------------------------------------------------- 1 | { 2 | "Locations": [ 3 | { 4 | "MapName": "bigmap", 5 | "Name": "dorms center courtyard scav", 6 | "Position": { 7 | "x": 203.292038, 8 | "y": -0.876712441, 9 | "z": 153.84726 10 | }, 11 | "WildSpawnType": "assault", 12 | "MinDistance": 25.0, 13 | "MaxDistance": 60, 14 | "MaxRandomNumBots": 2, 15 | "SpawnChance": 100 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /Patches/BotMemoryAddEnemyPatch.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using SPT.Reflection.Patching; 3 | using EFT; 4 | 5 | namespace Donuts.Patches 6 | { 7 | internal class BotMemoryAddEnemyPatch : ModulePatch 8 | { 9 | protected override MethodBase GetTargetMethod() => typeof(BotMemoryClass).GetMethod("AddEnemy"); 10 | [PatchPrefix] 11 | public static bool PatchPrefix(IPlayer enemy) 12 | { 13 | if (enemy == null || (enemy.IsAI && enemy.AIData?.BotOwner?.GetPlayer == null)) 14 | { 15 | return false; 16 | } 17 | 18 | return true; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Models/StartingBots.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Donuts.Models 4 | { 5 | public class BotConfig 6 | { 7 | public int MinCount { get; set; } 8 | public int MaxCount { get; set; } 9 | public int MinGroupSize { get; set; } 10 | public int MaxGroupSize { get; set; } 11 | public List Zones { get; set; } 12 | } 13 | 14 | public class MapBotConfig 15 | { 16 | public BotConfig PMC { get; set; } 17 | public BotConfig SCAV { get; set; } 18 | } 19 | 20 | public class StartingBotConfig 21 | { 22 | public Dictionary Maps { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Patches/BotGroupAddEnemyPatch.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using SPT.Reflection.Patching; 3 | using EFT; 4 | 5 | namespace Donuts.Patches 6 | { 7 | // Don't add invalid enemies 8 | internal class BotGroupAddEnemyPatch : ModulePatch 9 | { 10 | protected override MethodBase GetTargetMethod() => typeof(BotsGroup).GetMethod("AddEnemy"); 11 | [PatchPrefix] 12 | public static bool PatchPrefix(IPlayer person) 13 | { 14 | if (person == null || (person.IsAI && person.AIData?.BotOwner?.GetPlayer == null)) 15 | { 16 | return false; 17 | } 18 | 19 | return true; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Models/PrepBotInfo.cs: -------------------------------------------------------------------------------- 1 | using EFT; 2 | 3 | namespace Donuts.Models 4 | { 5 | public class PrepBotInfo 6 | { 7 | public WildSpawnType SpawnType { get; set; } 8 | public BotDifficulty Difficulty { get; set; } 9 | public EPlayerSide Side { get; set; } 10 | public BotCreationDataClass Bots { get; set; } 11 | public bool IsGroup { get; set; } 12 | public int GroupSize { get; set; } 13 | 14 | public PrepBotInfo(WildSpawnType spawnType, BotDifficulty difficulty, EPlayerSide side, bool isGroup = false, int groupSize = 1) 15 | { 16 | SpawnType = spawnType; 17 | Difficulty = difficulty; 18 | Side = side; 19 | IsGroup = isGroup; 20 | GroupSize = groupSize; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Models/BotSpawnInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using EFT; 3 | using UnityEngine; 4 | 5 | public class BotSpawnInfo 6 | { 7 | public WildSpawnType BotType { get; set; } 8 | public int GroupSize { get; set; } 9 | public List Coordinates { get; set; } 10 | public BotDifficulty Difficulty { get; set; } 11 | public EPlayerSide Faction { get; set; } 12 | public string Zone { get; set; } 13 | 14 | public BotSpawnInfo(WildSpawnType botType, int groupSize, List coordinates, BotDifficulty difficulty, EPlayerSide faction, string zone) 15 | { 16 | BotType = botType; 17 | GroupSize = groupSize; 18 | Coordinates = coordinates; 19 | Difficulty = difficulty; 20 | Faction = faction; 21 | Zone = zone; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Models/GetGroupWrapper.cs: -------------------------------------------------------------------------------- 1 | using EFT; 2 | 3 | using static Donuts.DonutComponent; 4 | 5 | namespace Donuts.Models 6 | { 7 | // Custom GetGroupAndSetEnemies wrapper that handles grouping bots into multiple groups within the same botzone 8 | internal class GetGroupWrapper 9 | { 10 | private BotsGroup group = null; 11 | 12 | public BotsGroup GetGroupAndSetEnemies(BotOwner bot, BotZone zone) 13 | { 14 | // If we haven't found/created our BotsGroup yet, do so, and then lock it so nobody else can use it 15 | if (group == null) 16 | { 17 | group = botSpawnerClass.GetGroupAndSetEnemies(bot, zone); 18 | group.Lock(); 19 | } 20 | 21 | return group; 22 | } 23 | } 24 | 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Models/CreateBotCallbackWrapper.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using EFT; 3 | using static Donuts.DonutComponent; 4 | 5 | namespace Donuts.Models 6 | { 7 | // Wrapper around method_10 called after bot creation, so we can pass it the BotCreationDataClass data 8 | 9 | internal class CreateBotCallbackWrapper 10 | { 11 | public BotCreationDataClass botData; 12 | public Stopwatch stopWatch = new Stopwatch(); 13 | 14 | public void CreateBotCallback(BotOwner bot) 15 | { 16 | bool shallBeGroup = botData.SpawnParams?.ShallBeGroup != null; 17 | 18 | // I have no idea why BSG passes a stopwatch into this call... 19 | stopWatch.Start(); 20 | methodCache["method_10"].Invoke(botSpawnerClass, new object[] { bot, botData, null, shallBeGroup, stopWatch }); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Models/Folder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Donuts.Models 4 | { 5 | internal class Folder 6 | { 7 | public string Name 8 | { 9 | get; set; 10 | } 11 | public int Weight 12 | { 13 | get; set; 14 | } 15 | public bool RandomSelection 16 | { 17 | get; set; 18 | } 19 | 20 | public PMCBotLimitPresets PMCBotLimitPresets 21 | { 22 | get; set; 23 | } 24 | 25 | public SCAVBotLimitPresets SCAVBotLimitPresets 26 | { 27 | get; set; 28 | } 29 | 30 | public string RandomScenarioConfig 31 | { 32 | get; set; 33 | } 34 | 35 | public List presets 36 | { 37 | get; set; 38 | } 39 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Patterns/ReservePMC.json: -------------------------------------------------------------------------------- 1 | { 2 | "Locations": [ 3 | { 4 | "MapName": "rezervbase", 5 | "Name": "basement usec", 6 | "Position": { 7 | "x": -103.623047, 8 | "y": -14.5272884, 9 | "z": 42.0608864 10 | }, 11 | "WildSpawnType": "sptusec", 12 | "MinDistance": 25.0, 13 | "MaxDistance": 60, 14 | "MaxRandomNumBots": 2, 15 | "SpawnChance": 100 16 | }, 17 | { 18 | "MapName": "rezervbase", 19 | "Name": "basement bears", 20 | "Position": { 21 | "x": -103.623047, 22 | "y": -14.5272884, 23 | "z": 42.0608864 24 | }, 25 | "WildSpawnType": "sptbear", 26 | "MinDistance": 25.0, 27 | "MaxDistance": 60, 28 | "MaxRandomNumBots": 2, 29 | "SpawnChance": 100 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /Patterns/CustomsPMC.json: -------------------------------------------------------------------------------- 1 | { 2 | "Locations": [ 3 | { 4 | "MapName": "bigmap", 5 | "Name": "dorms center courtyard usec", 6 | "Position": { 7 | "x": 203.292038, 8 | "y": -0.876712441, 9 | "z": 153.84726 10 | }, 11 | "WildSpawnType": "sptusec", 12 | "MinDistance": 25.0, 13 | "MaxDistance": 60, 14 | "MaxRandomNumBots": 2, 15 | "SpawnChance": 100 16 | }, 17 | { 18 | "MapName": "bigmap", 19 | "Name": "dorms center courtyard bear", 20 | "Position": { 21 | "x": 203.292038, 22 | "y": -0.876712441, 23 | "z": 153.84726 24 | }, 25 | "WildSpawnType": "sptbear", 26 | "MinDistance": 25.0, 27 | "MaxDistance": 60, 28 | "MaxRandomNumBots": 2, 29 | "SpawnChance": 100 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /PluginGUI/DrawDebugging.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using static Donuts.PluginGUIHelper; 3 | using static Donuts.DefaultPluginVars; 4 | 5 | namespace Donuts 6 | { 7 | internal class DrawDebugging 8 | { 9 | internal static void Enable() 10 | { 11 | GUILayout.Space(30); 12 | GUILayout.BeginHorizontal(); 13 | GUILayout.BeginVertical(); 14 | 15 | // Add toggles for DebugGizmos and gizmoRealSize 16 | DebugGizmos.Value = ImGUIToolkit.Toggle(DebugGizmos.Name, DebugGizmos.ToolTipText, DebugGizmos.Value); 17 | GUILayout.Space(10); 18 | 19 | gizmoRealSize.Value = ImGUIToolkit.Toggle(gizmoRealSize.Name, gizmoRealSize.ToolTipText, gizmoRealSize.Value); 20 | GUILayout.Space(10); 21 | 22 | GUILayout.EndVertical(); 23 | GUILayout.EndHorizontal(); 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Patches/CoverPointMasterNullRef.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Reflection; 3 | using SPT.Reflection.Patching; 4 | using EFT; 5 | using HarmonyLib; 6 | using UnityEngine; 7 | 8 | namespace Donuts.Patches 9 | { 10 | internal class CoverPointMasterNullRef : ModulePatch 11 | { 12 | protected override MethodBase GetTargetMethod() 13 | { 14 | 15 | return AccessTools.Method(typeof(CoverPointMaster), nameof(CoverPointMaster.GetClosePoints)); 16 | } 17 | 18 | [PatchPrefix] 19 | public static bool Prefix(Vector3 pos, BotOwner bot, float dist, ref List __result) 20 | { 21 | if (bot == null || bot.Covers == null) 22 | { 23 | __result = new List(); // Return an empty list or handle as needed 24 | return false; 25 | } 26 | return true; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Models/SCAVBotLimitPresets.cs: -------------------------------------------------------------------------------- 1 | namespace Donuts.Models 2 | { 3 | internal class SCAVBotLimitPresets 4 | { 5 | public int FactoryBotLimit 6 | { 7 | get; set; 8 | } 9 | public int InterchangeBotLimit 10 | { 11 | get; set; 12 | } 13 | public int LaboratoryBotLimit 14 | { 15 | get; set; 16 | } 17 | public int LighthouseBotLimit 18 | { 19 | get; set; 20 | } 21 | public int ReserveBotLimit 22 | { 23 | get; set; 24 | } 25 | public int ShorelineBotLimit 26 | { 27 | get; set; 28 | } 29 | public int WoodsBotLimit 30 | { 31 | get; set; 32 | } 33 | public int CustomsBotLimit 34 | { 35 | get; set; 36 | } 37 | public int TarkovStreetsBotLimit 38 | { 39 | get; set; 40 | } 41 | public int GroundZeroBotLimit 42 | { 43 | get; set; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Models/PMCBotLimitPresets.cs: -------------------------------------------------------------------------------- 1 | namespace Donuts.Models 2 | { 3 | internal class PMCBotLimitPresets 4 | { 5 | public int FactoryBotLimit 6 | { 7 | get; set; 8 | } 9 | public int InterchangeBotLimit 10 | { 11 | get; set; 12 | } 13 | public int LaboratoryBotLimit 14 | { 15 | get; set; 16 | } 17 | public int LighthouseBotLimit 18 | { 19 | get; set; 20 | } 21 | public int ReserveBotLimit 22 | { 23 | get; set; 24 | } 25 | public int ShorelineBotLimit 26 | { 27 | get; set; 28 | } 29 | public int WoodsBotLimit 30 | { 31 | get; set; 32 | } 33 | public int CustomsBotLimit 34 | { 35 | get; set; 36 | } 37 | public int TarkovStreetsBotLimit 38 | { 39 | get; set; 40 | } 41 | public int GroundZeroBotLimit 42 | { 43 | get; set; 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Patches/PatchStandbyTeleport.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using SPT.Reflection.Patching; 4 | using EFT; 5 | using HarmonyLib; 6 | 7 | namespace Donuts.Patches 8 | { 9 | internal class PatchStandbyTeleport : ModulePatch 10 | { 11 | private static MethodInfo _method1; 12 | 13 | protected override MethodBase GetTargetMethod() 14 | { 15 | Type standbyClassType = typeof(BotStandBy); 16 | _method1 = AccessTools.Method(standbyClassType, "method_1"); 17 | 18 | return AccessTools.Method(typeof(BotStandBy), nameof(BotStandBy.UpdateNode)); 19 | } 20 | 21 | [PatchPrefix] 22 | public static bool Prefix(BotStandBy __instance, BotStandByType ___standByType, BotOwner ___botOwner_0) 23 | { 24 | if (!___botOwner_0.Settings.FileSettings.Mind.CAN_STAND_BY) 25 | { 26 | return false; 27 | } 28 | 29 | if (!__instance.CanDoStandBy) 30 | { 31 | return false; 32 | } 33 | 34 | if (___standByType == BotStandByType.goToSave) 35 | { 36 | _method1.Invoke(__instance, new object[] { }); 37 | } 38 | 39 | return false; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Patches/ShootDataNullRefPatch.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using SPT.Reflection.Patching; 3 | using EFT; 4 | using HarmonyLib; 5 | using UnityEngine; 6 | 7 | namespace Donuts.Patches 8 | { 9 | internal class ShootDataNullRefPatch : ModulePatch 10 | { 11 | protected override MethodBase GetTargetMethod() 12 | { 13 | 14 | return AccessTools.Method(typeof(ShootData), nameof(ShootData.method_0)); 15 | } 16 | 17 | [PatchPrefix] 18 | private static bool Prefix(ShootData __instance, BotOwner ____owner) 19 | { 20 | // Check for null references in necessary fields 21 | if (____owner == null) 22 | { 23 | Debug.LogError("ShootData.method_0(): _owner is null."); 24 | return false; 25 | } 26 | 27 | if (____owner.WeaponRoot == null) 28 | { 29 | Debug.LogError("ShootData.method_0(): _owner.WeaponRoot is null."); 30 | return false; 31 | } 32 | 33 | if (____owner.Position == null) 34 | { 35 | Debug.LogError("ShootData.method_0(): _owner.Position is null."); 36 | return false; 37 | } 38 | 39 | return true; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Donuts Rules 2 | 3 | 1. Bots will only spawn in same level/height as the spawn marker 4 | 2. Bots will only spawn in maximum distance (radius) around the spawn marker 5 | 3. One random spawn marker will be picked in a group 6 | - if the timer is passed its eligible to spawn (Unless IgnoreTimerFirstSpawn is true for the point. It will be set to false after a successful spawn) 7 | - if they are within the BotTimerTrigger distance the point is eligible to spawn. 8 | - If the SpawnChance is reached, it is eligible to spawn. 9 | - Validate that the spawn is not in a wall, in the air, in the player's line of site, minimum distance from the player. It will attempt to find a valid point up to the Bepinex Configured Max Tries specified. 10 | - One to MaxRandomNumBots from the Spawn Marker info will be generated of type WildSpawnType 11 | 4. Timers will be reset if there is a successful spawn or a failure from within a group. 12 | 5. If a spawn sucessfully spawns up to their MaxSpawnsBeforeCooldown number, then it is in 'cooldown' until the timer specified in the bepinex config is reached. 13 | 14 | Assumptions 15 | - Spawns within a group will be on/around the same bot trigger distance otherwise only the closest spawn will be enabled. 16 | - Each unique or standalone spawn should be given its own group number. 17 | -------------------------------------------------------------------------------- /Patches/MatchEndPlayerDisposePatch.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using SPT.Reflection.Patching; 5 | using EFT; 6 | using EFT.AssetsManager; 7 | using HarmonyLib; 8 | using UnityEngine; 9 | 10 | namespace Donuts.Patches 11 | { 12 | internal class MatchEndPlayerDisposePatch : ModulePatch 13 | { 14 | protected override MethodBase GetTargetMethod() 15 | { 16 | // Method used by SPT for finding BaseLocalGame 17 | return AccessTools.Method(typeof(BaseLocalGame), nameof(BaseLocalGame.smethod_4)); 18 | } 19 | 20 | [PatchPrefix] 21 | private static bool PatchPrefix(IDictionary players) 22 | { 23 | foreach (Player player in players.Values) 24 | { 25 | if (player != null) 26 | { 27 | try 28 | { 29 | player.Dispose(); 30 | AssetPoolObject.ReturnToPool(player.gameObject, true); 31 | } 32 | catch (Exception ex) 33 | { 34 | Debug.LogException(ex); 35 | } 36 | } 37 | } 38 | players.Clear(); 39 | 40 | return false; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Models/Entry.cs: -------------------------------------------------------------------------------- 1 | namespace Donuts.Models 2 | { 3 | internal class Entry 4 | { 5 | public string MapName 6 | { 7 | get; set; 8 | } 9 | public int GroupNum 10 | { 11 | get; set; 12 | } 13 | public string Name 14 | { 15 | get; set; 16 | } 17 | public Position Position 18 | { 19 | get; set; 20 | } 21 | public string WildSpawnType 22 | { 23 | get; set; 24 | } 25 | public float MinDistance 26 | { 27 | get; set; 28 | } 29 | public float MaxDistance 30 | { 31 | get; set; 32 | } 33 | 34 | public float BotTriggerDistance 35 | { 36 | get; set; 37 | } 38 | 39 | public float BotTimerTrigger 40 | { 41 | get; set; 42 | } 43 | public int MaxRandomNumBots 44 | { 45 | get; set; 46 | } 47 | 48 | public int SpawnChance 49 | { 50 | get; set; 51 | } 52 | 53 | public int MaxSpawnsBeforeCoolDown 54 | { 55 | get; set; 56 | } 57 | 58 | public bool IgnoreTimerFirstSpawn 59 | { 60 | get; set; 61 | } 62 | 63 | public float MinSpawnDistanceFromPlayer 64 | { 65 | get; set; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Donuts.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.4.33205.214 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Donuts", "Donuts.csproj", "{967E5737-8917-4C2B-A0A4-B2B553498462}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Debug|x64 = Debug|x64 12 | Release|Any CPU = Release|Any CPU 13 | Release|x64 = Release|x64 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {967E5737-8917-4C2B-A0A4-B2B553498462}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {967E5737-8917-4C2B-A0A4-B2B553498462}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {967E5737-8917-4C2B-A0A4-B2B553498462}.Debug|x64.ActiveCfg = Debug|x64 19 | {967E5737-8917-4C2B-A0A4-B2B553498462}.Debug|x64.Build.0 = Debug|x64 20 | {967E5737-8917-4C2B-A0A4-B2B553498462}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {967E5737-8917-4C2B-A0A4-B2B553498462}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {967E5737-8917-4C2B-A0A4-B2B553498462}.Release|x64.ActiveCfg = Release|x64 23 | {967E5737-8917-4C2B-A0A4-B2B553498462}.Release|x64.Build.0 = Release|x64 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {A22BA89D-F884-4F59-9D1D-A5C72E1CD38D} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /Patches/PlayerFireControlPatch.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Donuts; 8 | using HarmonyLib; 9 | using SPT.Reflection.Patching; 10 | 11 | namespace dvize.Donuts.Patches 12 | { 13 | internal class PlayerFireControlPatchGetter : ModulePatch 14 | { 15 | protected override MethodBase GetTargetMethod() 16 | { 17 | var playerType = typeof(EFT.Player.FirearmController); 18 | return AccessTools.PropertyGetter(playerType, "IsTriggerPressed"); 19 | } 20 | 21 | [PatchPrefix] 22 | public static bool PrefixGet(ref bool __result) 23 | { 24 | if (DefaultPluginVars.showGUI) 25 | { 26 | __result = false; 27 | return false; 28 | } 29 | return true; // Continue with the original getter 30 | } 31 | } 32 | 33 | internal class PlayerFireControlPatchSetter : ModulePatch 34 | { 35 | protected override MethodBase GetTargetMethod() 36 | { 37 | var playerType = typeof(EFT.Player.FirearmController); 38 | return AccessTools.PropertySetter(playerType, "IsTriggerPressed"); 39 | } 40 | 41 | [PatchPrefix] 42 | public static bool PrefixSet(ref bool value) 43 | { 44 | if (DefaultPluginVars.showGUI) 45 | { 46 | value = false; 47 | return false; 48 | } 49 | return true; // Continue with the original setter 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Patches/AddEnemyPatch.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Reflection; 4 | using SPT.Reflection.Patching; 5 | using EFT; 6 | 7 | // Stolen from DanW's Questing Bots, thanks Dan! 8 | 9 | namespace Donuts.Patches 10 | { 11 | public class AddEnemyPatch : ModulePatch 12 | { 13 | protected override MethodBase GetTargetMethod() 14 | { 15 | return typeof(BotsGroup).GetMethod("AddEnemy", BindingFlags.Public | BindingFlags.Instance); 16 | } 17 | 18 | [PatchPrefix] 19 | private static bool PatchPrefix(BotsGroup __instance, IPlayer person, EBotEnemyCause cause) 20 | { 21 | // We only care about bot groups adding you as an enemy 22 | if (!person.IsYourPlayer) 23 | { 24 | return true; 25 | } 26 | 27 | // This only matters in Scav raids 28 | // TO DO: This might also matter in PMC raids if a mod adds groups that are friendly to the player 29 | /*if (!Aki.SinglePlayer.Utils.InRaid.RaidChangesUtil.IsScavRaid) 30 | { 31 | return true; 32 | }*/ 33 | 34 | // We only care about one enemy cause 35 | if (cause != EBotEnemyCause.pmcBossKill) 36 | { 37 | return true; 38 | } 39 | 40 | // Get the ID's of all group members 41 | List groupMemberList = new List(); 42 | for (int m = 0; m < __instance.MembersCount; m++) 43 | { 44 | groupMemberList.Add(__instance.Member(m)); 45 | } 46 | string[] groupMemberIDs = groupMemberList.Select(m => m.Profile.Id).ToArray(); 47 | 48 | return true; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Models/HotspotTimer.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace Donuts.Models 4 | { 5 | internal class HotspotTimer 6 | { 7 | private Entry hotspot; 8 | private float timer; 9 | public bool inCooldown; 10 | public int timesSpawned; 11 | private float cooldownTimer; 12 | public Entry Hotspot => hotspot; 13 | 14 | public HotspotTimer(Entry hotspot) 15 | { 16 | this.hotspot = hotspot; 17 | this.timer = 0f; 18 | this.inCooldown = false; 19 | this.timesSpawned = 0; 20 | this.cooldownTimer = 0f; 21 | } 22 | 23 | public void UpdateTimer() 24 | { 25 | timer += Time.deltaTime; 26 | if (inCooldown) 27 | { 28 | cooldownTimer += Time.deltaTime; 29 | if (cooldownTimer >= DefaultPluginVars.coolDownTimer.Value) 30 | { 31 | inCooldown = false; 32 | cooldownTimer = 0f; 33 | timesSpawned = 0; 34 | } 35 | } 36 | } 37 | 38 | public float GetTimer() => timer; 39 | 40 | public bool ShouldSpawn() 41 | { 42 | if (inCooldown) 43 | { 44 | return false; 45 | } 46 | 47 | if (hotspot.IgnoreTimerFirstSpawn) 48 | { 49 | hotspot.IgnoreTimerFirstSpawn = false; // Ensure this is only true for the first spawn 50 | return true; 51 | } 52 | 53 | return timer >= hotspot.BotTimerTrigger; 54 | } 55 | 56 | public void ResetTimer() => timer = 0f; 57 | 58 | public void TriggerCooldown() 59 | { 60 | inCooldown = true; 61 | cooldownTimer = 0f; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Bots/BotCountManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using SPT.PrePatch; 3 | using Cysharp.Threading.Tasks; 4 | using EFT; 5 | using static Donuts.DonutComponent; 6 | using System.Threading; 7 | 8 | #pragma warning disable IDE0007, IDE0044 9 | 10 | namespace Donuts 11 | { 12 | public static class BotCountManager 13 | { 14 | public static UniTask GetAlivePlayers(string spawnType, CancellationToken cancellationToken) 15 | { 16 | return UniTask.Create(async () => 17 | { 18 | int count = 0; 19 | foreach (Player bot in gameWorld.AllAlivePlayersList) 20 | { 21 | if (!bot.IsYourPlayer) 22 | { 23 | switch (spawnType) 24 | { 25 | case "scav": 26 | if (IsSCAV(bot.Profile.Info.Settings.Role)) 27 | { 28 | count++; 29 | } 30 | break; 31 | 32 | case "pmc": 33 | if (IsPMC(bot.Profile.Info.Settings.Role)) 34 | { 35 | count++; 36 | } 37 | break; 38 | 39 | default: 40 | throw new ArgumentException("Invalid spawnType", nameof(spawnType)); 41 | } 42 | } 43 | } 44 | 45 | return count; 46 | }); 47 | } 48 | 49 | public static bool IsPMC(WildSpawnType role) 50 | { 51 | return role == WildSpawnType.pmcUSEC || role == WildSpawnType.pmcBEAR; 52 | } 53 | 54 | public static bool IsSCAV(WildSpawnType role) 55 | { 56 | return role == WildSpawnType.assault; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Models/ZoneSpawnPoints.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using Newtonsoft.Json; 4 | 5 | namespace Donuts.Models 6 | { 7 | public class Coordinate 8 | { 9 | public float x { get; set; } 10 | public float y { get; set; } 11 | public float z { get; set; } 12 | } 13 | 14 | public class MapZoneConfig 15 | { 16 | public string MapName { get; set; } 17 | public Dictionary> Zones { get; set; } 18 | } 19 | 20 | public class AllMapsZoneConfig 21 | { 22 | public Dictionary Maps { get; set; } = new Dictionary(); 23 | 24 | public static AllMapsZoneConfig LoadFromDirectory(string directoryPath) 25 | { 26 | var allMapsConfig = new AllMapsZoneConfig(); 27 | var files = Directory.GetFiles(directoryPath, "*.json"); 28 | 29 | foreach (var file in files) 30 | { 31 | var jsonString = File.ReadAllText(file); 32 | var mapConfig = JsonConvert.DeserializeObject(jsonString); 33 | 34 | if (mapConfig != null) 35 | { 36 | if (!allMapsConfig.Maps.ContainsKey(mapConfig.MapName)) 37 | { 38 | allMapsConfig.Maps[mapConfig.MapName] = new MapZoneConfig 39 | { 40 | MapName = mapConfig.MapName, 41 | Zones = new Dictionary>() 42 | }; 43 | } 44 | 45 | foreach (var zone in mapConfig.Zones) 46 | { 47 | if (!allMapsConfig.Maps[mapConfig.MapName].Zones.ContainsKey(zone.Key)) 48 | { 49 | allMapsConfig.Maps[mapConfig.MapName].Zones[zone.Key] = new List(); 50 | } 51 | 52 | allMapsConfig.Maps[mapConfig.MapName].Zones[zone.Key].AddRange(zone.Value); 53 | } 54 | } 55 | } 56 | 57 | return allMapsConfig; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Patches/DelayedGameStartPatch.cs: -------------------------------------------------------------------------------- 1 | using EFT; 2 | using System; 3 | using System.Collections; 4 | using System.Reflection; 5 | using SPT.Reflection.Patching; 6 | using Donuts; 7 | using UnityEngine; 8 | using Comfort.Common; 9 | 10 | namespace Donuts.Patches 11 | { 12 | public class DelayedGameStartPatch : ModulePatch 13 | { 14 | private static object localGameObj = null; 15 | protected override MethodBase GetTargetMethod() 16 | { 17 | Type baseGameType = typeof(BaseLocalGame); 18 | return baseGameType.GetMethod("vmethod_4", BindingFlags.Public | BindingFlags.Instance); 19 | } 20 | 21 | [PatchPostfix] 22 | private static void PatchPostfix(ref IEnumerator __result, object __instance) 23 | { 24 | if(!Singleton.Instance.InRaid) 25 | { 26 | return; 27 | } 28 | 29 | localGameObj = __instance; 30 | 31 | if(DonutComponent.IsBotSpawningEnabled) 32 | { 33 | __result = addIterationsToWaitForBotGenerators(__result); // Thanks danW 34 | } 35 | } 36 | 37 | private static IEnumerator addIterationsToWaitForBotGenerators(IEnumerator originalTask) 38 | { 39 | // Now also wait for all bots to be fully initialized 40 | Logger.LogWarning("Donuts is waiting for bot preparation to complete..."); 41 | float lastLogTime = Time.time; 42 | float startTime = Time.time; 43 | float maxWaitTime = DefaultPluginVars.maxRaidDelay.Value; 44 | 45 | while (!DonutsBotPrep.IsBotPreparationComplete) 46 | { 47 | yield return new WaitForEndOfFrame(); // Check every frame 48 | 49 | if (Time.time - lastLogTime >= 1.0f) 50 | { 51 | lastLogTime = Time.time; // Update the last log time 52 | Logger.LogWarning("Donuts still waiting..."); 53 | } 54 | 55 | if (Time.time - startTime >= maxWaitTime) 56 | { 57 | Logger.LogWarning("Max raid delay time reached. Proceeding with raid start, some bots might spawn late!"); 58 | break; 59 | } 60 | } 61 | 62 | // Continue with the original task 63 | Logger.LogWarning("Donuts bot preparation is complete..."); 64 | yield return originalTask; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Models/BotWaves.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEngine; 3 | 4 | namespace Donuts.Models 5 | { 6 | public class BotWave 7 | { 8 | public int GroupNum { get; set; } 9 | public int TriggerTimer { get; set; } 10 | public int TriggerDistance { get; set; } 11 | public int SpawnChance { get; set; } 12 | public int MaxTriggersBeforeCooldown { get; set; } 13 | public bool IgnoreTimerFirstSpawn { get; set; } 14 | public int MinGroupSize { get; set; } 15 | public int MaxGroupSize { get; set; } 16 | public List Zones { get; set; } 17 | 18 | // Timer properties 19 | private float timer; 20 | public bool InCooldown { get; set; } 21 | public int TimesSpawned { get; set; } 22 | private float cooldownTimer; 23 | 24 | public BotWave() 25 | { 26 | this.timer = 0f; 27 | this.InCooldown = false; 28 | this.TimesSpawned = 0; 29 | this.cooldownTimer = 0f; 30 | } 31 | 32 | public void UpdateTimer(float deltaTime, float coolDownDuration) 33 | { 34 | timer += deltaTime; 35 | if (InCooldown) 36 | { 37 | cooldownTimer += deltaTime; 38 | 39 | if (cooldownTimer >= coolDownDuration) 40 | { 41 | InCooldown = false; 42 | cooldownTimer = 0f; 43 | TimesSpawned = 0; 44 | } 45 | } 46 | } 47 | 48 | public bool ShouldSpawn() 49 | { 50 | if (InCooldown) 51 | { 52 | return false; 53 | } 54 | 55 | if (IgnoreTimerFirstSpawn) 56 | { 57 | IgnoreTimerFirstSpawn = false; // Ensure this is only true for the first spawn 58 | return true; 59 | } 60 | 61 | return timer >= TriggerTimer; 62 | } 63 | 64 | public void ResetTimer() 65 | { 66 | timer = 0f; 67 | } 68 | 69 | public void TriggerCooldown() 70 | { 71 | InCooldown = true; 72 | cooldownTimer = 0f; 73 | } 74 | } 75 | 76 | public class MapBotWaves 77 | { 78 | public List PMC { get; set; } 79 | public List SCAV { get; set; } 80 | } 81 | 82 | public class BotWavesConfig 83 | { 84 | public Dictionary Maps { get; set; } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /Models/Setting.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Donuts.Models 6 | { 7 | internal class Setting 8 | { 9 | private string toolTipText; 10 | 11 | public string Name 12 | { 13 | get; set; 14 | } 15 | 16 | public string ToolTipText 17 | { 18 | get => toolTipText; 19 | set => toolTipText = InsertCarriageReturns(value, 50); 20 | } 21 | 22 | public T Value 23 | { 24 | get; set; 25 | } 26 | public T DefaultValue 27 | { 28 | get; set; 29 | } 30 | public T MinValue 31 | { 32 | get; set; 33 | } 34 | public T MaxValue 35 | { 36 | get; set; 37 | } 38 | public T[] Options 39 | { 40 | get; set; 41 | } 42 | 43 | private bool hasLoggedError = false; 44 | 45 | // Constructor to handle regular settings 46 | public Setting(string name, string tooltipText, T value, T defaultValue, T minValue = default(T), T maxValue = default(T), T[] options = null) 47 | { 48 | Name = name; 49 | ToolTipText = tooltipText; 50 | Value = value; 51 | DefaultValue = defaultValue; 52 | MinValue = minValue; 53 | MaxValue = maxValue; 54 | Options = options ?? new T[0]; 55 | } 56 | 57 | public bool LogErrorOnceIfOptionsInvalid() 58 | { 59 | if (Options == null || Options.Length == 0) 60 | { 61 | if (!hasLoggedError) 62 | { 63 | DonutsPlugin.Logger.LogError($"Dropdown setting '{Name}' has an uninitialized or empty options list."); 64 | hasLoggedError = true; 65 | } 66 | return true; 67 | } 68 | return false; 69 | } 70 | 71 | private string InsertCarriageReturns(string text, int maxLength) 72 | { 73 | if (string.IsNullOrEmpty(text) || text.Length <= maxLength) 74 | { 75 | return text; 76 | } 77 | 78 | StringBuilder formattedText = new StringBuilder(); 79 | int start = 0; 80 | 81 | while (start < text.Length) 82 | { 83 | int end = Math.Min(start + maxLength, text.Length); 84 | if (end < text.Length && text[end] != ' ') 85 | { 86 | int lastSpace = text.LastIndexOf(' ', end, end - start); 87 | if (lastSpace > start) 88 | { 89 | end = lastSpace; 90 | } 91 | } 92 | 93 | formattedText.Append(text.Substring(start, end - start).Trim()); 94 | formattedText.AppendLine(); 95 | start = end + 1; 96 | } 97 | 98 | return formattedText.ToString().Trim(); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /PluginGUI/DrawSpawnSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Donuts.Models; 4 | using UnityEngine; 5 | using static Donuts.DefaultPluginVars; 6 | 7 | namespace Donuts 8 | { 9 | internal class DrawSpawnSettings 10 | { 11 | internal static void Enable() 12 | { 13 | 14 | 15 | GUILayout.Space(30); 16 | GUILayout.BeginHorizontal(); 17 | GUILayout.BeginVertical(); 18 | 19 | ImGUIToolkit.Accordion("Global Min Distance To Player Settings", "Click to expand/collapse", () => 20 | { 21 | // Toggle for globalMinSpawnDistanceFromPlayerBool 22 | globalMinSpawnDistanceFromPlayerBool.Value = ImGUIToolkit.Toggle( 23 | globalMinSpawnDistanceFromPlayerBool.Name, 24 | globalMinSpawnDistanceFromPlayerBool.ToolTipText, 25 | globalMinSpawnDistanceFromPlayerBool.Value 26 | ); 27 | 28 | // List of float settings 29 | var floatSettings = new List> 30 | { 31 | globalMinSpawnDistanceFromPlayerFactory, 32 | globalMinSpawnDistanceFromPlayerCustoms, 33 | globalMinSpawnDistanceFromPlayerReserve, 34 | globalMinSpawnDistanceFromPlayerStreets, 35 | globalMinSpawnDistanceFromPlayerWoods, 36 | globalMinSpawnDistanceFromPlayerLaboratory, 37 | globalMinSpawnDistanceFromPlayerShoreline, 38 | globalMinSpawnDistanceFromPlayerGroundZero, 39 | globalMinSpawnDistanceFromPlayerInterchange, 40 | globalMinSpawnDistanceFromPlayerLighthouse 41 | }; 42 | 43 | // Sort the settings by name in ascending order 44 | floatSettings.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); 45 | 46 | // Create sliders for the sorted settings 47 | foreach (var setting in floatSettings) 48 | { 49 | setting.Value = ImGUIToolkit.Slider( 50 | setting.Name, 51 | setting.ToolTipText, 52 | setting.Value, 53 | 0f, 54 | 1000f 55 | ); 56 | } 57 | }); 58 | 59 | 60 | ImGUIToolkit.Accordion("Global Min Distance To Other Bots Settings", "Click to expand/collapse", () => 61 | { 62 | // Toggle for globalMinSpawnDistanceFromOtherBotsBool 63 | globalMinSpawnDistanceFromOtherBotsBool.Value = ImGUIToolkit.Toggle( 64 | globalMinSpawnDistanceFromOtherBotsBool.Name, 65 | globalMinSpawnDistanceFromOtherBotsBool.ToolTipText, 66 | globalMinSpawnDistanceFromOtherBotsBool.Value 67 | ); 68 | 69 | // List of float settings for other bots 70 | var otherBotsFloatSettings = new List> 71 | { 72 | globalMinSpawnDistanceFromOtherBotsFactory, 73 | globalMinSpawnDistanceFromOtherBotsCustoms, 74 | globalMinSpawnDistanceFromOtherBotsReserve, 75 | globalMinSpawnDistanceFromOtherBotsStreets, 76 | globalMinSpawnDistanceFromOtherBotsWoods, 77 | globalMinSpawnDistanceFromOtherBotsLaboratory, 78 | globalMinSpawnDistanceFromOtherBotsShoreline, 79 | globalMinSpawnDistanceFromOtherBotsGroundZero, 80 | globalMinSpawnDistanceFromOtherBotsInterchange, 81 | globalMinSpawnDistanceFromOtherBotsLighthouse 82 | }; 83 | 84 | // Sort the settings by name in ascending order 85 | otherBotsFloatSettings.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); 86 | 87 | // Create sliders for the sorted settings 88 | foreach (var setting in otherBotsFloatSettings) 89 | { 90 | setting.Value = ImGUIToolkit.Slider( 91 | setting.Name, 92 | setting.ToolTipText, 93 | setting.Value, 94 | 0f, 95 | 1000f 96 | ); 97 | } 98 | }); 99 | 100 | GUILayout.EndVertical(); 101 | 102 | GUILayout.EndHorizontal(); 103 | } 104 | 105 | 106 | 107 | 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /PluginGUI/DrawAdvancedSettings.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using static Donuts.PluginGUIHelper; 3 | using static Donuts.DefaultPluginVars; 4 | using Donuts.Models; 5 | using System.Collections.Generic; 6 | using System; 7 | using System.Reflection; 8 | 9 | namespace Donuts 10 | { 11 | internal class DrawAdvancedSettings 12 | { 13 | internal static void Enable() 14 | { 15 | 16 | GUILayout.Space(30); 17 | GUILayout.BeginHorizontal(); 18 | GUILayout.BeginVertical(); 19 | 20 | // Slider for maxRaidDelay 21 | maxRaidDelay.Value = ImGUIToolkit.Slider( 22 | maxRaidDelay.Name, 23 | maxRaidDelay.ToolTipText, 24 | maxRaidDelay.Value, 25 | maxRaidDelay.MinValue, 26 | maxRaidDelay.MaxValue 27 | ); 28 | 29 | // Slider for replenishInterval 30 | replenishInterval.Value = ImGUIToolkit.Slider( 31 | replenishInterval.Name, 32 | replenishInterval.ToolTipText, 33 | replenishInterval.Value, 34 | replenishInterval.MinValue, 35 | replenishInterval.MaxValue 36 | ); 37 | 38 | // Slider for maxSpawnTriesPerBot 39 | maxSpawnTriesPerBot.Value = ImGUIToolkit.Slider( 40 | maxSpawnTriesPerBot.Name, 41 | maxSpawnTriesPerBot.ToolTipText, 42 | maxSpawnTriesPerBot.Value, 43 | maxSpawnTriesPerBot.MinValue, 44 | maxSpawnTriesPerBot.MaxValue 45 | ); 46 | 47 | // Slider for despawnInterval 48 | despawnInterval.Value = ImGUIToolkit.Slider( 49 | despawnInterval.Name, 50 | despawnInterval.ToolTipText, 51 | despawnInterval.Value, 52 | despawnInterval.MinValue, 53 | despawnInterval.MaxValue 54 | ); 55 | 56 | groupWeightDistroLow.Value = ImGUIToolkit.TextField(groupWeightDistroLow.Name, groupWeightDistroLow.ToolTipText, groupWeightDistroLow.Value); 57 | groupWeightDistroDefault.Value = ImGUIToolkit.TextField(groupWeightDistroDefault.Name, groupWeightDistroDefault.ToolTipText, groupWeightDistroDefault.Value); 58 | groupWeightDistroHigh.Value = ImGUIToolkit.TextField(groupWeightDistroHigh.Name, groupWeightDistroHigh.ToolTipText, groupWeightDistroHigh.Value); 59 | 60 | GUILayout.Space(150); 61 | 62 | // Reset to Default Values button 63 | GUIStyle redButtonStyle = new GUIStyle(buttonStyle) 64 | { 65 | normal = { background = MakeTex(1, 1, new Color(0.5f, 0.0f, 0.0f)), textColor = Color.white }, 66 | fontSize = 20, 67 | fontStyle = FontStyle.Bold, 68 | alignment = TextAnchor.MiddleCenter 69 | }; 70 | 71 | if (GUILayout.Button("Reset to Default Values", redButtonStyle, GUILayout.Width(250), GUILayout.Height(50))) 72 | { 73 | ResetToDefaults(); 74 | PluginGUIHelper.DisplayMessageNotificationGUI("All Donuts Settings have been reset to default values, but they still need to be saved."); 75 | DonutsPlugin.Logger.LogWarning("All settings have been reset to default values."); 76 | RestartPluginGUIHelper(); 77 | } 78 | GUILayout.EndVertical(); 79 | GUILayout.EndHorizontal(); 80 | 81 | } 82 | 83 | public static void ResetToDefaults() 84 | { 85 | foreach (var field in typeof(DefaultPluginVars).GetFields(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)) 86 | { 87 | if (field.FieldType.IsGenericType && field.FieldType.GetGenericTypeDefinition() == typeof(Setting<>)) 88 | { 89 | var settingValue = field.GetValue(null); 90 | var valueProperty = settingValue.GetType().GetProperty("Value"); 91 | var defaultValueProperty = settingValue.GetType().GetProperty("DefaultValue"); 92 | 93 | var defaultValue = defaultValueProperty.GetValue(settingValue); 94 | valueProperty.SetValue(settingValue, defaultValue); 95 | } 96 | } 97 | 98 | // Reset dropdown indices 99 | DrawMainSettings.InitializeDropdownIndices(); 100 | 101 | // Reset dropdown indices for spawn point maker settings 102 | DrawSpawnPointMaker.InitializeDropdownIndices(); 103 | } 104 | 105 | private static void RestartPluginGUIHelper() 106 | { 107 | if (DonutsPlugin.pluginGUIHelper != null) 108 | { 109 | DonutsPlugin.pluginGUIHelper.enabled = false; 110 | DonutsPlugin.pluginGUIHelper.enabled = true; 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Tools/DependencyChecker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using BepInEx; 3 | using BepInEx.Bootstrap; 4 | using BepInEx.Configuration; 5 | using BepInEx.Logging; 6 | using UnityEngine; 7 | 8 | namespace Donuts 9 | { 10 | internal class DependencyChecker 11 | { 12 | /// 13 | /// Check that all of the BepInDependency entries for the given pluginType are available and instantiated. This allows a 14 | /// plugin to validate that its dependent plugins weren't disabled post-dependency check (Such as for the wrong EFT version) 15 | /// 16 | /// 17 | /// 18 | /// 19 | /// 20 | /// 21 | public static bool ValidateDependencies(ManualLogSource Logger, PluginInfo Info, Type pluginType, ConfigFile Config = null) 22 | { 23 | var noVersion = new Version("0.0.0"); 24 | var dependencies = pluginType.GetCustomAttributes(typeof(BepInDependency), true) as BepInDependency[]; 25 | 26 | foreach (var dependency in dependencies) 27 | { 28 | PluginInfo pluginInfo; 29 | if (!Chainloader.PluginInfos.TryGetValue(dependency.DependencyGUID, out pluginInfo)) 30 | { 31 | pluginInfo = null; 32 | } 33 | 34 | // If the plugin isn't found, or the instance isn't enabled, it means the required plugin failed to load 35 | if (pluginInfo == null || pluginInfo.Instance?.enabled == false) 36 | { 37 | string dependencyName = pluginInfo?.Metadata.Name ?? dependency.DependencyGUID; 38 | string dependencyVersion = ""; 39 | if (dependency.MinimumVersion > noVersion) 40 | { 41 | dependencyVersion = $" v{dependency.MinimumVersion}"; 42 | } 43 | 44 | string errorMessage = $"ERROR: This version of {Info.Metadata.Name} v{Info.Metadata.Version} depends on {dependencyName}{dependencyVersion}, but it was not loaded."; 45 | Logger.LogError(errorMessage); 46 | Chainloader.DependencyErrors.Add(errorMessage); 47 | 48 | if (Config != null) 49 | { 50 | // This results in a bogus config entry in the BepInEx config file for the plugin, but it shouldn't hurt anything 51 | // We leave the "section" parameter empty so there's no section header drawn 52 | Config.Bind("", "MissingDeps", "", new ConfigDescription( 53 | errorMessage, null, new ConfigurationManagerAttributes 54 | { 55 | CustomDrawer = ErrorLabelDrawer, 56 | ReadOnly = true, 57 | HideDefaultButton = true, 58 | HideSettingName = true, 59 | Category = null 60 | } 61 | )); 62 | } 63 | 64 | return false; 65 | } 66 | } 67 | 68 | return true; 69 | } 70 | 71 | static void ErrorLabelDrawer(ConfigEntryBase entry) 72 | { 73 | GUIStyle styleNormal = new GUIStyle(GUI.skin.label); 74 | styleNormal.wordWrap = true; 75 | styleNormal.stretchWidth = true; 76 | 77 | GUIStyle styleError = new GUIStyle(GUI.skin.label); 78 | styleError.stretchWidth = true; 79 | styleError.alignment = TextAnchor.MiddleCenter; 80 | styleError.normal.textColor = Color.red; 81 | styleError.fontStyle = FontStyle.Bold; 82 | 83 | // General notice that we're the wrong version 84 | GUILayout.BeginVertical(); 85 | GUILayout.Label(entry.Description.Description, styleNormal, new GUILayoutOption[] { GUILayout.ExpandWidth(true) }); 86 | 87 | // Centered red disabled text 88 | GUILayout.Label("Plugin has been disabled!", styleError, new GUILayoutOption[] { GUILayout.ExpandWidth(true) }); 89 | GUILayout.EndVertical(); 90 | } 91 | 92 | #pragma warning disable 0169, 0414, 0649 93 | internal sealed class ConfigurationManagerAttributes 94 | { 95 | public bool? ShowRangeAsPercent; 96 | public System.Action CustomDrawer; 97 | public CustomHotkeyDrawerFunc CustomHotkeyDrawer; 98 | public delegate void CustomHotkeyDrawerFunc(BepInEx.Configuration.ConfigEntryBase setting, ref bool isCurrentlyAcceptingInput); 99 | public bool? Browsable; 100 | public string Category; 101 | public object DefaultValue; 102 | public bool? HideDefaultButton; 103 | public bool? HideSettingName; 104 | public string Description; 105 | public string DispName; 106 | public int? Order; 107 | public bool? ReadOnly; 108 | public bool? IsAdvanced; 109 | public System.Func ObjToStr; 110 | public System.Func StrToObj; 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /PluginGUI/DrawSpawnPointMaker.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using static Donuts.PluginGUIHelper; 3 | using static Donuts.ImGUIToolkit; 4 | using static Donuts.DefaultPluginVars; 5 | 6 | #pragma warning disable IDE0007, IDE0044 7 | 8 | namespace Donuts 9 | { 10 | internal class DrawSpawnPointMaker 11 | { 12 | // For tab 13 | private static int selectedSpawnMakerSettingsIndex = 0; 14 | private static string[] spawnMakerSettingsSubTabs = { "Keybinds", "Spawn Setup" }; 15 | 16 | // For dropdowns 17 | internal static int wildSpawnsIndex = 0; 18 | 19 | // Need this for dropdowns to show loaded values and also reset to default 20 | static DrawSpawnPointMaker() 21 | { 22 | InitializeDropdownIndices(); 23 | } 24 | 25 | internal static void InitializeDropdownIndices() 26 | { 27 | wildSpawnsIndex = FindIndex(wildSpawns); 28 | } 29 | 30 | internal static void Enable() 31 | { 32 | 33 | // Initialize the custom styles for the dropdown 34 | GUILayout.Space(30); 35 | GUILayout.BeginHorizontal(); 36 | 37 | // Left-hand navigation menu for sub-tabs 38 | GUILayout.BeginVertical(GUILayout.Width(150)); 39 | 40 | GUILayout.Space(20); 41 | DrawSubTabs(); 42 | GUILayout.EndVertical(); 43 | 44 | // Space between menu and subtab pages 45 | GUILayout.Space(40); 46 | 47 | // Right-hand content area for selected sub-tab 48 | GUILayout.BeginVertical(); 49 | 50 | switch (selectedSpawnMakerSettingsIndex) 51 | { 52 | case 0: 53 | DrawKeybindsTab(); 54 | break; 55 | case 1: 56 | DrawSpawnSetupTab(); 57 | break; 58 | } 59 | 60 | GUILayout.EndVertical(); 61 | GUILayout.EndHorizontal(); 62 | } 63 | 64 | private static void DrawSubTabs() 65 | { 66 | for (int i = 0; i < spawnMakerSettingsSubTabs.Length; i++) 67 | { 68 | GUIStyle currentStyle = subTabButtonStyle; // subTabButtonStyle 69 | if (selectedSpawnMakerSettingsIndex == i) 70 | { 71 | currentStyle = subTabButtonActiveStyle; // subTabButtonActiveStyle 72 | } 73 | 74 | if (GUILayout.Button(spawnMakerSettingsSubTabs[i], currentStyle)) 75 | { 76 | selectedSpawnMakerSettingsIndex = i; 77 | } 78 | } 79 | } 80 | 81 | internal static void DrawKeybindsTab() 82 | { 83 | // Draw general spawn settings 84 | GUILayout.BeginHorizontal(); 85 | GUILayout.BeginVertical(); 86 | 87 | // Draw Keybind settings 88 | CreateSpawnMarkerKey.Value = KeybindField(CreateSpawnMarkerKey.Name, CreateSpawnMarkerKey.ToolTipText, CreateSpawnMarkerKey.Value); 89 | DeleteSpawnMarkerKey.Value = KeybindField(DeleteSpawnMarkerKey.Name, DeleteSpawnMarkerKey.ToolTipText, DeleteSpawnMarkerKey.Value); 90 | WriteToFileKey.Value = KeybindField(WriteToFileKey.Name, WriteToFileKey.ToolTipText, WriteToFileKey.Value); 91 | 92 | // Draw Toggle setting 93 | saveNewFileOnly.Value = Toggle(saveNewFileOnly.Name, saveNewFileOnly.ToolTipText, saveNewFileOnly.Value); 94 | 95 | GUILayout.EndVertical(); 96 | GUILayout.EndHorizontal(); 97 | } 98 | 99 | internal static void DrawSpawnSetupTab() 100 | { 101 | // Draw advanced spawn settings 102 | GUILayout.BeginHorizontal(); 103 | 104 | // First column 105 | GUILayout.BeginVertical(); 106 | 107 | // Define the position and size for the spawnName text field 108 | spawnName.Value = TextField(spawnName.Name, spawnName.ToolTipText, spawnName.Value); 109 | GUILayout.Space(10); 110 | 111 | groupNum.Value = Slider(groupNum.Name, groupNum.ToolTipText, groupNum.Value, groupNum.MinValue, groupNum.MaxValue); 112 | GUILayout.Space(10); 113 | 114 | // Dropdown for wildSpawns 115 | wildSpawnsIndex = Dropdown(wildSpawns, wildSpawnsIndex); 116 | wildSpawns.Value = wildSpawns.Options[wildSpawnsIndex]; 117 | GUILayout.Space(10); 118 | 119 | minSpawnDist.Value = Slider(minSpawnDist.Name, minSpawnDist.ToolTipText, minSpawnDist.Value, minSpawnDist.MinValue, minSpawnDist.MaxValue); 120 | GUILayout.Space(10); 121 | 122 | maxSpawnDist.Value = Slider(maxSpawnDist.Name, maxSpawnDist.ToolTipText, maxSpawnDist.Value, maxSpawnDist.MinValue, maxSpawnDist.MaxValue); 123 | GUILayout.Space(10); 124 | 125 | botTriggerDistance.Value = Slider(botTriggerDistance.Name, botTriggerDistance.ToolTipText, botTriggerDistance.Value, botTriggerDistance.MinValue, botTriggerDistance.MaxValue); 126 | GUILayout.Space(10); 127 | 128 | GUILayout.EndVertical(); 129 | 130 | // Second column 131 | GUILayout.BeginVertical(); 132 | 133 | botTimerTrigger.Value = Slider(botTimerTrigger.Name, botTimerTrigger.ToolTipText, botTimerTrigger.Value, botTimerTrigger.MinValue, botTimerTrigger.MaxValue); 134 | GUILayout.Space(10); 135 | 136 | maxRandNumBots.Value = Slider(maxRandNumBots.Name, maxRandNumBots.ToolTipText, maxRandNumBots.Value, maxRandNumBots.MinValue, maxRandNumBots.MaxValue); 137 | GUILayout.Space(10); 138 | 139 | spawnChance.Value = Slider(spawnChance.Name, spawnChance.ToolTipText, spawnChance.Value, spawnChance.MinValue, spawnChance.MaxValue); 140 | GUILayout.Space(10); 141 | 142 | maxSpawnsBeforeCooldown.Value = Slider(maxSpawnsBeforeCooldown.Name, maxSpawnsBeforeCooldown.ToolTipText, maxSpawnsBeforeCooldown.Value, maxSpawnsBeforeCooldown.MinValue, maxSpawnsBeforeCooldown.MaxValue); 143 | GUILayout.Space(10); 144 | 145 | ignoreTimerFirstSpawn.Value = Toggle(ignoreTimerFirstSpawn.Name, ignoreTimerFirstSpawn.ToolTipText, ignoreTimerFirstSpawn.Value); 146 | GUILayout.Space(10); 147 | 148 | minSpawnDistanceFromPlayer.Value = Slider(minSpawnDistanceFromPlayer.Name, minSpawnDistanceFromPlayer.ToolTipText, minSpawnDistanceFromPlayer.Value, minSpawnDistanceFromPlayer.MinValue, minSpawnDistanceFromPlayer.MaxValue); 149 | GUILayout.Space(10); 150 | 151 | GUILayout.EndVertical(); 152 | GUILayout.EndHorizontal(); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Donuts - Backup.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | dvize.Donuts 6 | dvize.Donuts 7 | true 8 | 9 | 10 | 11 | true 12 | portable 13 | false 14 | bin\Debug\ 15 | DEBUG;TRACE 16 | 8.0 17 | 18 | 19 | 20 | true 21 | portable 22 | true 23 | bin\Release\ 24 | TRACE 25 | 8.0 26 | 27 | 28 | 29 | 30 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\Aki.Common.dll 31 | 32 | 33 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\Aki.Reflection.dll 34 | 35 | 36 | F:\SPT-AKI-DEV\BepInEx\patchers\aki_PrePatch.dll 37 | 38 | 39 | F:\SPT-AKI-DEV\BepInEx\plugins\spt\aki-singleplayer.dll 40 | 41 | 42 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\Assembly-CSharp.dll 43 | 44 | 45 | F:\SPT-AKI-DEV\BepInEx\core\BepInEx.dll 46 | 47 | 48 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\Comfort.dll 49 | 50 | 51 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\DissonanceVoip.dll 52 | 53 | 54 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\DOTween.dll 55 | 56 | 57 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\DOTween.Modules.dll 58 | 59 | 60 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\Newtonsoft.Json.dll 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | F:\SPT-AKI-DEV\BepInEx\plugins\UnityToolkit\UniTask.dll 72 | 73 | 74 | F:\SPT-AKI-DEV\BepInEx\plugins\UnityToolkit\UniTask.DOTween.dll 75 | 76 | 77 | F:\SPT-AKI-DEV\BepInEx\plugins\UnityToolkit\UniTask.Linq.dll 78 | 79 | 80 | F:\SPT-AKI-DEV\BepInEx\plugins\UnityToolkit\UniTask.TextMeshPro.dll 81 | 82 | 83 | F:\SPT-AKI-DEV\BepInEx\plugins\UnityToolkit\Unity.Collections.dll 84 | 85 | 86 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.dll 87 | 88 | 89 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.AIModule.dll 90 | 91 | 92 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.CoreModule.dll 93 | 94 | 95 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.ImageConversionModule.dll 96 | 97 | 98 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.IMGUIModule.dll 99 | 100 | 101 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.InputLegacyModule.dll 102 | 103 | 104 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.PhysicsModule.dll 105 | 106 | 107 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.TextCoreModule.dll 108 | 109 | 110 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.TextRenderingModule.dll 111 | 112 | 113 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.UI.dll 114 | 115 | 116 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.UIModule.dll 117 | 118 | 119 | F:\SPT-AKI-DEV\BepInEx\plugins\UnityToolkit\UnityToolkit.dll 120 | 121 | 122 | 123 | 124 | 125 | copy "$(TargetPath)" "F:\SPT-AKI-DEV\BepInEx\plugins\dvize.Donuts\$(TargetName).dll" 126 | if "$(ConfigurationName)"=="Debug" ( 127 | if exist "$(TargetDir)$(TargetName).pdb" ( 128 | copy "$(TargetDir)$(TargetName).pdb" "F:\SPT-AKI-DEV\BepInEx\plugins\dvize.Donuts\$(TargetName).pdb" 129 | ) else ( 130 | echo Debug symbols not found! 131 | ) 132 | ) else ( 133 | if exist "F:\SPT-AKI-DEV\BepInEx\plugins\dvize.Donuts\$(TargetName).pdb" ( 134 | del "F:\SPT-AKI-DEV\BepInEx\plugins\dvize.Donuts\$(TargetName).pdb" 135 | ) 136 | ) 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /Tools/ConfigurationManagerAttributes.cs: -------------------------------------------------------------------------------- 1 | /// 2 | /// Class that specifies how a setting should be displayed inside the ConfigurationManager settings window. 3 | /// 4 | /// Usage: 5 | /// This class template has to be copied inside the plugin's project and referenced by its code directly. 6 | /// make a new instance, assign any fields that you want to override, and pass it as a tag for your setting. 7 | /// 8 | /// If a field is null (default), it will be ignored and won't change how the setting is displayed. 9 | /// If a field is non-null (you assigned a value to it), it will override default behavior. 10 | /// 11 | /// 12 | /// 13 | /// Here's an example of overriding order of settings and marking one of the settings as advanced: 14 | /// 15 | /// // Override IsAdvanced and Order 16 | /// Config.Bind("X", "1", 1, new ConfigDescription("", null, new ConfigurationManagerAttributes { IsAdvanced = true, Order = 3 })); 17 | /// // Override only Order, IsAdvanced stays as the default value assigned by ConfigManager 18 | /// Config.Bind("X", "2", 2, new ConfigDescription("", null, new ConfigurationManagerAttributes { Order = 1 })); 19 | /// Config.Bind("X", "3", 3, new ConfigDescription("", null, new ConfigurationManagerAttributes { Order = 2 })); 20 | /// 21 | /// 22 | /// 23 | /// 24 | /// You can read more and see examples in the readme at https://github.com/BepInEx/BepInEx.ConfigurationManager 25 | /// You can optionally remove fields that you won't use from this class, it's the same as leaving them null. 26 | /// 27 | #pragma warning disable 0169, 0414, 0649 28 | internal sealed class ConfigurationManagerAttributes 29 | { 30 | /// 31 | /// Should the setting be shown as a percentage (only use with value range settings). 32 | /// 33 | public bool? ShowRangeAsPercent; 34 | 35 | /// 36 | /// Custom setting editor (OnGUI code that replaces the default editor provided by ConfigurationManager). 37 | /// See below for a deeper explanation. Using a custom drawer will cause many of the other fields to do nothing. 38 | /// 39 | public System.Action CustomDrawer; 40 | 41 | /// 42 | /// Custom setting editor that allows polling keyboard input with the Input (or UnityInput) class. 43 | /// Use either CustomDrawer or CustomHotkeyDrawer, using both at the same time leads to undefined behaviour. 44 | /// 45 | public CustomHotkeyDrawerFunc CustomHotkeyDrawer; 46 | 47 | /// 48 | /// Custom setting draw action that allows polling keyboard input with the Input class. 49 | /// Note: Make sure to focus on your UI control when you are accepting input so user doesn't type in the search box or in another setting (best to do this on every frame). 50 | /// If you don't draw any selectable UI controls You can use `GUIUtility.keyboardControl = -1;` on every frame to make sure that nothing is selected. 51 | /// 52 | /// 53 | /// CustomHotkeyDrawer = (ConfigEntryBase setting, ref bool isEditing) => 54 | /// { 55 | /// if (isEditing) 56 | /// { 57 | /// // Make sure nothing else is selected since we aren't focusing on a text box with GUI.FocusControl. 58 | /// GUIUtility.keyboardControl = -1; 59 | /// 60 | /// // Use Input.GetKeyDown and others here, remember to set isEditing to false after you're done! 61 | /// // It's best to check Input.anyKeyDown and set isEditing to false immediately if it's true, 62 | /// // so that the input doesn't have a chance to propagate to the game itself. 63 | /// 64 | /// if (GUILayout.Button("Stop")) 65 | /// isEditing = false; 66 | /// } 67 | /// else 68 | /// { 69 | /// if (GUILayout.Button("Start")) 70 | /// isEditing = true; 71 | /// } 72 | /// 73 | /// // This will only be true when isEditing is true and you hold any key 74 | /// GUILayout.Label("Any key pressed: " + Input.anyKey); 75 | /// } 76 | /// 77 | /// 78 | /// Setting currently being set (if available). 79 | /// 80 | /// 81 | /// Set this ref parameter to true when you want the current setting drawer to receive Input events. 82 | /// The value will persist after being set, use it to see if the current instance is being edited. 83 | /// Remember to set it to false after you are done! 84 | /// 85 | public delegate void CustomHotkeyDrawerFunc(BepInEx.Configuration.ConfigEntryBase setting, ref bool isCurrentlyAcceptingInput); 86 | 87 | /// 88 | /// Show this setting in the settings screen at all? If false, don't show. 89 | /// 90 | public bool? Browsable; 91 | 92 | /// 93 | /// Category the setting is under. Null to be directly under the plugin. 94 | /// 95 | public string Category; 96 | 97 | /// 98 | /// If set, a "Normal" button will be shown next to the setting to allow resetting to default. 99 | /// 100 | public object DefaultValue; 101 | 102 | /// 103 | /// Force the "Reset" button to not be displayed, even if a valid DefaultValue is available. 104 | /// 105 | public bool? HideDefaultButton; 106 | 107 | /// 108 | /// Force the setting name to not be displayed. Should only be used with a to get more space. 109 | /// Can be used together with to gain even more space. 110 | /// 111 | public bool? HideSettingName; 112 | 113 | /// 114 | /// Optional description shown when hovering over the setting. 115 | /// Not recommended, provide the description when creating the setting instead. 116 | /// 117 | public string Description; 118 | 119 | /// 120 | /// Name of the setting. 121 | /// 122 | public string DispName; 123 | 124 | /// 125 | /// Order of the setting on the settings list relative to other settings in a category. 126 | /// 0 by default, higher number is higher on the list. 127 | /// 128 | public int? Order; 129 | 130 | /// 131 | /// Only show the value, don't allow editing it. 132 | /// 133 | public bool? ReadOnly; 134 | 135 | /// 136 | /// If true, don't show the setting by default. User has to turn on showing advanced settings or search for it. 137 | /// 138 | public bool? IsAdvanced; 139 | 140 | /// 141 | /// Custom converter from setting type to string for the built-in editor textboxes. 142 | /// 143 | public System.Func ObjToStr; 144 | 145 | /// 146 | /// Custom converter from string to setting type for the built-in editor textboxes. 147 | /// 148 | public System.Func StrToObj; 149 | } 150 | -------------------------------------------------------------------------------- /Donuts.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net471 5 | dvize.Donuts 6 | dvize.Donuts 7 | true 8 | 9 | 10 | 11 | true 12 | portable 13 | true 14 | bin\Debug\ 15 | DEBUG;TRACE 16 | 8.0 17 | 18 | 19 | 20 | true 21 | portable 22 | true 23 | bin\Release\ 24 | TRACE 25 | 8.0 26 | 27 | 28 | 29 | 30 | F:\SPT-AKI-DEV\BepInEx\core\0Harmony.dll 31 | 32 | 33 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\Assembly-CSharp.dll 34 | 35 | 36 | F:\SPT-AKI-DEV\BepInEx\core\BepInEx.dll 37 | 38 | 39 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\Comfort.dll 40 | 41 | 42 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\DissonanceVoip.dll 43 | 44 | 45 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\DOTween.dll 46 | 47 | 48 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\DOTween.Modules.dll 49 | 50 | 51 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\Newtonsoft.Json.dll 52 | 53 | 54 | F:\SPT-AKI-DEV\BepInEx\plugins\spt\spt-common.dll 55 | 56 | 57 | F:\SPT-AKI-DEV\BepInEx\plugins\spt\spt-core.dll 58 | 59 | 60 | F:\SPT-AKI-DEV\BepInEx\patchers\spt-prepatch.dll 61 | 62 | 63 | F:\SPT-AKI-DEV\BepInEx\plugins\spt\spt-reflection.dll 64 | 65 | 66 | F:\SPT-AKI-DEV\BepInEx\plugins\spt\spt-singleplayer.dll 67 | 68 | 69 | F:\SPT-AKI-DEV\BepInEx\plugins\UnityToolkit\UniTask.dll 70 | 71 | 72 | F:\SPT-AKI-DEV\BepInEx\plugins\UnityToolkit\UniTask.DOTween.dll 73 | 74 | 75 | F:\SPT-AKI-DEV\BepInEx\plugins\UnityToolkit\UniTask.Linq.dll 76 | 77 | 78 | F:\SPT-AKI-DEV\BepInEx\plugins\UnityToolkit\UniTask.TextMeshPro.dll 79 | 80 | 81 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\Unity.Burst.dll 82 | 83 | 84 | F:\SPT-AKI-DEV\BepInEx\plugins\UnityToolkit\Unity.Collections.dll 85 | 86 | 87 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\Unity.Mathematics.dll 88 | 89 | 90 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.dll 91 | 92 | 93 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.AIModule.dll 94 | 95 | 96 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.CoreModule.dll 97 | 98 | 99 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.ImageConversionModule.dll 100 | 101 | 102 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.IMGUIModule.dll 103 | 104 | 105 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.InputLegacyModule.dll 106 | 107 | 108 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.PhysicsModule.dll 109 | 110 | 111 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.TextCoreModule.dll 112 | 113 | 114 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.TextRenderingModule.dll 115 | 116 | 117 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.UI.dll 118 | 119 | 120 | F:\SPT-AKI-DEV\EscapeFromTarkov_Data\Managed\UnityEngine.UIModule.dll 121 | 122 | 123 | F:\SPT-AKI-DEV\BepInEx\plugins\UnityToolkit\UnityToolkit.dll 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /Bots/Initialization.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using SPT.PrePatch; 7 | using EFT; 8 | using EFT.Communications; 9 | using Newtonsoft.Json; 10 | using UnityEngine; 11 | using Donuts.Models; 12 | using Cysharp.Threading.Tasks; 13 | using static Donuts.DonutComponent; 14 | using static Donuts.DefaultPluginVars; 15 | using static Donuts.Gizmos; 16 | using System.Threading; 17 | 18 | #pragma warning disable IDE0007, IDE0044 19 | 20 | namespace Donuts 21 | { 22 | internal class Initialization 23 | { 24 | 25 | internal static int PMCBotLimit; 26 | internal static int SCAVBotLimit; 27 | 28 | internal static void InitializeStaticVariables() 29 | { 30 | fightLocations = new FightLocations() 31 | { 32 | Locations = new List() 33 | }; 34 | 35 | sessionLocations = new FightLocations() 36 | { 37 | Locations = new List() 38 | }; 39 | 40 | fileLoaded = false; 41 | groupedHotspotTimers = new Dictionary>(); 42 | groupedFightLocations = new List>(); 43 | hotspotTimers = new List(); 44 | currentInitialPMCs = 0; 45 | currentInitialSCAVs = 0; 46 | } 47 | 48 | internal static void SetupBotLimit(string folderName) 49 | { 50 | Folder raidFolderSelected = DonutsPlugin.GrabDonutsFolder(folderName); 51 | switch (DonutsBotPrep.maplocation) 52 | { 53 | case "factory4_day": 54 | case "factory4_night": 55 | PMCBotLimit = raidFolderSelected.PMCBotLimitPresets.FactoryBotLimit; 56 | SCAVBotLimit = raidFolderSelected.SCAVBotLimitPresets.FactoryBotLimit; 57 | break; 58 | case "bigmap": 59 | PMCBotLimit = raidFolderSelected.PMCBotLimitPresets.CustomsBotLimit; 60 | SCAVBotLimit = raidFolderSelected.SCAVBotLimitPresets.CustomsBotLimit; 61 | break; 62 | case "interchange": 63 | PMCBotLimit = raidFolderSelected.PMCBotLimitPresets.InterchangeBotLimit; 64 | SCAVBotLimit = raidFolderSelected.SCAVBotLimitPresets.InterchangeBotLimit; 65 | break; 66 | case "rezervbase": 67 | PMCBotLimit = raidFolderSelected.PMCBotLimitPresets.ReserveBotLimit; 68 | SCAVBotLimit = raidFolderSelected.SCAVBotLimitPresets.ReserveBotLimit; 69 | break; 70 | case "laboratory": 71 | PMCBotLimit = raidFolderSelected.PMCBotLimitPresets.LaboratoryBotLimit; 72 | SCAVBotLimit = raidFolderSelected.SCAVBotLimitPresets.LaboratoryBotLimit; 73 | break; 74 | case "lighthouse": 75 | PMCBotLimit = raidFolderSelected.PMCBotLimitPresets.LighthouseBotLimit; 76 | SCAVBotLimit = raidFolderSelected.SCAVBotLimitPresets.LighthouseBotLimit; 77 | break; 78 | case "shoreline": 79 | PMCBotLimit = raidFolderSelected.PMCBotLimitPresets.ShorelineBotLimit; 80 | SCAVBotLimit = raidFolderSelected.SCAVBotLimitPresets.ShorelineBotLimit; 81 | break; 82 | case "woods": 83 | PMCBotLimit = raidFolderSelected.PMCBotLimitPresets.WoodsBotLimit; 84 | SCAVBotLimit = raidFolderSelected.SCAVBotLimitPresets.WoodsBotLimit; 85 | break; 86 | case "tarkovstreets": 87 | PMCBotLimit = raidFolderSelected.PMCBotLimitPresets.TarkovStreetsBotLimit; 88 | SCAVBotLimit = raidFolderSelected.SCAVBotLimitPresets.TarkovStreetsBotLimit; 89 | break; 90 | case "sandbox": 91 | case "sandbox_high": 92 | PMCBotLimit = raidFolderSelected.PMCBotLimitPresets.GroundZeroBotLimit; 93 | SCAVBotLimit = raidFolderSelected.SCAVBotLimitPresets.GroundZeroBotLimit; 94 | break; 95 | default: 96 | PMCBotLimit = 8; 97 | SCAVBotLimit = 5; 98 | break; 99 | } 100 | } 101 | 102 | internal static async UniTask LoadFightLocations(CancellationToken cancellationToken) 103 | { 104 | // Check if the operation has been cancelled 105 | cancellationToken.ThrowIfCancellationRequested(); 106 | 107 | if (!fileLoaded) 108 | { 109 | methodCache.TryGetValue("DisplayMessageNotification", out MethodInfo displayMessageNotificationMethod); 110 | 111 | string dllPath = Assembly.GetExecutingAssembly().Location; 112 | string directoryPath = Path.GetDirectoryName(dllPath); 113 | string jsonFolderPath = Path.Combine(directoryPath, "patterns"); 114 | 115 | if (DonutsBotPrep.selectionName == null) 116 | { 117 | var txt = "Donuts Plugin: No valid Scenario Selection found for map"; 118 | DonutComponent.Logger.LogError(txt); 119 | EFT.UI.ConsoleScreen.LogError(txt); 120 | displayMessageNotificationMethod?.Invoke(null, new object[] { txt, ENotificationDurationType.Long, ENotificationIconType.Alert, Color.yellow }); 121 | return; 122 | } 123 | 124 | string patternFolderPath = Path.Combine(jsonFolderPath, DonutsBotPrep.selectionName); 125 | 126 | if (!Directory.Exists(patternFolderPath)) 127 | { 128 | var txt = $"Donuts Plugin: Folder from ScenarioConfig.json does not actually exist: {patternFolderPath}\nDisabling the donuts plugin for this raid."; 129 | DonutComponent.Logger.LogError(txt); 130 | EFT.UI.ConsoleScreen.LogError(txt); 131 | displayMessageNotificationMethod?.Invoke(null, new object[] { txt, ENotificationDurationType.Long, ENotificationIconType.Alert, Color.yellow }); 132 | fileLoaded = false; 133 | return; 134 | } 135 | 136 | string[] jsonFiles = Directory.GetFiles(patternFolderPath, "*.json"); 137 | 138 | if (jsonFiles.Length == 0) 139 | { 140 | var txt = $"Donuts Plugin: No JSON Pattern files found in folder: {patternFolderPath}\nDisabling the donuts plugin for this raid."; 141 | DonutComponent.Logger.LogError(txt); 142 | EFT.UI.ConsoleScreen.LogError(txt); 143 | displayMessageNotificationMethod?.Invoke(null, new object[] { txt, ENotificationDurationType.Long, ENotificationIconType.Alert, Color.yellow }); 144 | fileLoaded = false; 145 | return; 146 | } 147 | 148 | fileLoaded = true; 149 | 150 | // Display selected preset 151 | DonutsPlugin.LogSelectedPreset(DonutsBotPrep.selectionName); 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /Tools/Gizmos.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Text; 7 | using EFT; 8 | using EFT.Communications; 9 | using UnityEngine; 10 | using Donuts.Models; 11 | using static Donuts.DonutComponent; 12 | 13 | #pragma warning disable IDE0007, IDE0044 14 | 15 | namespace Donuts 16 | { 17 | internal class Gizmos 18 | { 19 | internal bool isGizmoEnabled = false; 20 | internal static ConcurrentDictionary gizmoMarkers = new ConcurrentDictionary(); 21 | internal static Coroutine gizmoUpdateCoroutine; 22 | internal static MonoBehaviour monoBehaviourRef; 23 | internal static StringBuilder DisplayedMarkerInfo = new StringBuilder(); 24 | internal static StringBuilder PreviousMarkerInfo = new StringBuilder(); 25 | internal static Coroutine resetMarkerInfoCoroutine; 26 | 27 | internal Gizmos(MonoBehaviour monoBehaviour) 28 | { 29 | monoBehaviourRef = monoBehaviour; 30 | } 31 | 32 | private IEnumerator UpdateGizmoSpheresCoroutine() 33 | { 34 | while (isGizmoEnabled) 35 | { 36 | RefreshGizmoDisplay(); 37 | yield return new WaitForSeconds(3f); 38 | } 39 | } 40 | 41 | internal static void DrawMarkers(IEnumerable locations, Color color, PrimitiveType primitiveType) 42 | { 43 | foreach (var hotspot in locations) 44 | { 45 | var newCoordinate = new Vector3(hotspot.Position.x, hotspot.Position.y, hotspot.Position.z); 46 | 47 | if (DonutsBotPrep.maplocation == hotspot.MapName && !gizmoMarkers.ContainsKey(newCoordinate)) 48 | { 49 | var marker = CreateMarker(newCoordinate, color, primitiveType, hotspot.MaxDistance); 50 | gizmoMarkers[newCoordinate] = marker; 51 | } 52 | } 53 | } 54 | 55 | private static GameObject CreateMarker(Vector3 position, Color color, PrimitiveType primitiveType, float size) 56 | { 57 | var marker = GameObject.CreatePrimitive(primitiveType); 58 | var material = marker.GetComponent().material; 59 | material.color = color; 60 | marker.GetComponent().enabled = false; 61 | marker.transform.position = position; 62 | marker.transform.localScale = DefaultPluginVars.gizmoRealSize.Value ? new Vector3(size, 3f, size) : Vector3.one; 63 | return marker; 64 | } 65 | 66 | public void ToggleGizmoDisplay(bool enableGizmos) 67 | { 68 | isGizmoEnabled = enableGizmos; 69 | 70 | if (isGizmoEnabled && gizmoUpdateCoroutine == null) 71 | { 72 | RefreshGizmoDisplay(); 73 | gizmoUpdateCoroutine = monoBehaviourRef.StartCoroutine(UpdateGizmoSpheresCoroutine()); 74 | } 75 | else if (!isGizmoEnabled && gizmoUpdateCoroutine != null) 76 | { 77 | monoBehaviourRef.StopCoroutine(gizmoUpdateCoroutine); 78 | gizmoUpdateCoroutine = null; 79 | ClearGizmoMarkers(); 80 | } 81 | } 82 | 83 | internal static void RefreshGizmoDisplay() 84 | { 85 | ClearGizmoMarkers(); 86 | 87 | if (DefaultPluginVars.DebugGizmos.Value) 88 | { 89 | DrawMarkers(fightLocations?.Locations ?? Enumerable.Empty(), Color.green, PrimitiveType.Sphere); 90 | DrawMarkers(sessionLocations?.Locations ?? Enumerable.Empty(), Color.red, PrimitiveType.Cube); 91 | } 92 | } 93 | 94 | internal static void ClearGizmoMarkers() 95 | { 96 | foreach (var marker in gizmoMarkers.Values) 97 | { 98 | GameWorld.Destroy(marker); 99 | } 100 | gizmoMarkers.Clear(); 101 | } 102 | 103 | internal static void DisplayMarkerInformation() 104 | { 105 | if (gizmoMarkers.Count == 0) return; 106 | 107 | var closestShape = gizmoMarkers.Values 108 | .OrderBy(shape => (shape.transform.position - gameWorld.MainPlayer.Transform.position).sqrMagnitude) 109 | .FirstOrDefault(); 110 | 111 | if (closestShape == null || !IsShapeVisible(closestShape.transform.position)) return; 112 | 113 | UpdateDisplayedMarkerInfo(closestShape.transform.position); 114 | } 115 | 116 | private static bool IsShapeVisible(Vector3 shapePosition) 117 | { 118 | var direction = shapePosition - gameWorld.MainPlayer.Transform.position; 119 | return direction.sqrMagnitude <= 10f * 10f && Vector3.Angle(gameWorld.MainPlayer.Transform.forward, direction) < 20f; 120 | } 121 | 122 | private static void UpdateDisplayedMarkerInfo(Vector3 closestShapePosition) 123 | { 124 | var closestEntry = GetClosestEntry(closestShapePosition); 125 | if (closestEntry == null) return; 126 | 127 | PreviousMarkerInfo.Clear().Append(DisplayedMarkerInfo.ToString()); 128 | 129 | DisplayedMarkerInfo.Clear() 130 | .AppendLine("Donuts: Marker Info") 131 | .AppendLine($"GroupNum: {closestEntry.GroupNum}") 132 | .AppendLine($"Name: {closestEntry.Name}") 133 | .AppendLine($"SpawnType: {closestEntry.WildSpawnType}") 134 | .AppendLine($"Position: {closestEntry.Position.x}, {closestEntry.Position.y}, {closestEntry.Position.z}") 135 | .AppendLine($"Bot Timer Trigger: {closestEntry.BotTimerTrigger}") 136 | .AppendLine($"Spawn Chance: {closestEntry.SpawnChance}") 137 | .AppendLine($"Max Random Number of Bots: {closestEntry.MaxRandomNumBots}") 138 | .AppendLine($"Max Spawns Before Cooldown: {closestEntry.MaxSpawnsBeforeCoolDown}") 139 | .AppendLine($"Ignore Timer for First Spawn: {closestEntry.IgnoreTimerFirstSpawn}") 140 | .AppendLine($"Min Spawn Distance From Player: {closestEntry.MinSpawnDistanceFromPlayer}"); 141 | 142 | if (DisplayedMarkerInfo.ToString() != PreviousMarkerInfo.ToString()) 143 | { 144 | ShowMarkerNotification(DisplayedMarkerInfo.ToString()); 145 | StartResetMarkerInfoCoroutine(); 146 | } 147 | } 148 | 149 | private static void ShowMarkerNotification(string message) 150 | { 151 | if (methodCache.TryGetValue("DisplayMessageNotification", out var displayMessageNotificationMethod)) 152 | { 153 | displayMessageNotificationMethod.Invoke(null, new object[] { message, ENotificationDurationType.Long, ENotificationIconType.Default, Color.yellow }); 154 | } 155 | } 156 | 157 | private static void StartResetMarkerInfoCoroutine() 158 | { 159 | if (resetMarkerInfoCoroutine != null) 160 | { 161 | monoBehaviourRef.StopCoroutine(resetMarkerInfoCoroutine); 162 | } 163 | 164 | resetMarkerInfoCoroutine = monoBehaviourRef.StartCoroutine(ResetMarkerInfoAfterDelay()); 165 | } 166 | 167 | internal static IEnumerator ResetMarkerInfoAfterDelay() 168 | { 169 | yield return new WaitForSeconds(5f); 170 | DisplayedMarkerInfo.Clear(); 171 | resetMarkerInfoCoroutine = null; 172 | } 173 | 174 | internal static Entry GetClosestEntry(Vector3 position) 175 | { 176 | Entry closestEntry = null; 177 | float closestDistanceSq = float.MaxValue; 178 | 179 | foreach (var entry in fightLocations.Locations.Concat(sessionLocations.Locations)) 180 | { 181 | var entryPosition = new Vector3(entry.Position.x, entry.Position.y, entry.Position.z); 182 | float distanceSq = (entryPosition - position).sqrMagnitude; 183 | if (distanceSq < closestDistanceSq) 184 | { 185 | closestDistanceSq = distanceSq; 186 | closestEntry = entry; 187 | } 188 | } 189 | 190 | return closestEntry; 191 | } 192 | 193 | public static MethodInfo GetDisplayMessageNotificationMethod() => methodCache["DisplayMessageNotification"]; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /Bots/SpawnChecks.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Comfort.Common; 4 | using EFT; 5 | using UnityEngine; 6 | using UnityEngine.AI; 7 | using Donuts.Models; 8 | using static Donuts.DonutComponent; 9 | using System.Linq; 10 | using Cysharp.Threading.Tasks; 11 | using System.Threading; 12 | 13 | #pragma warning disable IDE0007, IDE0044 14 | 15 | namespace Donuts 16 | { 17 | internal class SpawnChecks 18 | { 19 | #region spawnchecks 20 | 21 | internal static async Task GetValidSpawnPosition(float hotspotMinDistFromPlayer, float hotspotMaxDist, float hotspotMinDist, Vector3 coordinate, int maxSpawnAttempts, CancellationToken cancellationToken) 22 | { 23 | for (int i = 0; i < maxSpawnAttempts; i++) 24 | { 25 | Vector3 spawnPosition = coordinate; 26 | 27 | if (NavMesh.SamplePosition(spawnPosition, out var navHit, 2f, NavMesh.AllAreas)) 28 | { 29 | spawnPosition = navHit.position; 30 | 31 | if (await IsValidSpawnPosition(spawnPosition, hotspotMinDistFromPlayer, cancellationToken)) 32 | { 33 | #if DEBUG 34 | DonutComponent.Logger.LogDebug("Found spawn position at: " + spawnPosition); 35 | #endif 36 | return spawnPosition; 37 | } 38 | } 39 | 40 | await Task.Delay(1, cancellationToken); 41 | } 42 | 43 | return null; 44 | } 45 | 46 | internal static async Task IsValidSpawnPosition(Vector3 spawnPosition, float hotspotMinDistFromPlayer, CancellationToken cancellationToken) 47 | { 48 | if (spawnPosition == null) 49 | { 50 | DonutComponent.Logger.LogDebug("Spawn position or hotspot is null."); 51 | return false; 52 | } 53 | 54 | var tasks = new List> 55 | { 56 | IsSpawnPositionInsideWall(spawnPosition, cancellationToken), 57 | IsSpawnPositionInPlayerLineOfSight(spawnPosition, cancellationToken), 58 | IsSpawnInAir(spawnPosition, cancellationToken) 59 | }; 60 | 61 | var tasks2 = new List> 62 | { 63 | }; 64 | 65 | if (DefaultPluginVars.globalMinSpawnDistanceFromPlayerBool.Value) 66 | { 67 | tasks2.Add(IsMinSpawnDistanceFromPlayerTooShort(spawnPosition, cancellationToken)); 68 | } 69 | 70 | if (DefaultPluginVars.globalMinSpawnDistanceFromOtherBotsBool.Value) 71 | { 72 | tasks2.Add(IsPositionTooCloseToOtherBots(spawnPosition, cancellationToken)); 73 | } 74 | 75 | bool[] results = await UniTask.WhenAll(tasks); 76 | 77 | //add to results if tasks2 not empty 78 | if (tasks2.Count > 0) 79 | { 80 | bool[] results2 = await Task.WhenAll(tasks2); 81 | results = results.Concat(results2).ToArray(); 82 | } 83 | 84 | if (results.Any(result => result)) 85 | { 86 | DonutComponent.Logger.LogDebug("Spawn position failed one or more checks."); 87 | return false; 88 | } 89 | 90 | return true; 91 | } 92 | 93 | internal static async UniTask IsSpawnPositionInPlayerLineOfSight(Vector3 spawnPosition, CancellationToken cancellationToken) 94 | { 95 | foreach (var player in playerList) 96 | { 97 | if (player == null || player.HealthController == null || !player.HealthController.IsAlive) 98 | { 99 | continue; 100 | } 101 | Vector3 playerPosition = player.MainParts[BodyPartType.head].Position; 102 | Vector3 direction = (playerPosition - spawnPosition).normalized; 103 | float distance = Vector3.Distance(spawnPosition, playerPosition); 104 | if (!Physics.Raycast(spawnPosition, direction, distance, LayerMaskClass.HighPolyWithTerrainMask)) 105 | { 106 | return true; 107 | } 108 | } 109 | return false; 110 | } 111 | 112 | internal static async UniTask IsSpawnPositionInsideWall(Vector3 position, CancellationToken cancellationToken) 113 | { 114 | Vector3 boxSize = new Vector3(1f, 1f, 1f); 115 | Collider[] colliders = Physics.OverlapBox(position, boxSize, Quaternion.identity, LayerMaskClass.LowPolyColliderLayer); 116 | 117 | foreach (var collider in colliders) 118 | { 119 | Transform currentTransform = collider.transform; 120 | while (currentTransform != null) 121 | { 122 | if (currentTransform.gameObject.name.ToUpper().Contains("WALLS")) 123 | { 124 | return true; 125 | } 126 | currentTransform = currentTransform.parent; 127 | } 128 | } 129 | 130 | return false; 131 | } 132 | 133 | internal static async UniTask IsSpawnInAir(Vector3 position, CancellationToken cancellationToken) 134 | { 135 | Ray ray = new Ray(position, Vector3.down); 136 | float distance = 10f; 137 | 138 | return !Physics.Raycast(ray, distance, LayerMaskClass.HighPolyWithTerrainMask); 139 | } 140 | 141 | internal static float GetMinDistanceFromPlayer() 142 | { 143 | if (DefaultPluginVars.globalMinSpawnDistanceFromPlayerBool.Value) 144 | { 145 | switch (DonutsBotPrep.maplocation) 146 | { 147 | case "bigmap": return DefaultPluginVars.globalMinSpawnDistanceFromPlayerCustoms.Value; 148 | case "factory4_day": return DefaultPluginVars.globalMinSpawnDistanceFromPlayerFactory.Value; 149 | case "factory4_night": return DefaultPluginVars.globalMinSpawnDistanceFromPlayerFactory.Value; 150 | case "tarkovstreets": return DefaultPluginVars.globalMinSpawnDistanceFromPlayerStreets.Value; 151 | case "sandbox": return DefaultPluginVars.globalMinSpawnDistanceFromPlayerGroundZero.Value; 152 | case "sandbox_high": return DefaultPluginVars.globalMinSpawnDistanceFromPlayerGroundZero.Value; 153 | case "rezervbase": return DefaultPluginVars.globalMinSpawnDistanceFromPlayerReserve.Value; 154 | case "lighthouse": return DefaultPluginVars.globalMinSpawnDistanceFromPlayerLighthouse.Value; 155 | case "shoreline": return DefaultPluginVars.globalMinSpawnDistanceFromPlayerShoreline.Value; 156 | case "woods": return DefaultPluginVars.globalMinSpawnDistanceFromPlayerWoods.Value; 157 | case "laboratory": return DefaultPluginVars.globalMinSpawnDistanceFromPlayerLaboratory.Value; 158 | case "interchange": return DefaultPluginVars.globalMinSpawnDistanceFromPlayerInterchange.Value; 159 | default: return 50f; 160 | } 161 | } 162 | else 163 | { 164 | return 0f; 165 | } 166 | } 167 | 168 | internal static float GetMinDistanceFromOtherBots() 169 | { 170 | switch (DonutsBotPrep.maplocation) 171 | { 172 | case "bigmap": return DefaultPluginVars.globalMinSpawnDistanceFromOtherBotsCustoms.Value; 173 | case "factory4_day": return DefaultPluginVars.globalMinSpawnDistanceFromOtherBotsFactory.Value; 174 | case "factory4_night": return DefaultPluginVars.globalMinSpawnDistanceFromOtherBotsFactory.Value; 175 | case "tarkovstreets": return DefaultPluginVars.globalMinSpawnDistanceFromOtherBotsStreets.Value; 176 | case "sandbox": return DefaultPluginVars.globalMinSpawnDistanceFromOtherBotsGroundZero.Value; 177 | case "sandbox_high": return DefaultPluginVars.globalMinSpawnDistanceFromPlayerGroundZero.Value; 178 | case "rezervbase": return DefaultPluginVars.globalMinSpawnDistanceFromOtherBotsReserve.Value; 179 | case "lighthouse": return DefaultPluginVars.globalMinSpawnDistanceFromOtherBotsLighthouse.Value; 180 | case "shoreline": return DefaultPluginVars.globalMinSpawnDistanceFromOtherBotsShoreline.Value; 181 | case "woods": return DefaultPluginVars.globalMinSpawnDistanceFromOtherBotsWoods.Value; 182 | case "laboratory": return DefaultPluginVars.globalMinSpawnDistanceFromOtherBotsLaboratory.Value; 183 | case "interchange": return DefaultPluginVars.globalMinSpawnDistanceFromOtherBotsInterchange.Value; 184 | default: return 0f; 185 | } 186 | } 187 | 188 | internal static async Task IsMinSpawnDistanceFromPlayerTooShort(Vector3 position, CancellationToken cancellationToken) 189 | { 190 | float minDistanceFromPlayer = GetMinDistanceFromPlayer(); 191 | 192 | var tasks = playerList 193 | .Where(player => player != null && player.HealthController != null && player.HealthController.IsAlive) 194 | .Select(player => Task.Run(() => 195 | { 196 | if ((player.Position - position).sqrMagnitude < (minDistanceFromPlayer * minDistanceFromPlayer)) 197 | { 198 | return true; 199 | } 200 | return false; 201 | }, cancellationToken)) 202 | .ToList(); 203 | 204 | bool[] results = await Task.WhenAll(tasks); 205 | return results.Any(result => result); 206 | } 207 | 208 | internal static async Task IsPositionTooCloseToOtherBots(Vector3 position, CancellationToken cancellationToken) 209 | { 210 | float minDistanceFromOtherBots = GetMinDistanceFromOtherBots(); 211 | List players = Singleton.Instance.AllAlivePlayersList; 212 | 213 | var tasks = players 214 | .Where(player => player != null && player.HealthController.IsAlive && !player.IsYourPlayer) 215 | .Select(player => Task.Run(() => 216 | { 217 | if ((player.Position - position).sqrMagnitude < (minDistanceFromOtherBots * minDistanceFromOtherBots)) 218 | { 219 | return true; 220 | } 221 | return false; 222 | }, cancellationToken)) 223 | .ToList(); 224 | 225 | bool[] results = await Task.WhenAll(tasks); 226 | return results.Any(result => result); 227 | } 228 | 229 | #endregion 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /Tools/EditorFunctions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Reflection; 5 | using BepInEx.Logging; 6 | using EFT; 7 | using EFT.Communications; 8 | using Newtonsoft.Json; 9 | using UnityEngine; 10 | using Donuts.Models; 11 | using Cysharp.Threading.Tasks; 12 | using static Donuts.DefaultPluginVars; 13 | 14 | #pragma warning disable IDE0007, IDE0044 15 | 16 | namespace Donuts 17 | { 18 | internal class EditorFunctions 19 | { 20 | internal static ManualLogSource Logger 21 | { 22 | get; private set; 23 | } 24 | 25 | public EditorFunctions() 26 | { 27 | Logger ??= BepInEx.Logging.Logger.CreateLogSource(nameof(EditorFunctions)); 28 | } 29 | 30 | internal static void DeleteSpawnMarker() 31 | { 32 | // Check if any of the required objects are null 33 | if (Donuts.DonutComponent.gameWorld == null) 34 | { 35 | Logger.LogDebug("IBotGame Not Instantiated or gameWorld is null."); 36 | return; 37 | } 38 | 39 | //need to be able to see it to delete it 40 | if (DebugGizmos.Value) 41 | { 42 | //temporarily combine fightLocations and sessionLocations so i can find the closest entry 43 | var combinedLocations = Donuts.DonutComponent.fightLocations.Locations.Concat(Donuts.DonutComponent.sessionLocations.Locations).ToList(); 44 | 45 | // if for some reason its empty already return 46 | if (combinedLocations.Count == 0) 47 | { 48 | return; 49 | } 50 | 51 | // Get the closest spawn marker to the player 52 | var closestEntry = combinedLocations.OrderBy(x => Vector3.Distance(Donuts.DonutComponent.gameWorld.MainPlayer.Position, new Vector3(x.Position.x, x.Position.y, x.Position.z))).FirstOrDefault(); 53 | 54 | // Check if the closest entry is null 55 | if (closestEntry == null) 56 | { 57 | var displayMessageNotificationMethod = Gizmos.GetDisplayMessageNotificationMethod(); 58 | if (displayMessageNotificationMethod != null) 59 | { 60 | var txt = $"Donuts: The Spawn Marker could not be deleted because closest entry could not be found"; 61 | displayMessageNotificationMethod.Invoke(null, new object[] { txt, ENotificationDurationType.Long, ENotificationIconType.Default, Color.grey }); 62 | } 63 | return; 64 | } 65 | 66 | // Remove the entry from the list if the distance from the player is less than 5m 67 | if (Vector3.Distance(Donuts.DonutComponent.gameWorld.MainPlayer.Position, new Vector3(closestEntry.Position.x, closestEntry.Position.y, closestEntry.Position.z)) < 5f) 68 | { 69 | // check which list the entry is in and remove it from that list 70 | if (Donuts.DonutComponent.fightLocations.Locations.Count > 0 && 71 | Donuts.DonutComponent.fightLocations.Locations.Contains(closestEntry)) 72 | { 73 | Donuts.DonutComponent.fightLocations.Locations.Remove(closestEntry); 74 | } 75 | else if (Donuts.DonutComponent.sessionLocations.Locations.Count > 0 && 76 | Donuts.DonutComponent.sessionLocations.Locations.Contains(closestEntry)) 77 | { 78 | Donuts.DonutComponent.sessionLocations.Locations.Remove(closestEntry); 79 | } 80 | 81 | // Remove the timer if it exists from the list of hotspotTimer in DonutComponent.groupedHotspotTimers[closestEntry.GroupNum] 82 | if (Donuts.DonutComponent.groupedHotspotTimers.ContainsKey(closestEntry.GroupNum)) 83 | { 84 | var timerList = Donuts.DonutComponent.groupedHotspotTimers[closestEntry.GroupNum]; 85 | var timer = timerList.FirstOrDefault(x => x.Hotspot == closestEntry); 86 | 87 | if (timer != null) 88 | { 89 | timerList.Remove(timer); 90 | } 91 | else 92 | { 93 | // Handle the case where no timer was found 94 | Logger.LogDebug("Donuts: No matching timer found to delete."); 95 | } 96 | } 97 | else 98 | { 99 | Logger.LogDebug("Donuts: GroupNum does not exist in groupedHotspotTimers."); 100 | } 101 | 102 | // Display a message to the player 103 | var displayMessageNotificationMethod = Gizmos.GetDisplayMessageNotificationMethod(); 104 | if (displayMessageNotificationMethod != null) 105 | { 106 | var txt = $"Donuts: Spawn Marker Deleted for \n {closestEntry.Name}\n SpawnType: {closestEntry.WildSpawnType}\n Position: {closestEntry.Position.x}, {closestEntry.Position.y}, {closestEntry.Position.z}"; 107 | displayMessageNotificationMethod.Invoke(null, new object[] { txt, ENotificationDurationType.Long, ENotificationIconType.Default, Color.yellow }); 108 | } 109 | 110 | // Edit the DonutComponent.drawnCoordinates and gizmoSpheres list to remove the objects 111 | var coordinate = new Vector3(closestEntry.Position.x, closestEntry.Position.y, closestEntry.Position.z); 112 | if (Gizmos.gizmoMarkers.TryRemove(coordinate, out var sphere)) 113 | { 114 | GameWorld.Destroy(sphere); 115 | } 116 | } 117 | } 118 | } 119 | 120 | internal static void CreateSpawnMarker() 121 | { 122 | // Check if any of the required objects are null 123 | if (DonutComponent.gameWorld == null) 124 | { 125 | Logger.LogDebug("IBotGame Not Instantiated or gameWorld is null."); 126 | return; 127 | } 128 | 129 | // Create new Donuts.Entry 130 | Entry newEntry = new Entry 131 | { 132 | Name = spawnName.Value, 133 | GroupNum = groupNum.Value, 134 | MapName = DonutsBotPrep.maplocation, 135 | WildSpawnType = wildSpawns.Value, 136 | MinDistance = minSpawnDist.Value, 137 | MaxDistance = maxSpawnDist.Value, 138 | MaxRandomNumBots = maxRandNumBots.Value, 139 | SpawnChance = spawnChance.Value, 140 | BotTimerTrigger = botTimerTrigger.Value, 141 | BotTriggerDistance = botTriggerDistance.Value, 142 | Position = new Position 143 | { 144 | x = DonutComponent.gameWorld.MainPlayer.Position.x, 145 | y = DonutComponent.gameWorld.MainPlayer.Position.y, 146 | z = DonutComponent.gameWorld.MainPlayer.Position.z 147 | }, 148 | 149 | MaxSpawnsBeforeCoolDown = maxSpawnsBeforeCooldown.Value, 150 | IgnoreTimerFirstSpawn = ignoreTimerFirstSpawn.Value, 151 | MinSpawnDistanceFromPlayer = minSpawnDistanceFromPlayer.Value 152 | }; 153 | 154 | // Add new entry to sessionLocations.Locations list since we adding new ones 155 | 156 | // Check if Locations is null 157 | DonutComponent.sessionLocations.Locations ??= new List(); 158 | 159 | DonutComponent.sessionLocations.Locations.Add(newEntry); 160 | 161 | // make it testable immediately by adding the timer needed to the groupnum in DonutComponent.groupedHotspotTimers 162 | if (!DonutComponent.groupedHotspotTimers.ContainsKey(newEntry.GroupNum)) 163 | { 164 | //create a new list for the groupnum and add the timer to it 165 | DonutComponent.groupedHotspotTimers.Add(newEntry.GroupNum, new List()); 166 | } 167 | 168 | //create a new timer for the entry and add it to the list 169 | var timer = new HotspotTimer(newEntry); 170 | DonutComponent.groupedHotspotTimers[newEntry.GroupNum].Add(timer); 171 | 172 | var txt = $"Donuts: Wrote Entry for {newEntry.Name}\n SpawnType: {newEntry.WildSpawnType}\n Position: {newEntry.Position.x}, {newEntry.Position.y}, {newEntry.Position.z}"; 173 | var displayMessageNotificationMethod = Gizmos.GetDisplayMessageNotificationMethod(); 174 | if (displayMessageNotificationMethod != null) 175 | { 176 | displayMessageNotificationMethod.Invoke(null, new object[] { txt, ENotificationDurationType.Long, ENotificationIconType.Default, Color.yellow }); 177 | } 178 | } 179 | 180 | internal static async UniTask WriteToJsonFile() 181 | { 182 | // Check if any of the required objects are null 183 | if (Donuts.DonutComponent.gameWorld == null) 184 | { 185 | Logger.LogDebug("IBotGame Not Instantiated or gameWorld is null."); 186 | return; 187 | } 188 | 189 | string dllPath = Assembly.GetExecutingAssembly().Location; 190 | string directoryPath = Path.GetDirectoryName(dllPath); 191 | string jsonFolderPath = Path.Combine(directoryPath, "patterns"); 192 | string json = string.Empty; 193 | string fileName = string.Empty; 194 | 195 | //check if saveNewFileOnly is true then we use the sessionLocations object to serialize. Otherwise we use combinedLocations 196 | if (saveNewFileOnly.Value) 197 | { 198 | // take the sessionLocations object only and serialize it to json 199 | json = JsonConvert.SerializeObject(Donuts.DonutComponent.sessionLocations, Formatting.Indented); 200 | fileName = DonutsBotPrep.maplocation + "_" + UnityEngine.Random.Range(0, 1000) + "_NewLocOnly.json"; 201 | } 202 | else 203 | { 204 | //combine the fightLocations and sessionLocations objects into one variable 205 | FightLocations combinedLocations = new Donuts.Models.FightLocations 206 | { 207 | Locations = Donuts.DonutComponent.fightLocations.Locations.Concat(Donuts.DonutComponent.sessionLocations.Locations).ToList() 208 | }; 209 | 210 | json = JsonConvert.SerializeObject(combinedLocations, Formatting.Indented); 211 | fileName = DonutsBotPrep.maplocation + "_" + UnityEngine.Random.Range(0, 1000) + "_All.json"; 212 | } 213 | 214 | //write json to file with filename == Donuts.DonutComponent.maplocation + random number 215 | string jsonFilePath = Path.Combine(jsonFolderPath, fileName); 216 | 217 | await UniTask.SwitchToThreadPool(); 218 | using (StreamWriter writer = new StreamWriter(jsonFilePath, false)) 219 | { 220 | await writer.WriteAsync(json); 221 | } 222 | await UniTask.SwitchToMainThread(); 223 | 224 | var txt = $"Donuts: Wrote Json File to: {jsonFilePath}"; 225 | var displayMessageNotificationMethod = Gizmos.GetDisplayMessageNotificationMethod(); 226 | if (displayMessageNotificationMethod != null) 227 | { 228 | displayMessageNotificationMethod.Invoke(null, new object[] { txt, ENotificationDurationType.Long, ENotificationIconType.Default, Color.yellow }); 229 | } 230 | } 231 | 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /Plugin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Threading.Tasks; 7 | using SPT.Reflection.Patching; 8 | using BepInEx; 9 | using BepInEx.Configuration; 10 | using BepInEx.Logging; 11 | using Donuts.Models; 12 | using Donuts.Patches; 13 | using EFT; 14 | using EFT.Communications; 15 | using Newtonsoft.Json; 16 | using UnityEngine; 17 | using dvize.Donuts.Patches; 18 | 19 | //disable the ide0007 warning for the entire file 20 | #pragma warning disable IDE0007 21 | 22 | namespace Donuts 23 | { 24 | [BepInPlugin("com.dvize.Donuts", "dvize.Donuts", "1.6.1")] 25 | [BepInDependency("com.SPT.core", "3.9.0")] 26 | [BepInDependency("xyz.drakia.waypoints")] 27 | [BepInDependency("com.Arys.UnityToolkit")] 28 | public class DonutsPlugin : BaseUnityPlugin 29 | { 30 | internal static PluginGUIHelper pluginGUIHelper; 31 | internal static ConfigEntry toggleGUIKey; 32 | internal static KeyCode escapeKey; 33 | internal static new ManualLogSource Logger; 34 | DonutsPlugin() 35 | { 36 | Logger ??= BepInEx.Logging.Logger.CreateLogSource(nameof(DonutsPlugin)); 37 | } 38 | private async void Awake() 39 | { 40 | // Run dependency checker 41 | if (!DependencyChecker.ValidateDependencies(Logger, Info, this.GetType(), Config)) 42 | { 43 | throw new Exception($"Missing Dependencies"); 44 | } 45 | 46 | pluginGUIHelper = gameObject.AddComponent(); 47 | 48 | toggleGUIKey = Config.Bind( 49 | "Config Settings", 50 | "Key To Enable/Disable Config Interface", 51 | new KeyboardShortcut(KeyCode.F9), 52 | "Key to Enable/Disable Donuts Configuration Menu"); 53 | 54 | escapeKey = KeyCode.Escape; 55 | 56 | // Patches 57 | new NewGameDonutsPatch().Enable(); 58 | new BotGroupAddEnemyPatch().Enable(); 59 | new BotMemoryAddEnemyPatch().Enable(); 60 | new MatchEndPlayerDisposePatch().Enable(); 61 | new PatchStandbyTeleport().Enable(); 62 | new BotProfilePreparationHook().Enable(); 63 | new AddEnemyPatch().Enable(); 64 | new ShootDataNullRefPatch().Enable(); 65 | new CoverPointMasterNullRef().Enable(); 66 | new DelayedGameStartPatch().Enable(); 67 | new PlayerFireControlPatchGetter().Enable(); 68 | new PlayerFireControlPatchSetter().Enable(); 69 | 70 | ImportConfig(); 71 | await SetupScenariosUI(); 72 | } 73 | 74 | private async Task SetupScenariosUI() 75 | { 76 | await LoadDonutsFoldersAsync(); 77 | 78 | var scenarioValuesList = DefaultPluginVars.pmcScenarioCombinedArray?.ToList() ?? new List(); 79 | var scavScenarioValuesList = DefaultPluginVars.scavScenarioCombinedArray?.ToList() ?? new List(); 80 | 81 | DefaultPluginVars.pmcScenarioCombinedArray = scenarioValuesList.ToArray(); 82 | DefaultPluginVars.scavScenarioCombinedArray = scavScenarioValuesList.ToArray(); 83 | 84 | #if DEBUG 85 | Logger.LogWarning($"Loaded PMC Scenarios: {string.Join(", ", DefaultPluginVars.pmcScenarioCombinedArray)}"); 86 | Logger.LogWarning($"Loaded Scav Scenarios: {string.Join(", ", DefaultPluginVars.scavScenarioCombinedArray)}"); 87 | #endif 88 | 89 | // Dynamically initialize the scenario settings 90 | DefaultPluginVars.pmcScenarioSelection = new Setting( 91 | "PMC Raid Spawn Preset Selection", 92 | "Select a preset to use when spawning as PMC", 93 | DefaultPluginVars.pmcScenarioSelectionValue ?? "live-like", 94 | "live-like", 95 | null, 96 | null, 97 | DefaultPluginVars.pmcScenarioCombinedArray 98 | ); 99 | 100 | DefaultPluginVars.scavScenarioSelection = new Setting( 101 | "SCAV Raid Spawn Preset Selection", 102 | "Select a preset to use when spawning as SCAV", 103 | DefaultPluginVars.scavScenarioSelectionValue ?? "live-like", 104 | "live-like", 105 | null, 106 | null, 107 | DefaultPluginVars.scavScenarioCombinedArray 108 | ); 109 | 110 | // Call InitializeDropdownIndices to ensure scenarios are loaded and indices are set 111 | DrawMainSettings.InitializeDropdownIndices(); 112 | } 113 | 114 | 115 | private void Update() 116 | { 117 | if (ImGUIToolkit.IsSettingKeybind()) 118 | { 119 | // If setting a keybind, do not trigger functionality 120 | return; 121 | } 122 | 123 | if (IsKeyPressed(toggleGUIKey.Value) || IsKeyPressed(escapeKey)) 124 | { 125 | if (IsKeyPressed(escapeKey)) 126 | { 127 | //check if the config window is open 128 | if (DefaultPluginVars.showGUI) 129 | { 130 | DefaultPluginVars.showGUI = false; 131 | } 132 | } 133 | else 134 | { 135 | DefaultPluginVars.showGUI = !DefaultPluginVars.showGUI; 136 | } 137 | } 138 | 139 | if (IsKeyPressed(DefaultPluginVars.CreateSpawnMarkerKey.Value)) 140 | { 141 | EditorFunctions.CreateSpawnMarker(); 142 | } 143 | if (IsKeyPressed(DefaultPluginVars.WriteToFileKey.Value)) 144 | { 145 | EditorFunctions.WriteToJsonFile(); 146 | } 147 | if (IsKeyPressed(DefaultPluginVars.DeleteSpawnMarkerKey.Value)) 148 | { 149 | EditorFunctions.DeleteSpawnMarker(); 150 | } 151 | } 152 | public static void ImportConfig() 153 | { 154 | // Get the path of the currently executing assembly 155 | var dllPath = Assembly.GetExecutingAssembly().Location; 156 | var configDirectory = Path.Combine(Path.GetDirectoryName(dllPath), "Config"); 157 | var configFilePath = Path.Combine(configDirectory, "DefaultPluginVars.json"); 158 | 159 | if (!File.Exists(configFilePath)) 160 | { 161 | Logger.LogError($"Config file not found: {configFilePath}, creating a new one"); 162 | PluginGUIHelper.ExportConfig(); 163 | return; 164 | 165 | } 166 | 167 | string json = File.ReadAllText(configFilePath); 168 | DefaultPluginVars.ImportFromJson(json); 169 | } 170 | 171 | #region Donuts Non-Raid Related Methods 172 | private async Task LoadDonutsFoldersAsync() 173 | { 174 | var dllPath = Assembly.GetExecutingAssembly().Location; 175 | var directoryPath = Path.GetDirectoryName(dllPath); 176 | 177 | DefaultPluginVars.pmcScenarios = await LoadFoldersAsync(Path.Combine(directoryPath, "ScenarioConfig.json")); 178 | DefaultPluginVars.pmcRandomScenarios = await LoadFoldersAsync(Path.Combine(directoryPath, "RandomScenarioConfig.json")); 179 | DefaultPluginVars.scavScenarios = await LoadFoldersAsync(Path.Combine(directoryPath, "ScenarioConfig.json")); 180 | DefaultPluginVars.randomScavScenarios = await LoadFoldersAsync(Path.Combine(directoryPath, "RandomScenarioConfig.json")); 181 | 182 | await PopulateScenarioValuesAsync(); 183 | } 184 | 185 | private async Task PopulateScenarioValuesAsync() 186 | { 187 | DefaultPluginVars.pmcScenarioCombinedArray = await GenerateScenarioValuesAsync(DefaultPluginVars.pmcScenarios, DefaultPluginVars.pmcRandomScenarios); 188 | Logger.LogWarning($"Loaded {DefaultPluginVars.pmcScenarioCombinedArray.Length} PMC Scenarios and Finished Generating"); 189 | 190 | DefaultPluginVars.scavScenarioCombinedArray = await GenerateScenarioValuesAsync(DefaultPluginVars.scavScenarios, DefaultPluginVars.randomScavScenarios); 191 | Logger.LogWarning($"Loaded {DefaultPluginVars.scavScenarioCombinedArray.Length} SCAV Scenarios and Finished Generating"); 192 | } 193 | 194 | private async Task GenerateScenarioValuesAsync(List scenarios, List randomScenarios) 195 | { 196 | var valuesList = new List(); 197 | 198 | await AddScenarioNamesToListAsync(scenarios, valuesList, folder => folder.Name); 199 | await AddScenarioNamesToListAsync(randomScenarios, valuesList, folder => folder.RandomScenarioConfig); 200 | 201 | return valuesList.ToArray(); 202 | } 203 | 204 | private async Task AddScenarioNamesToListAsync(IEnumerable folders, List valuesList, Func getNameFunc) 205 | { 206 | if (folders != null) 207 | { 208 | foreach (var folder in folders) 209 | { 210 | var name = getNameFunc(folder); 211 | Logger.LogWarning($"Adding scenario: {name}"); 212 | valuesList.Add(name); 213 | } 214 | } 215 | } 216 | 217 | private static async Task> LoadFoldersAsync(string filePath) 218 | { 219 | if (!File.Exists(filePath)) 220 | { 221 | Logger.LogWarning($"File not found: {filePath}"); 222 | return new List(); 223 | } 224 | 225 | var fileContent = await Task.Run(() => File.ReadAllText(filePath)); 226 | var folders = JsonConvert.DeserializeObject>(fileContent); 227 | 228 | if (folders == null || folders.Count == 0) 229 | { 230 | Logger.LogError("No Donuts Folders found in Scenario Config file at: " + filePath); 231 | return new List(); 232 | } 233 | 234 | Logger.LogWarning($"Loaded {folders.Count} Donuts Scenario Folders"); 235 | return folders; 236 | } 237 | 238 | 239 | #endregion 240 | 241 | #region Donuts Raid Related Scenario Selection Methods 242 | 243 | internal static Folder GrabDonutsFolder(string folderName) 244 | { 245 | return DefaultPluginVars.pmcScenarios.FirstOrDefault(folder => folder.Name == folderName); 246 | } 247 | 248 | internal static string RunWeightedScenarioSelection() 249 | { 250 | try 251 | { 252 | var scenarioSelection = DefaultPluginVars.pmcScenarioSelection.Value; 253 | 254 | if (SPT.SinglePlayer.Utils.InRaid.RaidChangesUtil.IsScavRaid) 255 | { 256 | Logger.LogWarning("This is a SCAV raid, using SCAV raid preset selector"); 257 | scenarioSelection = DefaultPluginVars.scavScenarioSelection.Value; 258 | } 259 | 260 | var selectedFolder = DefaultPluginVars.pmcScenarios.FirstOrDefault(folder => folder.Name == scenarioSelection) 261 | ?? DefaultPluginVars.pmcRandomScenarios.FirstOrDefault(folder => folder.RandomScenarioConfig == scenarioSelection); 262 | 263 | if (selectedFolder != null) 264 | { 265 | return SelectPreset(selectedFolder); 266 | } 267 | 268 | return null; 269 | } 270 | catch (Exception e) 271 | { 272 | Logger.LogError("Error in RunWeightedScenarioSelection: " + e); 273 | return null; 274 | } 275 | } 276 | 277 | private static string SelectPreset(Folder folder) 278 | { 279 | if (folder.presets == null || folder.presets.Count == 0) return folder.Name; 280 | 281 | var totalWeight = folder.presets.Sum(preset => preset.Weight); 282 | var randomWeight = UnityEngine.Random.Range(0, totalWeight); 283 | 284 | var cumulativeWeight = 0; 285 | foreach (var preset in folder.presets) 286 | { 287 | cumulativeWeight += preset.Weight; 288 | if (randomWeight < cumulativeWeight) 289 | { 290 | return preset.Name; 291 | } 292 | } 293 | 294 | // In case something goes wrong, return the last preset as a fallback 295 | var fallbackPreset = folder.presets.LastOrDefault(); 296 | Logger.LogError("Fallback Preset: " + fallbackPreset.Name); 297 | return fallbackPreset?.Name; 298 | } 299 | 300 | public static void LogSelectedPreset(string selectedPreset) 301 | { 302 | Console.WriteLine($"Donuts Selected Spawn Preset: {selectedPreset}"); 303 | 304 | if (DefaultPluginVars.ShowRandomFolderChoice.Value && DonutComponent.methodCache.TryGetValue("DisplayMessageNotification", out var displayMessageNotificationMethod)) 305 | { 306 | var txt = $"Donuts Selected Spawn Preset: {selectedPreset}"; 307 | EFT.UI.ConsoleScreen.Log(txt); 308 | displayMessageNotificationMethod.Invoke(null, new object[] { txt, ENotificationDurationType.Long, ENotificationIconType.Default, Color.yellow }); 309 | } 310 | } 311 | 312 | #endregion 313 | 314 | internal static bool IsKeyPressed(KeyboardShortcut key) 315 | { 316 | if (!UnityInput.Current.GetKeyDown(key.MainKey)) return false; 317 | 318 | return key.Modifiers.All(modifier => UnityInput.Current.GetKey(modifier)); 319 | } 320 | 321 | internal static bool IsKeyPressed(KeyCode key) 322 | { 323 | if (!UnityInput.Current.GetKeyDown(key)) return false; 324 | 325 | return true; 326 | } 327 | 328 | } 329 | 330 | //re-initializes each new game 331 | internal class NewGameDonutsPatch : ModulePatch 332 | { 333 | protected override MethodBase GetTargetMethod() => typeof(GameWorld).GetMethod(nameof(GameWorld.OnGameStarted)); 334 | 335 | [PatchPrefix] 336 | public static void PatchPrefix() => DonutComponent.Enable(); 337 | } 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | } 350 | -------------------------------------------------------------------------------- /PluginGUI/IMGUIToolkit.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Reflection; 3 | using Donuts.Models; 4 | using EFT.Visual; 5 | using HarmonyLib; 6 | using UnityEngine; 7 | 8 | #pragma warning disable IDE0007, IDE0044 9 | 10 | namespace Donuts 11 | { 12 | public class ImGUIToolkit 13 | { 14 | private static Dictionary dropdownStates = new Dictionary(); 15 | private static Dictionary accordionStates = new Dictionary(); 16 | private static Dictionary newKeybinds = new Dictionary(); 17 | private static Dictionary keybindStates = new Dictionary(); 18 | private static bool isSettingKeybind = false; // Flag to indicate if setting a keybind 19 | 20 | private static bool stylesInitialized = false; 21 | 22 | private static void EnsureStylesInitialized() 23 | { 24 | if (!stylesInitialized) 25 | { 26 | PluginGUIHelper.InitializeStyles(); 27 | stylesInitialized = true; 28 | } 29 | } 30 | 31 | internal static int Dropdown(Setting setting, int selectedIndex) 32 | { 33 | EnsureStylesInitialized(); 34 | 35 | if (setting.LogErrorOnceIfOptionsInvalid()) 36 | { 37 | return selectedIndex; 38 | } 39 | 40 | if (selectedIndex >= setting.Options.Length) 41 | { 42 | selectedIndex = 0; 43 | } 44 | 45 | int dropdownId = GUIUtility.GetControlID(FocusType.Passive); 46 | 47 | if (!dropdownStates.ContainsKey(dropdownId)) 48 | { 49 | dropdownStates[dropdownId] = false; 50 | } 51 | 52 | GUILayout.BeginHorizontal(); 53 | 54 | GUIContent labelContent = new GUIContent(setting.Name, setting.ToolTipText); 55 | GUILayout.Label(labelContent, PluginGUIHelper.labelStyle, GUILayout.Width(200)); 56 | 57 | GUIStyle currentDropdownStyle = dropdownStates[dropdownId] ? PluginGUIHelper.subTabButtonActiveStyle : PluginGUIHelper.subTabButtonStyle; 58 | 59 | GUIContent buttonContent = new GUIContent(setting.Options[selectedIndex]?.ToString(), setting.ToolTipText); 60 | if (GUILayout.Button(buttonContent, currentDropdownStyle, GUILayout.Width(300))) 61 | { 62 | dropdownStates[dropdownId] = !dropdownStates[dropdownId]; 63 | } 64 | 65 | GUILayout.EndHorizontal(); 66 | 67 | if (dropdownStates[dropdownId]) 68 | { 69 | GUILayout.BeginHorizontal(); 70 | GUILayout.Space(209); 71 | 72 | GUILayout.BeginVertical(); 73 | 74 | for (int i = 0; i < setting.Options.Length; i++) 75 | { 76 | GUIContent optionContent = new GUIContent(setting.Options[i]?.ToString(), setting.ToolTipText); 77 | if (GUILayout.Button(optionContent, PluginGUIHelper.subTabButtonStyle, GUILayout.Width(300))) 78 | { 79 | selectedIndex = i; 80 | setting.Value = setting.Options[i]; 81 | dropdownStates[dropdownId] = false; 82 | } 83 | } 84 | 85 | GUILayout.EndVertical(); 86 | GUILayout.EndHorizontal(); 87 | } 88 | 89 | ShowTooltip(); 90 | 91 | return selectedIndex; 92 | } 93 | 94 | public static float Slider(string label, string toolTip, float value, float min, float max) 95 | { 96 | EnsureStylesInitialized(); 97 | 98 | GUILayout.BeginHorizontal(); 99 | 100 | GUIContent labelContent = new GUIContent(label, toolTip); 101 | GUILayout.Label(labelContent, PluginGUIHelper.labelStyle, GUILayout.Width(200)); 102 | 103 | value = GUILayout.HorizontalSlider(value, min, max, PluginGUIHelper.horizontalSliderStyle, PluginGUIHelper.horizontalSliderThumbStyle, GUILayout.Width(300)); 104 | 105 | GUI.SetNextControlName("floatTextField"); 106 | string textFieldValue = GUILayout.TextField(value.ToString("F2"), PluginGUIHelper.textFieldStyle, GUILayout.Width(100)); 107 | if (float.TryParse(textFieldValue, out float newValue)) 108 | { 109 | value = Mathf.Clamp(newValue, min, max); 110 | } 111 | 112 | GUILayout.EndHorizontal(); 113 | ShowTooltip(); 114 | 115 | // Check for Enter key press (both Enter keys) to escape focus 116 | if (Event.current.isKey && (Event.current.keyCode == KeyCode.Return || Event.current.keyCode == KeyCode.KeypadEnter) && GUI.GetNameOfFocusedControl() == "floatTextField") 117 | { 118 | GUI.FocusControl(null); 119 | } 120 | 121 | // Check for mouse click outside the control to escape focus 122 | if (Event.current.type == EventType.MouseDown && GUI.GetNameOfFocusedControl() == "floatTextField") 123 | { 124 | // Check if the mouse click is outside the text field 125 | Rect textFieldRect = GUILayoutUtility.GetLastRect(); 126 | if (!textFieldRect.Contains(Event.current.mousePosition)) 127 | { 128 | GUI.FocusControl(null); 129 | } 130 | } 131 | 132 | return value; 133 | } 134 | 135 | public static int Slider(string label, string toolTip, int value, int min, int max) 136 | { 137 | EnsureStylesInitialized(); 138 | 139 | GUILayout.BeginHorizontal(); 140 | 141 | GUIContent labelContent = new GUIContent(label, toolTip); 142 | GUILayout.Label(labelContent, PluginGUIHelper.labelStyle, GUILayout.Width(200)); 143 | 144 | value = (int)GUILayout.HorizontalSlider(value, min, max, PluginGUIHelper.horizontalSliderStyle, PluginGUIHelper.horizontalSliderThumbStyle, GUILayout.Width(300)); 145 | 146 | GUI.SetNextControlName("intTextField"); 147 | string textFieldValue = GUILayout.TextField(value.ToString(), PluginGUIHelper.textFieldStyle, GUILayout.Width(100)); 148 | if (int.TryParse(textFieldValue, out int newValue)) 149 | { 150 | value = Mathf.Clamp(newValue, min, max); 151 | } 152 | 153 | GUILayout.EndHorizontal(); 154 | ShowTooltip(); 155 | 156 | // Check for Enter key press (both Enter keys) to escape focus 157 | if (Event.current.isKey && (Event.current.keyCode == KeyCode.Return || Event.current.keyCode == KeyCode.KeypadEnter) && GUI.GetNameOfFocusedControl() == "intTextField") 158 | { 159 | GUI.FocusControl(null); 160 | } 161 | 162 | // Check for mouse click outside the control to escape focus 163 | if (Event.current.type == EventType.MouseDown && GUI.GetNameOfFocusedControl() == "intTextField") 164 | { 165 | // Check if the mouse click is outside the text field 166 | Rect textFieldRect = GUILayoutUtility.GetLastRect(); 167 | if (!textFieldRect.Contains(Event.current.mousePosition)) 168 | { 169 | GUI.FocusControl(null); 170 | } 171 | } 172 | 173 | return value; 174 | } 175 | 176 | public static string TextField(string label, string toolTip, string text, int maxLength = 50) 177 | { 178 | // Create GUIContent for the label with tooltip 179 | GUIContent labelContent = new GUIContent(label, toolTip); 180 | GUILayout.BeginHorizontal(); 181 | 182 | GUILayout.Label(labelContent, PluginGUIHelper.labelStyle, GUILayout.Width(200)); 183 | 184 | GUI.SetNextControlName("textField"); 185 | string newText = GUILayout.TextField(text, maxLength, PluginGUIHelper.textFieldStyle, GUILayout.Width(300)); 186 | 187 | // Ensure the text does not exceed the maximum length 188 | if (newText.Length > maxLength) 189 | { 190 | newText = newText.Substring(0, maxLength); 191 | } 192 | 193 | // Check for Enter key press to escape focus 194 | if (Event.current.isKey && Event.current.keyCode == KeyCode.Return && GUI.GetNameOfFocusedControl() == "textField") 195 | { 196 | GUI.FocusControl(null); 197 | } 198 | 199 | GUILayout.EndHorizontal(); 200 | 201 | return newText; 202 | } 203 | 204 | public static bool Toggle(string label, string toolTip, bool value) 205 | { 206 | EnsureStylesInitialized(); 207 | 208 | GUILayout.BeginHorizontal(); 209 | GUIContent labelContent = new GUIContent(label, toolTip); 210 | GUILayout.Label(labelContent, PluginGUIHelper.labelStyle, GUILayout.Width(200)); 211 | GUILayout.Space(10); 212 | 213 | GUIContent toggleContent = new GUIContent(value ? "YES" : "NO", toolTip); 214 | bool newValue = GUILayout.Toggle(value, toggleContent, PluginGUIHelper.toggleButtonStyle, GUILayout.Width(150), GUILayout.Height(35)); 215 | 216 | GUILayout.EndHorizontal(); 217 | 218 | ShowTooltip(); 219 | 220 | return newValue; 221 | } 222 | 223 | public static bool Button(string label, string toolTip, GUIStyle style = null) 224 | { 225 | EnsureStylesInitialized(); 226 | 227 | style ??= PluginGUIHelper.buttonStyle; 228 | 229 | GUIContent buttonContent = new GUIContent(label, toolTip); 230 | bool result = GUILayout.Button(buttonContent, style, GUILayout.Width(200)); 231 | 232 | ShowTooltip(); 233 | 234 | return result; 235 | } 236 | 237 | public static void Accordion(string label, string toolTip, System.Action drawContents) 238 | { 239 | EnsureStylesInitialized(); 240 | 241 | int accordionId = GUIUtility.GetControlID(FocusType.Passive); 242 | 243 | if (!accordionStates.ContainsKey(accordionId)) 244 | { 245 | accordionStates[accordionId] = false; 246 | } 247 | 248 | GUIContent buttonContent = new GUIContent(label, toolTip); 249 | if (GUILayout.Button(buttonContent, PluginGUIHelper.buttonStyle)) 250 | { 251 | accordionStates[accordionId] = !accordionStates[accordionId]; 252 | } 253 | 254 | if (accordionStates[accordionId]) 255 | { 256 | GUILayout.BeginVertical(GUI.skin.box); 257 | drawContents(); 258 | GUILayout.EndVertical(); 259 | } 260 | 261 | ShowTooltip(); 262 | } 263 | 264 | public static KeyCode KeybindField(string label, string toolTip, KeyCode currentKey) 265 | { 266 | EnsureStylesInitialized(); 267 | 268 | int keybindId = GUIUtility.GetControlID(FocusType.Passive); 269 | 270 | if (!keybindStates.ContainsKey(keybindId)) 271 | { 272 | keybindStates[keybindId] = false; 273 | newKeybinds[keybindId] = currentKey; 274 | } 275 | 276 | GUILayout.BeginHorizontal(); 277 | GUIContent labelContent = new GUIContent(label, toolTip); 278 | GUILayout.Label(labelContent, PluginGUIHelper.labelStyle, GUILayout.Width(200)); 279 | GUILayout.Space(10); 280 | 281 | if (keybindStates[keybindId]) 282 | { 283 | GUIContent waitingContent = new GUIContent("Press any key...", toolTip); 284 | GUILayout.Button(waitingContent, PluginGUIHelper.buttonStyle, GUILayout.Width(200)); 285 | isSettingKeybind = true; 286 | } 287 | else 288 | { 289 | GUIContent keyContent = new GUIContent(currentKey.ToString(), toolTip); 290 | if (GUILayout.Button(keyContent, PluginGUIHelper.buttonStyle, GUILayout.Width(200))) 291 | { 292 | keybindStates[keybindId] = true; 293 | isSettingKeybind = true; 294 | } 295 | } 296 | 297 | if (GUILayout.Button("Clear", GUILayout.Width(90))) 298 | { 299 | currentKey = KeyCode.None; 300 | } 301 | 302 | GUILayout.EndHorizontal(); 303 | 304 | if (keybindStates[keybindId]) 305 | { 306 | Event e = Event.current; 307 | if (e.isKey) 308 | { 309 | newKeybinds[keybindId] = e.keyCode; 310 | keybindStates[keybindId] = false; 311 | currentKey = e.keyCode; 312 | System.Threading.Tasks.Task.Delay(1000).ContinueWith(t => isSettingKeybind = false); 313 | } 314 | } 315 | 316 | ShowTooltip(); 317 | 318 | return currentKey; 319 | } 320 | 321 | public static bool IsSettingKeybind() 322 | { 323 | return isSettingKeybind; 324 | } 325 | 326 | private static void ShowTooltip() 327 | { 328 | if (!string.IsNullOrEmpty(GUI.tooltip)) 329 | { 330 | Vector2 mousePosition = Event.current.mousePosition; 331 | Vector2 size = PluginGUIHelper.tooltipStyle.CalcSize(new GUIContent(GUI.tooltip)); 332 | size.y = PluginGUIHelper.tooltipStyle.CalcHeight(new GUIContent(GUI.tooltip), size.x); 333 | Rect tooltipRect = new Rect(mousePosition.x, mousePosition.y - size.y, size.x, size.y); 334 | GUI.Box(tooltipRect, GUI.tooltip, PluginGUIHelper.tooltipStyle); 335 | } 336 | } 337 | 338 | internal static int FindIndex(Setting setting) 339 | { 340 | if (setting == null) 341 | { 342 | DonutsPlugin.Logger.LogError("Setting is null."); 343 | return -1; 344 | } 345 | 346 | for (int i = 0; i < setting.Options.Length; i++) 347 | { 348 | if (EqualityComparer.Default.Equals(setting.Options[i], setting.Value)) 349 | { 350 | return i; 351 | } 352 | } 353 | DonutsPlugin.Logger.LogError($"Value '{setting.Value}' not found in Options for setting '{setting.Name}'"); 354 | return -1; 355 | } 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /PluginGUI/DrawMainSettings.cs: -------------------------------------------------------------------------------- 1 | using Donuts.Models; 2 | using EFT.Visual; 3 | using UnityEngine; 4 | using static Donuts.ImGUIToolkit; 5 | using static Donuts.PluginGUIHelper; 6 | 7 | #pragma warning disable IDE0007, IDE0044 8 | 9 | namespace Donuts 10 | { 11 | internal class DrawMainSettings 12 | { 13 | private static int selectedMainSettingsIndex = 0; 14 | private static string[] mainSettingsSubTabs = { "General", "Spawn Frequency", "Bot Attributes" }; 15 | 16 | // For dropdowns 17 | internal static int botDifficultiesPMCIndex = 0; 18 | internal static int botDifficultiesSCAVIndex = 0; 19 | internal static int botDifficultiesOtherIndex = 0; 20 | internal static int pmcGroupChanceIndex = 0; 21 | internal static int scavGroupChanceIndex = 0; 22 | internal static int pmcFactionIndex = 0; 23 | internal static int forceAllBotTypeIndex = 0; 24 | internal static int pmcScenarioSelectionIndex = 0; 25 | internal static int scavScenarioSelectionIndex = 0; 26 | 27 | // Flag to check if scenarios are loaded 28 | internal static bool scenariosLoaded = false; 29 | 30 | // Need this for dropdowns to show loaded values and also reset to default 31 | static DrawMainSettings() 32 | { 33 | //InitializeDropdownIndices(); 34 | } 35 | 36 | internal static void InitializeDropdownIndices() 37 | { 38 | botDifficultiesPMCIndex = FindIndex(DefaultPluginVars.botDifficultiesPMC); 39 | botDifficultiesSCAVIndex = FindIndex(DefaultPluginVars.botDifficultiesSCAV); 40 | botDifficultiesOtherIndex = FindIndex(DefaultPluginVars.botDifficultiesOther); 41 | 42 | pmcGroupChanceIndex = FindIndex(DefaultPluginVars.pmcGroupChance); 43 | scavGroupChanceIndex = FindIndex(DefaultPluginVars.scavGroupChance); 44 | 45 | pmcFactionIndex = FindIndex(DefaultPluginVars.pmcFaction); 46 | forceAllBotTypeIndex = FindIndex(DefaultPluginVars.forceAllBotType); 47 | 48 | 49 | 50 | 51 | if (DefaultPluginVars.pmcScenarioSelection.Options != null && DefaultPluginVars.pmcScenarioSelection.Options.Length > 0) 52 | { 53 | pmcScenarioSelectionIndex = FindIndex(DefaultPluginVars.pmcScenarioSelection); 54 | 55 | if (pmcScenarioSelectionIndex == -1) 56 | { 57 | DonutsPlugin.Logger.LogError("Warning: pmcScenarioSelectionIndex not found, defaulting to 0"); 58 | pmcScenarioSelectionIndex = 0; 59 | } 60 | } 61 | 62 | if (DefaultPluginVars.scavScenarioSelection.Options != null && DefaultPluginVars.scavScenarioSelection.Options.Length > 0) 63 | { 64 | scavScenarioSelectionIndex = FindIndex(DefaultPluginVars.scavScenarioSelection); 65 | 66 | if (scavScenarioSelectionIndex == -1) 67 | { 68 | DonutsPlugin.Logger.LogError("Warning: scavScenarioSelectionIndex not found, defaulting to 0"); 69 | scavScenarioSelectionIndex = 0; 70 | } 71 | } 72 | 73 | scenariosLoaded = (DefaultPluginVars.pmcScenarioSelection?.Options != null && DefaultPluginVars.pmcScenarioSelection.Options.Length > 0) && 74 | (DefaultPluginVars.scavScenarioSelection?.Options != null && DefaultPluginVars.scavScenarioSelection.Options.Length > 0); 75 | 76 | #if DEBUG 77 | DonutsPlugin.Logger.LogWarning("scenariosLoaded:" + scenariosLoaded); 78 | #endif 79 | } 80 | 81 | 82 | internal static void Enable() 83 | { 84 | // Initialize the custom styles for the dropdown 85 | GUILayout.Space(30); 86 | GUILayout.BeginHorizontal(); 87 | 88 | // Left-hand navigation menu for sub-tabs 89 | GUILayout.BeginVertical(GUILayout.Width(150)); 90 | 91 | GUILayout.Space(20); 92 | DrawSubTabs(); 93 | GUILayout.EndVertical(); 94 | 95 | // Space between menu and subtab pages 96 | GUILayout.Space(40); 97 | 98 | // Right-hand content area for selected sub-tab 99 | GUILayout.BeginVertical(); 100 | 101 | switch (selectedMainSettingsIndex) 102 | { 103 | case 0: 104 | DrawMainSettingsGeneral(); 105 | break; 106 | case 1: 107 | DrawMainSettingsSpawnFrequency(); 108 | break; 109 | case 2: 110 | DrawMainSettingsBotAttributes(); 111 | break; 112 | } 113 | 114 | GUILayout.EndVertical(); 115 | GUILayout.EndHorizontal(); 116 | } 117 | 118 | private static void DrawSubTabs() 119 | { 120 | for (int i = 0; i < mainSettingsSubTabs.Length; i++) 121 | { 122 | GUIStyle currentStyle = subTabButtonStyle; // Use the defined subTabButtonStyle 123 | if (selectedMainSettingsIndex == i) 124 | { 125 | currentStyle = subTabButtonActiveStyle; // Use the defined subTabButtonActiveStyle 126 | } 127 | 128 | if (GUILayout.Button(mainSettingsSubTabs[i], currentStyle)) 129 | { 130 | selectedMainSettingsIndex = i; 131 | } 132 | } 133 | } 134 | 135 | internal static void DrawMainSettingsGeneral() 136 | { 137 | GUILayout.BeginHorizontal(); 138 | GUILayout.BeginVertical(); 139 | 140 | DefaultPluginVars.PluginEnabled.Value = Toggle(DefaultPluginVars.PluginEnabled.Name, DefaultPluginVars.PluginEnabled.ToolTipText, DefaultPluginVars.PluginEnabled.Value); 141 | GUILayout.Space(10); 142 | 143 | DefaultPluginVars.DespawnEnabledPMC.Value = Toggle(DefaultPluginVars.DespawnEnabledPMC.Name, DefaultPluginVars.DespawnEnabledPMC.ToolTipText, DefaultPluginVars.DespawnEnabledPMC.Value); 144 | GUILayout.Space(10); 145 | 146 | DefaultPluginVars.DespawnEnabledSCAV.Value = Toggle(DefaultPluginVars.DespawnEnabledSCAV.Name, DefaultPluginVars.DespawnEnabledSCAV.ToolTipText, DefaultPluginVars.DespawnEnabledSCAV.Value); 147 | GUILayout.Space(10); 148 | 149 | DefaultPluginVars.despawnInterval.Value = Slider(DefaultPluginVars.despawnInterval.Name, DefaultPluginVars.despawnInterval.ToolTipText, DefaultPluginVars.despawnInterval.Value, 0f, 1000f); 150 | GUILayout.Space(10); 151 | 152 | DefaultPluginVars.ShowRandomFolderChoice.Value = Toggle(DefaultPluginVars.ShowRandomFolderChoice.Name, DefaultPluginVars.ShowRandomFolderChoice.ToolTipText, DefaultPluginVars.ShowRandomFolderChoice.Value); 153 | GUILayout.Space(10); 154 | 155 | DefaultPluginVars.battleStateCoolDown.Value = Slider(DefaultPluginVars.battleStateCoolDown.Name, DefaultPluginVars.battleStateCoolDown.ToolTipText, DefaultPluginVars.battleStateCoolDown.Value, 0f, 1000f); 156 | GUILayout.Space(10); 157 | 158 | if (scenariosLoaded) 159 | { 160 | pmcScenarioSelectionIndex = Dropdown(DefaultPluginVars.pmcScenarioSelection, pmcScenarioSelectionIndex); 161 | DefaultPluginVars.pmcScenarioSelection.Value = DefaultPluginVars.pmcScenarioSelection.Options[pmcScenarioSelectionIndex]; 162 | GUILayout.Space(10); 163 | 164 | scavScenarioSelectionIndex = Dropdown(DefaultPluginVars.scavScenarioSelection, scavScenarioSelectionIndex); 165 | DefaultPluginVars.scavScenarioSelection.Value = DefaultPluginVars.scavScenarioSelection.Options[scavScenarioSelectionIndex]; 166 | } 167 | else 168 | { 169 | GUILayout.Label("Loading PMC scenarios..."); 170 | GUILayout.Label("Loading SCAV scenarios..."); 171 | } 172 | 173 | GUILayout.EndVertical(); 174 | GUILayout.EndHorizontal(); 175 | } 176 | 177 | internal static void DrawMainSettingsSpawnFrequency() 178 | { 179 | GUILayout.BeginHorizontal(); 180 | GUILayout.BeginVertical(); 181 | 182 | DefaultPluginVars.HardCapEnabled.Value = Toggle(DefaultPluginVars.HardCapEnabled.Name, DefaultPluginVars.HardCapEnabled.ToolTipText, DefaultPluginVars.HardCapEnabled.Value); 183 | GUILayout.Space(10); 184 | 185 | DefaultPluginVars.useTimeBasedHardStop.Value = Toggle(DefaultPluginVars.useTimeBasedHardStop.Name, DefaultPluginVars.useTimeBasedHardStop.ToolTipText, DefaultPluginVars.useTimeBasedHardStop.Value); 186 | GUILayout.Space(10); 187 | 188 | DefaultPluginVars.hardStopOptionPMC.Value = Toggle(DefaultPluginVars.hardStopOptionPMC.Name, DefaultPluginVars.hardStopOptionPMC.ToolTipText, DefaultPluginVars.hardStopOptionPMC.Value); 189 | GUILayout.Space(10); 190 | 191 | if (DefaultPluginVars.useTimeBasedHardStop.Value) 192 | { 193 | DefaultPluginVars.hardStopTimePMC.Value = Slider(DefaultPluginVars.hardStopTimePMC.Name, DefaultPluginVars.hardStopTimePMC.ToolTipText, DefaultPluginVars.hardStopTimePMC.Value, 0, 10000); 194 | } 195 | else 196 | { 197 | DefaultPluginVars.hardStopPercentPMC.Value = Slider(DefaultPluginVars.hardStopPercentPMC.Name, DefaultPluginVars.hardStopPercentPMC.ToolTipText, DefaultPluginVars.hardStopPercentPMC.Value, 0, 100); 198 | } 199 | GUILayout.Space(10); 200 | 201 | DefaultPluginVars.hardStopOptionSCAV.Value = Toggle(DefaultPluginVars.hardStopOptionSCAV.Name, DefaultPluginVars.hardStopOptionSCAV.ToolTipText, DefaultPluginVars.hardStopOptionSCAV.Value); 202 | GUILayout.Space(10); 203 | 204 | if (DefaultPluginVars.useTimeBasedHardStop.Value) 205 | { 206 | DefaultPluginVars.hardStopTimeSCAV.Value = Slider(DefaultPluginVars.hardStopTimeSCAV.Name, DefaultPluginVars.hardStopTimeSCAV.ToolTipText, DefaultPluginVars.hardStopTimeSCAV.Value, 0, 10000); 207 | } 208 | else 209 | { 210 | DefaultPluginVars.hardStopPercentSCAV.Value = Slider(DefaultPluginVars.hardStopPercentSCAV.Name, DefaultPluginVars.hardStopPercentSCAV.ToolTipText, DefaultPluginVars.hardStopPercentSCAV.Value, 0, 100); 211 | } 212 | GUILayout.Space(10); 213 | 214 | DefaultPluginVars.maxRespawnsPMC.Value = Slider(DefaultPluginVars.maxRespawnsPMC.Name, DefaultPluginVars.maxRespawnsPMC.ToolTipText, DefaultPluginVars.maxRespawnsPMC.Value, 0, 100); 215 | GUILayout.Space(10); 216 | 217 | DefaultPluginVars.maxRespawnsSCAV.Value = Slider(DefaultPluginVars.maxRespawnsSCAV.Name, DefaultPluginVars.maxRespawnsSCAV.ToolTipText, DefaultPluginVars.maxRespawnsSCAV.Value, 0, 100); 218 | GUILayout.Space(10); 219 | 220 | GUILayout.EndVertical(); 221 | GUILayout.BeginVertical(); 222 | 223 | DefaultPluginVars.coolDownTimer.Value = Slider(DefaultPluginVars.coolDownTimer.Name, DefaultPluginVars.coolDownTimer.ToolTipText, DefaultPluginVars.coolDownTimer.Value, 0f, 1000f); 224 | GUILayout.Space(10); 225 | 226 | DefaultPluginVars.hotspotBoostPMC.Value = Toggle(DefaultPluginVars.hotspotBoostPMC.Name, DefaultPluginVars.hotspotBoostPMC.ToolTipText, DefaultPluginVars.hotspotBoostPMC.Value); 227 | GUILayout.Space(10); 228 | 229 | DefaultPluginVars.hotspotBoostSCAV.Value = Toggle(DefaultPluginVars.hotspotBoostSCAV.Name, DefaultPluginVars.hotspotBoostSCAV.ToolTipText, DefaultPluginVars.hotspotBoostSCAV.Value); 230 | GUILayout.Space(10); 231 | 232 | DefaultPluginVars.hotspotIgnoreHardCapPMC.Value = Toggle(DefaultPluginVars.hotspotIgnoreHardCapPMC.Name, DefaultPluginVars.hotspotIgnoreHardCapPMC.ToolTipText, DefaultPluginVars.hotspotIgnoreHardCapPMC.Value); 233 | GUILayout.Space(10); 234 | 235 | DefaultPluginVars.hotspotIgnoreHardCapSCAV.Value = Toggle(DefaultPluginVars.hotspotIgnoreHardCapSCAV.Name, DefaultPluginVars.hotspotIgnoreHardCapSCAV.ToolTipText, DefaultPluginVars.hotspotIgnoreHardCapSCAV.Value); 236 | GUILayout.Space(10); 237 | 238 | GUILayout.EndVertical(); 239 | GUILayout.EndHorizontal(); 240 | 241 | GUILayout.BeginHorizontal(); 242 | GUILayout.BeginVertical(); 243 | 244 | pmcGroupChanceIndex = Dropdown(DefaultPluginVars.pmcGroupChance, pmcGroupChanceIndex); 245 | DefaultPluginVars.pmcGroupChance.Value = DefaultPluginVars.pmcGroupChance.Options[pmcGroupChanceIndex]; 246 | GUILayout.Space(10); 247 | 248 | scavGroupChanceIndex = Dropdown(DefaultPluginVars.scavGroupChance, scavGroupChanceIndex); 249 | DefaultPluginVars.scavGroupChance.Value = DefaultPluginVars.scavGroupChance.Options[scavGroupChanceIndex]; 250 | 251 | GUILayout.EndVertical(); 252 | GUILayout.EndHorizontal(); 253 | } 254 | 255 | internal static void DrawMainSettingsBotAttributes() 256 | { 257 | // Draw other spawn settings 258 | GUILayout.BeginHorizontal(); 259 | GUILayout.BeginVertical(); 260 | 261 | // Add spacing before each control to ensure proper vertical alignment 262 | botDifficultiesPMCIndex = Dropdown(DefaultPluginVars.botDifficultiesPMC, botDifficultiesPMCIndex); 263 | DefaultPluginVars.botDifficultiesPMC.Value = DefaultPluginVars.botDifficultiesPMC.Options[botDifficultiesPMCIndex]; 264 | 265 | GUILayout.Space(10); // Add vertical space 266 | 267 | botDifficultiesSCAVIndex = Dropdown(DefaultPluginVars.botDifficultiesSCAV, botDifficultiesSCAVIndex); 268 | DefaultPluginVars.botDifficultiesSCAV.Value = DefaultPluginVars.botDifficultiesSCAV.Options[botDifficultiesSCAVIndex]; 269 | 270 | GUILayout.Space(10); // Add vertical space 271 | 272 | botDifficultiesOtherIndex = Dropdown(DefaultPluginVars.botDifficultiesOther, botDifficultiesOtherIndex); 273 | DefaultPluginVars.botDifficultiesOther.Value = DefaultPluginVars.botDifficultiesOther.Options[botDifficultiesOtherIndex]; 274 | 275 | GUILayout.Space(10); // Add vertical space 276 | 277 | pmcFactionIndex = Dropdown(DefaultPluginVars.pmcFaction, pmcFactionIndex); 278 | DefaultPluginVars.pmcFaction.Value = DefaultPluginVars.pmcFaction.Options[pmcFactionIndex]; 279 | 280 | GUILayout.Space(10); // Add vertical space 281 | 282 | forceAllBotTypeIndex = Dropdown(DefaultPluginVars.forceAllBotType, forceAllBotTypeIndex); 283 | DefaultPluginVars.forceAllBotType.Value = DefaultPluginVars.forceAllBotType.Options[forceAllBotTypeIndex]; 284 | 285 | GUILayout.Space(10); // Add vertical space 286 | 287 | DefaultPluginVars.pmcFactionRatio.Value = Slider(DefaultPluginVars.pmcFactionRatio.Name, 288 | DefaultPluginVars.pmcFactionRatio.ToolTipText, DefaultPluginVars.pmcFactionRatio.Value, 0, 100); 289 | 290 | GUILayout.EndVertical(); 291 | GUILayout.EndHorizontal(); 292 | } 293 | } 294 | 295 | 296 | 297 | 298 | } 299 | -------------------------------------------------------------------------------- /PluginGUI/PluginGUIHelper.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Reflection; 3 | using System.Linq; 4 | using SPT.Reflection.Utils; 5 | using UnityEngine; 6 | using EFT.Communications; 7 | using System; 8 | 9 | namespace Donuts 10 | { 11 | public class PluginGUIHelper : MonoBehaviour 12 | { 13 | internal static Rect windowRect = new Rect(20, 20, 1664, 936); 14 | private bool isDragging = false; 15 | private Vector2 dragOffset; 16 | private Vector2 scrollPosition = Vector2.zero; 17 | 18 | private const float ResizeHandleSize = 30f; 19 | private bool isResizing = false; 20 | private Vector2 resizeStartPos; 21 | 22 | // InitializeStyles static vars 23 | internal static GUIStyle windowStyle; 24 | internal static GUIStyle labelStyle; 25 | internal static GUIStyle buttonStyle; 26 | internal static GUIStyle activeButtonStyle; 27 | internal static GUIStyle subTabButtonStyle; 28 | internal static GUIStyle subTabButtonActiveStyle; 29 | internal static GUIStyle textFieldStyle; 30 | internal static GUIStyle horizontalSliderStyle; 31 | internal static GUIStyle horizontalSliderThumbStyle; 32 | internal static GUIStyle toggleButtonStyle; 33 | internal static GUIStyle tooltipStyle; 34 | 35 | // Static texture variables 36 | internal static Texture2D windowBackgroundTex; 37 | internal static Texture2D buttonNormalTex; 38 | internal static Texture2D buttonHoverTex; 39 | internal static Texture2D buttonActiveTex; 40 | internal static Texture2D subTabNormalTex; 41 | internal static Texture2D subTabHoverTex; 42 | internal static Texture2D subTabActiveTex; 43 | internal static Texture2D toggleEnabledTex; 44 | internal static Texture2D toggleDisabledTex; 45 | internal static Texture2D tooltipStyleBackgroundTex; 46 | 47 | private static GUISkin originalSkin; 48 | 49 | private static bool stylesInitialized = false; 50 | 51 | internal static MethodInfo displayMessageNotificationMethodGUICache; 52 | 53 | private void Start() 54 | { 55 | stylesInitialized = false; 56 | LoadWindowSettings(); 57 | 58 | var displayMessageNotificationMethodGUI = PatchConstants.EftTypes 59 | .Single(x => x.GetMethod("DisplayMessageNotification") != null) 60 | .GetMethod("DisplayMessageNotification"); 61 | 62 | // Cache the method for later use 63 | displayMessageNotificationMethodGUICache = displayMessageNotificationMethodGUI; 64 | } 65 | 66 | private void OnGUI() 67 | { 68 | if (DefaultPluginVars.showGUI) 69 | { 70 | if (!stylesInitialized) 71 | { 72 | InitializeStyles(); 73 | stylesInitialized = true; 74 | } 75 | 76 | // Save the current GUI skin 77 | originalSkin = GUI.skin; 78 | 79 | windowRect = GUI.Window(123, windowRect, MainWindowFunc, "", windowStyle); 80 | GUI.FocusWindow(123); 81 | 82 | if (Event.current.isMouse) 83 | { 84 | Event.current.Use(); 85 | } 86 | 87 | // Restore the original GUI skin 88 | GUI.skin = originalSkin; 89 | } 90 | } 91 | 92 | public static void InitializeStyles() 93 | { 94 | windowBackgroundTex = MakeTex(1, 1, new Color(0.1f, 0.1f, 0.1f, 1f)); 95 | buttonNormalTex = MakeTex(1, 1, new Color(0.2f, 0.2f, 0.2f, 1f)); 96 | buttonHoverTex = MakeTex(1, 1, new Color(0.3f, 0.3f, 0.3f, 1f)); 97 | buttonActiveTex = MakeTex(1, 1, new Color(0.4f, 0.4f, 0.4f, 1f)); 98 | subTabNormalTex = MakeTex(1, 1, new Color(0.15f, 0.15f, 0.15f, 1f)); 99 | subTabHoverTex = MakeTex(1, 1, new Color(0.2f, 0.2f, 0.5f, 1f)); 100 | subTabActiveTex = MakeTex(1, 1, new Color(0.25f, 0.25f, 0.7f, 1f)); 101 | toggleEnabledTex = MakeTex(1, 1, Color.red); 102 | toggleDisabledTex = MakeTex(1, 1, Color.gray); 103 | tooltipStyleBackgroundTex = MakeTex(1, 1, new Color(0.0f, 0.5f, 1.0f)); 104 | 105 | windowStyle = new GUIStyle(GUI.skin.window) 106 | { 107 | normal = { background = windowBackgroundTex, textColor = Color.white }, 108 | focused = { background = windowBackgroundTex, textColor = Color.white }, 109 | active = { background = windowBackgroundTex, textColor = Color.white }, 110 | hover = { background = windowBackgroundTex, textColor = Color.white }, 111 | onNormal = { background = windowBackgroundTex, textColor = Color.white }, 112 | onFocused = { background = windowBackgroundTex, textColor = Color.white }, 113 | onActive = { background = windowBackgroundTex, textColor = Color.white }, 114 | onHover = { background = windowBackgroundTex, textColor = Color.white }, 115 | }; 116 | 117 | labelStyle = new GUIStyle(GUI.skin.label) 118 | { 119 | normal = { textColor = Color.white }, 120 | fontSize = 20, 121 | fontStyle = FontStyle.Bold, 122 | }; 123 | 124 | buttonStyle = new GUIStyle(GUI.skin.button) 125 | { 126 | normal = { background = buttonNormalTex, textColor = Color.white }, 127 | hover = { background = buttonHoverTex, textColor = Color.white }, 128 | active = { background = buttonActiveTex, textColor = Color.white }, 129 | fontSize = 22, 130 | fontStyle = FontStyle.Bold, 131 | alignment = TextAnchor.MiddleCenter, 132 | }; 133 | 134 | activeButtonStyle = new GUIStyle(buttonStyle) 135 | { 136 | normal = { background = buttonActiveTex, textColor = Color.yellow }, 137 | hover = { background = buttonHoverTex, textColor = Color.yellow }, 138 | active = { background = buttonActiveTex, textColor = Color.yellow }, 139 | }; 140 | 141 | subTabButtonStyle = new GUIStyle(buttonStyle) 142 | { 143 | normal = { background = subTabNormalTex, textColor = Color.white }, 144 | hover = { background = subTabHoverTex, textColor = Color.white }, 145 | active = { background = subTabActiveTex, textColor = Color.white }, 146 | }; 147 | 148 | subTabButtonActiveStyle = new GUIStyle(subTabButtonStyle) 149 | { 150 | normal = { background = subTabActiveTex, textColor = Color.yellow }, 151 | hover = { background = subTabHoverTex, textColor = Color.yellow }, 152 | active = { background = subTabActiveTex, textColor = Color.yellow }, 153 | }; 154 | 155 | textFieldStyle = new GUIStyle(GUI.skin.textField) 156 | { 157 | fontSize = 18, 158 | normal = { textColor = Color.white, background = MakeTex(1, 1, new Color(0.2f, 0.2f, 0.2f, 1f)) } 159 | }; 160 | 161 | horizontalSliderStyle = new GUIStyle(GUI.skin.horizontalSlider); 162 | horizontalSliderThumbStyle = new GUIStyle(GUI.skin.horizontalSliderThumb) 163 | { 164 | normal = { background = MakeTex(1, 1, new Color(0.25f, 0.25f, 0.7f, 1f)) } 165 | }; 166 | 167 | toggleButtonStyle = new GUIStyle(GUI.skin.toggle) 168 | { 169 | normal = { background = toggleDisabledTex, textColor = Color.white }, 170 | onNormal = { background = toggleEnabledTex, textColor = Color.white }, 171 | hover = { background = toggleDisabledTex, textColor = Color.white }, 172 | onHover = { background = toggleEnabledTex, textColor = Color.white }, 173 | active = { background = toggleDisabledTex, textColor = Color.white }, 174 | onActive = { background = toggleEnabledTex, textColor = Color.white }, 175 | focused = { background = toggleDisabledTex, textColor = Color.white }, 176 | onFocused = { background = toggleEnabledTex, textColor = Color.white }, 177 | fontSize = 22, 178 | fontStyle = FontStyle.Bold, 179 | alignment = TextAnchor.MiddleCenter, 180 | padding = new RectOffset(10, 10, 10, 10), 181 | margin = new RectOffset(0, 0, 0, 0) 182 | }; 183 | 184 | tooltipStyle = new GUIStyle(GUI.skin.box) 185 | { 186 | fontSize = 18, 187 | wordWrap = true, 188 | normal = { background = tooltipStyleBackgroundTex, textColor = Color.white }, 189 | fontStyle = FontStyle.Bold 190 | }; 191 | } 192 | 193 | private void Update() 194 | { 195 | // if showing the gui, disable mouseclicks affecting the game 196 | if (DefaultPluginVars.showGUI) 197 | { 198 | Cursor.lockState = CursorLockMode.None; 199 | Cursor.visible = true; 200 | 201 | if (Input.anyKey) 202 | { 203 | Input.ResetInputAxes(); 204 | } 205 | } 206 | } 207 | 208 | private void MainWindowFunc(int windowID) 209 | { 210 | 211 | // Manually draw the window title centered at the top 212 | Rect titleRect = new Rect(0, 0, windowRect.width, 20); 213 | GUI.Label(titleRect, "Donuts Configuration", new GUIStyle(GUI.skin.label) 214 | { 215 | alignment = TextAnchor.MiddleCenter, 216 | fontSize = 20, 217 | fontStyle = FontStyle.Bold, 218 | normal = { textColor = Color.white } 219 | }); 220 | 221 | GUILayout.BeginVertical(); 222 | 223 | scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.ExpandHeight(true)); 224 | DrawMainTabs(); 225 | DrawSelectedTabContent(); 226 | GUILayout.EndScrollView(); 227 | 228 | DrawFooter(); 229 | 230 | GUILayout.EndVertical(); 231 | 232 | HandleWindowDragging(); 233 | HandleWindowResizing(); 234 | 235 | GUI.DragWindow(new Rect(0, 0, windowRect.width, 20)); 236 | } 237 | 238 | private void DrawMainTabs() 239 | { 240 | GUILayout.BeginHorizontal(); 241 | for (int i = 0; i < DefaultPluginVars.tabNames.Length; i++) 242 | { 243 | GUIStyle currentStyle = PluginGUIHelper.buttonStyle; 244 | if (DefaultPluginVars.selectedTabIndex == i) 245 | { 246 | currentStyle = PluginGUIHelper.activeButtonStyle; 247 | } 248 | 249 | if (GUILayout.Button(DefaultPluginVars.tabNames[i], currentStyle)) 250 | { 251 | DefaultPluginVars.selectedTabIndex = i; 252 | } 253 | } 254 | GUILayout.EndHorizontal(); 255 | } 256 | 257 | private void DrawSelectedTabContent() 258 | { 259 | switch (DefaultPluginVars.selectedTabIndex) 260 | { 261 | case 0: 262 | DrawMainSettings.Enable(); 263 | break; 264 | case 1: 265 | DrawSpawnSettings.Enable(); 266 | break; 267 | case 2: 268 | DrawAdvancedSettings.Enable(); 269 | break; 270 | case 3: 271 | DrawSpawnPointMaker.Enable(); 272 | break; 273 | case 4: 274 | DrawDebugging.Enable(); 275 | break; 276 | } 277 | } 278 | 279 | private void DrawFooter() 280 | { 281 | GUILayout.FlexibleSpace(); 282 | GUILayout.BeginHorizontal(); 283 | GUILayout.FlexibleSpace(); 284 | 285 | GUIStyle greenButtonStyle = new GUIStyle(buttonStyle) 286 | { 287 | normal = { background = MakeTex(1, 1, new Color(0.0f, 0.5f, 0.0f)), textColor = Color.white }, 288 | fontSize = 20, 289 | fontStyle = FontStyle.Bold, 290 | alignment = TextAnchor.MiddleCenter 291 | }; 292 | 293 | if (GUILayout.Button("Save All Changes", greenButtonStyle, GUILayout.Width(250), GUILayout.Height(50))) 294 | { 295 | ExportConfig(); 296 | DisplayMessageNotificationGUI("All Donuts Settings have been saved."); 297 | DonutsPlugin.Logger.LogWarning("All changes saved."); 298 | } 299 | 300 | GUILayout.Space(ResizeHandleSize); 301 | 302 | GUILayout.EndHorizontal(); 303 | } 304 | 305 | private void HandleWindowDragging() 306 | { 307 | if (Event.current.type == EventType.MouseDown && new Rect(0, 0, windowRect.width, 20).Contains(Event.current.mousePosition)) 308 | { 309 | isDragging = true; 310 | dragOffset = Event.current.mousePosition; 311 | } 312 | 313 | if (isDragging) 314 | { 315 | if (Event.current.type == EventType.MouseUp) 316 | { 317 | isDragging = false; 318 | } 319 | else if (Event.current.type == EventType.MouseDrag) 320 | { 321 | windowRect.position += (Vector2)Event.current.mousePosition - dragOffset; 322 | } 323 | } 324 | } 325 | 326 | private void HandleWindowResizing() 327 | { 328 | Rect resizeHandleRect = new Rect(windowRect.width - ResizeHandleSize, windowRect.height - ResizeHandleSize, ResizeHandleSize, ResizeHandleSize); 329 | GUI.DrawTexture(resizeHandleRect, Texture2D.whiteTexture); 330 | 331 | if (Event.current.type == EventType.MouseDown && resizeHandleRect.Contains(Event.current.mousePosition)) 332 | { 333 | isResizing = true; 334 | resizeStartPos = Event.current.mousePosition; 335 | Event.current.Use(); 336 | } 337 | 338 | if (isResizing) 339 | { 340 | if (Event.current.type == EventType.MouseUp) 341 | { 342 | isResizing = false; 343 | } 344 | else if (Event.current.type == EventType.MouseDrag) 345 | { 346 | Vector2 delta = Event.current.mousePosition - resizeStartPos; 347 | windowRect.width = Mathf.Max(300, windowRect.width + delta.x); 348 | windowRect.height = Mathf.Max(200, windowRect.height + delta.y); 349 | resizeStartPos = Event.current.mousePosition; 350 | Event.current.Use(); 351 | } 352 | else if (Event.current.type == EventType.MouseMove) 353 | { 354 | Event.current.Use(); 355 | } 356 | } 357 | } 358 | 359 | private void LoadWindowSettings() 360 | { 361 | var dllPath = Assembly.GetExecutingAssembly().Location; 362 | var configDirectory = Path.Combine(Path.GetDirectoryName(dllPath), "Config"); 363 | var configFilePath = Path.Combine(configDirectory, "DefaultPluginVars.json"); 364 | 365 | if (File.Exists(configFilePath)) 366 | { 367 | var json = File.ReadAllText(configFilePath); 368 | DefaultPluginVars.ImportFromJson(json); 369 | windowRect = DefaultPluginVars.windowRect; 370 | } 371 | } 372 | 373 | internal static Texture2D MakeTex(int width, int height, Color col) 374 | { 375 | Color[] pix = new Color[width * height]; 376 | for (int i = 0; i < pix.Length; i++) 377 | { 378 | pix[i] = col; 379 | } 380 | Texture2D result = new Texture2D(width, height); 381 | result.SetPixels(pix); 382 | result.Apply(); 383 | return result; 384 | } 385 | 386 | public static void ExportConfig() 387 | { 388 | var dllPath = Assembly.GetExecutingAssembly().Location; 389 | var configDirectory = Path.Combine(Path.GetDirectoryName(dllPath), "Config"); 390 | var configFilePath = Path.Combine(configDirectory, "DefaultPluginVars.json"); 391 | 392 | if (!Directory.Exists(configDirectory)) 393 | { 394 | Directory.CreateDirectory(configDirectory); 395 | } 396 | DefaultPluginVars.windowRect = windowRect; 397 | string json = DefaultPluginVars.ExportToJson(); 398 | File.WriteAllText(configFilePath, json); 399 | } 400 | 401 | internal static void DisplayMessageNotificationGUI(string message) 402 | { 403 | if (displayMessageNotificationMethodGUICache == null) 404 | { 405 | Debug.LogError("displayMessageNotificationMethodGUICache is not initialized."); 406 | return; 407 | } 408 | 409 | try 410 | { 411 | displayMessageNotificationMethodGUICache.Invoke(null, new object[] { message, ENotificationDurationType.Long, ENotificationIconType.Alert, Color.cyan }); 412 | } 413 | catch (Exception ex) 414 | { 415 | Debug.LogError($"Error invoking DisplayMessageNotification: {ex.Message}\n{ex.StackTrace}"); 416 | } 417 | } 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /Bots/ProfilePreparationComponent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using SPT.PrePatch; 7 | using BepInEx.Logging; 8 | using Comfort.Common; 9 | using Cysharp.Threading.Tasks; 10 | using Donuts.Models; 11 | using EFT; 12 | using HarmonyLib; 13 | using Newtonsoft.Json; 14 | using UnityEngine; 15 | using IProfileData = GClass592; 16 | using System.Threading; 17 | 18 | #pragma warning disable IDE0007, CS4014 19 | 20 | namespace Donuts 21 | { 22 | internal class DonutsBotPrep : MonoBehaviour 23 | { 24 | internal static string selectionName; 25 | internal static string maplocation 26 | { 27 | get 28 | { 29 | if (Singleton.Instance == null) 30 | { 31 | return ""; 32 | } 33 | 34 | string location = Singleton.Instance.MainPlayer.Location.ToLower(); 35 | 36 | // Lazy 37 | if (location == "sandbox_high") 38 | { 39 | location = "sandbox"; 40 | } 41 | 42 | return location; 43 | } 44 | } 45 | 46 | internal static string mapName 47 | { 48 | get 49 | { 50 | switch (maplocation) 51 | { 52 | case "bigmap": 53 | return "customs"; 54 | case "factory4_day": 55 | return "factory"; 56 | case "factory4_night": 57 | return "factory_night"; 58 | case "tarkovstreets": 59 | return "streets"; 60 | case "rezervbase": 61 | return "reserve"; 62 | case "interchange": 63 | return "interchange"; 64 | case "woods": 65 | return "woods"; 66 | case "sandbox": 67 | case "sandbox_high": 68 | return "groundzero"; 69 | case "laboratory": 70 | return "laboratory"; 71 | case "lighthouse": 72 | return "lighthouse"; 73 | case "shoreline": 74 | return "shoreline"; 75 | default: 76 | return maplocation; 77 | } 78 | } 79 | } 80 | 81 | private static GameWorld gameWorld; 82 | private static IBotCreator botCreator; 83 | private static BotSpawner botSpawnerClass; 84 | private static Player mainplayer; 85 | 86 | internal static Dictionary OriginalBotSpawnTypes; 87 | 88 | internal static List botSpawnInfos 89 | { 90 | get; set; 91 | } 92 | 93 | private HashSet usedZonesPMC = new HashSet(); 94 | private HashSet usedZonesSCAV = new HashSet(); 95 | 96 | public static List BotInfos 97 | { 98 | get; set; 99 | } 100 | 101 | public static AllMapsZoneConfig allMapsZoneConfig; 102 | 103 | internal static float timeSinceLastReplenish = 0f; 104 | 105 | private bool isReplenishing = false; 106 | public static bool IsBotPreparationComplete { get; private set; } = false; 107 | 108 | private readonly Dictionary spawnTypeToSideMapping = new Dictionary 109 | { 110 | { WildSpawnType.arenaFighterEvent, EPlayerSide.Savage }, 111 | { WildSpawnType.assault, EPlayerSide.Savage }, 112 | { WildSpawnType.assaultGroup, EPlayerSide.Savage }, 113 | { WildSpawnType.bossBoar, EPlayerSide.Savage }, 114 | { WildSpawnType.bossBoarSniper, EPlayerSide.Savage }, 115 | { WildSpawnType.bossBully, EPlayerSide.Savage }, 116 | { WildSpawnType.bossGluhar, EPlayerSide.Savage }, 117 | { WildSpawnType.bossKilla, EPlayerSide.Savage }, 118 | { WildSpawnType.bossKojaniy, EPlayerSide.Savage }, 119 | { WildSpawnType.bossSanitar, EPlayerSide.Savage }, 120 | { WildSpawnType.bossTagilla, EPlayerSide.Savage }, 121 | { WildSpawnType.bossZryachiy, EPlayerSide.Savage }, 122 | { WildSpawnType.crazyAssaultEvent, EPlayerSide.Savage }, 123 | { WildSpawnType.cursedAssault, EPlayerSide.Savage }, 124 | { WildSpawnType.exUsec, EPlayerSide.Savage }, 125 | { WildSpawnType.followerBoar, EPlayerSide.Savage }, 126 | { WildSpawnType.followerBully, EPlayerSide.Savage }, 127 | { WildSpawnType.followerGluharAssault, EPlayerSide.Savage }, 128 | { WildSpawnType.followerGluharScout, EPlayerSide.Savage }, 129 | { WildSpawnType.followerGluharSecurity, EPlayerSide.Savage }, 130 | { WildSpawnType.followerGluharSnipe, EPlayerSide.Savage }, 131 | { WildSpawnType.followerKojaniy, EPlayerSide.Savage }, 132 | { WildSpawnType.followerSanitar, EPlayerSide.Savage }, 133 | { WildSpawnType.followerTagilla, EPlayerSide.Savage }, 134 | { WildSpawnType.followerZryachiy, EPlayerSide.Savage }, 135 | { WildSpawnType.marksman, EPlayerSide.Savage }, 136 | { WildSpawnType.pmcBot, EPlayerSide.Savage }, 137 | { WildSpawnType.sectantPriest, EPlayerSide.Savage }, 138 | { WildSpawnType.sectantWarrior, EPlayerSide.Savage }, 139 | { WildSpawnType.followerBigPipe, EPlayerSide.Savage }, 140 | { WildSpawnType.followerBirdEye, EPlayerSide.Savage }, 141 | { WildSpawnType.bossKnight, EPlayerSide.Savage }, 142 | }; 143 | 144 | internal static ManualLogSource Logger 145 | { 146 | get; private set; 147 | } 148 | 149 | public DonutsBotPrep() 150 | { 151 | Logger ??= BepInEx.Logging.Logger.CreateLogSource(nameof(DonutsBotPrep)); 152 | } 153 | 154 | public static void Enable() 155 | { 156 | gameWorld = Singleton.Instance; 157 | gameWorld.GetOrAddComponent(); 158 | 159 | Logger.LogDebug("DonutBotPrep Enabled"); 160 | } 161 | 162 | public async void Awake() 163 | { 164 | var playerLoop = UnityEngine.LowLevel.PlayerLoop.GetCurrentPlayerLoop(); 165 | Cysharp.Threading.Tasks.PlayerLoopHelper.Initialize(ref playerLoop); 166 | 167 | botSpawnerClass = Singleton.Instance.BotsController.BotSpawner; 168 | botCreator = AccessTools.Field(typeof(BotSpawner), "_botCreator").GetValue(botSpawnerClass) as IBotCreator; 169 | mainplayer = gameWorld?.MainPlayer; 170 | OriginalBotSpawnTypes = new Dictionary(); 171 | BotInfos = new List(); 172 | botSpawnInfos = new List(); 173 | timeSinceLastReplenish = 0; 174 | IsBotPreparationComplete = false; 175 | 176 | botSpawnerClass.OnBotRemoved += BotSpawnerClass_OnBotRemoved; 177 | botSpawnerClass.OnBotCreated += BotSpawnerClass_OnBotCreated; 178 | 179 | if (mainplayer != null) 180 | { 181 | Logger.LogDebug("Mainplayer is not null, attaching event handlers"); 182 | mainplayer.BeingHitAction += Mainplayer_BeingHitAction; 183 | } 184 | 185 | // Get selected preset and setup bot limits now 186 | selectionName = DonutsPlugin.RunWeightedScenarioSelection(); 187 | Initialization.SetupBotLimit(selectionName); 188 | 189 | var startingBotConfig = DonutComponent.GetStartingBotConfig(selectionName); 190 | if (startingBotConfig != null) 191 | { 192 | Logger.LogDebug("startingBotConfig is not null: " + JsonConvert.SerializeObject(startingBotConfig)); 193 | 194 | allMapsZoneConfig = AllMapsZoneConfig.LoadFromDirectory(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "zoneSpawnPoints")); 195 | 196 | if (allMapsZoneConfig == null) 197 | { 198 | Logger.LogError("Failed to load AllMapsZoneConfig."); 199 | return; 200 | } 201 | 202 | if (string.IsNullOrEmpty(maplocation)) 203 | { 204 | Logger.LogError("Map location is null or empty."); 205 | return; 206 | } 207 | 208 | await InitializeAllBotInfos(startingBotConfig, maplocation, cancellationToken: this.GetCancellationTokenOnDestroy()); 209 | } 210 | else 211 | { 212 | Logger.LogError("startingBotConfig is null for selectionName: " + selectionName); 213 | } 214 | 215 | IsBotPreparationComplete = true; 216 | } 217 | 218 | private void BotSpawnerClass_OnBotRemoved(BotOwner bot) 219 | { 220 | bot.Memory.OnGoalEnemyChanged -= Memory_OnGoalEnemyChanged; 221 | OriginalBotSpawnTypes.Remove(bot.Profile.Id); 222 | } 223 | private void BotSpawnerClass_OnBotCreated(BotOwner bot) 224 | { 225 | bot.Memory.OnGoalEnemyChanged += Memory_OnGoalEnemyChanged; 226 | } 227 | private void Memory_OnGoalEnemyChanged(BotOwner owner) 228 | { 229 | if (owner != null && owner.Memory != null && owner.Memory.GoalEnemy != null && owner.Memory.HaveEnemy) 230 | { 231 | if (owner.Memory.GoalEnemy.Person == (IPlayer)gameWorld.MainPlayer.InteractablePlayer && owner.Memory.GoalEnemy.HaveSeenPersonal && owner.Memory.GoalEnemy.IsVisible) 232 | { 233 | timeSinceLastReplenish = 0f; 234 | } 235 | } 236 | } 237 | 238 | private void Mainplayer_BeingHitAction(DamageInfo arg1, EBodyPart arg2, float arg3) 239 | { 240 | switch (arg1.DamageType) 241 | { 242 | case EDamageType.Btr: 243 | case EDamageType.Melee: 244 | case EDamageType.Bullet: 245 | case EDamageType.Explosion: 246 | case EDamageType.GrenadeFragment: 247 | case EDamageType.Sniper: 248 | timeSinceLastReplenish = 0f; 249 | break; 250 | default: 251 | break; 252 | } 253 | } 254 | 255 | private async UniTask InitializeAllBotInfos(StartingBotConfig startingBotConfig, string maplocation, CancellationToken cancellationToken) 256 | { 257 | await UniTask.WhenAll( 258 | InitializeBotInfos(startingBotConfig, maplocation, "PMC", cancellationToken), 259 | InitializeBotInfos(startingBotConfig, maplocation, "SCAV", cancellationToken) 260 | ); 261 | } 262 | 263 | private async UniTask InitializeBotInfos(StartingBotConfig startingBotConfig, string maplocation, string botType, CancellationToken cancellationToken) 264 | { 265 | botType = DefaultPluginVars.forceAllBotType.Value switch 266 | { 267 | "PMC" => "PMC", 268 | "SCAV" => "SCAV", 269 | _ => botType 270 | }; 271 | 272 | string difficultySetting = botType == "PMC" ? DefaultPluginVars.botDifficultiesPMC.Value.ToLower() : DefaultPluginVars.botDifficultiesSCAV.Value.ToLower(); 273 | maplocation = maplocation == "sandbox_high" ? "sandbox" : maplocation; 274 | var mapBotConfig = botType == "PMC" ? startingBotConfig.Maps[maplocation].PMC : startingBotConfig.Maps[maplocation].SCAV; 275 | var difficultiesForSetting = GetDifficultiesForSetting(difficultySetting); 276 | int maxBots = UnityEngine.Random.Range(mapBotConfig.MinCount, mapBotConfig.MaxCount + 1); 277 | maxBots = botType switch 278 | { 279 | "PMC" when maxBots > Initialization.PMCBotLimit => Initialization.PMCBotLimit, 280 | "SCAV" when maxBots > Initialization.SCAVBotLimit => Initialization.SCAVBotLimit, 281 | _ => maxBots 282 | }; 283 | 284 | Logger.LogDebug($"Max starting bots for {botType}: {maxBots}"); 285 | 286 | var spawnPointsDict = DonutComponent.GetSpawnPointsForZones(allMapsZoneConfig, maplocation, mapBotConfig.Zones); 287 | 288 | int totalBots = 0; 289 | var usedZones = botType == "PMC" ? usedZonesPMC : usedZonesSCAV; 290 | var random = new System.Random(); 291 | 292 | while (totalBots < maxBots) 293 | { 294 | int groupSize = BotSpawn.DetermineMaxBotCount(botType.ToLower(), mapBotConfig.MinGroupSize, mapBotConfig.MaxGroupSize); 295 | groupSize = Math.Min(groupSize, maxBots - totalBots); 296 | 297 | var wildSpawnType = botType == "PMC" ? GetPMCWildSpawnType(WildSpawnType.pmcUSEC, WildSpawnType.pmcBEAR) : WildSpawnType.assault; 298 | var side = botType == "PMC" ? GetPMCSide(wildSpawnType, WildSpawnType.pmcUSEC, WildSpawnType.pmcBEAR) : EPlayerSide.Savage; 299 | 300 | var difficulty = difficultiesForSetting[UnityEngine.Random.Range(0, difficultiesForSetting.Count)]; 301 | 302 | var zoneKeys = spawnPointsDict.Keys.OrderBy(_ => random.Next()).ToList(); 303 | string selectedZone = zoneKeys.FirstOrDefault(z => !usedZones.Contains(z)); 304 | 305 | if (selectedZone == null) 306 | { 307 | usedZones.Clear(); 308 | selectedZone = zoneKeys.First(); 309 | } 310 | 311 | var coordinates = spawnPointsDict[selectedZone].OrderBy(_ => random.Next()).ToList(); 312 | usedZones.Add(selectedZone); 313 | 314 | var botInfo = new PrepBotInfo(wildSpawnType, difficulty, side, groupSize > 1, groupSize); 315 | await CreateBot(botInfo, botInfo.IsGroup, botInfo.GroupSize, cancellationToken); 316 | BotInfos.Add(botInfo); 317 | 318 | var botSpawnInfo = new BotSpawnInfo(wildSpawnType, groupSize, coordinates, difficulty, side, selectedZone); 319 | botSpawnInfos.Add(botSpawnInfo); 320 | 321 | totalBots += groupSize; 322 | } 323 | } 324 | 325 | private WildSpawnType GetPMCWildSpawnType(WildSpawnType sptUsec, WildSpawnType sptBear) 326 | { 327 | switch (DefaultPluginVars.pmcFaction.Value) 328 | { 329 | case "USEC": 330 | return WildSpawnType.pmcUSEC; 331 | case "BEAR": 332 | return WildSpawnType.pmcBEAR; 333 | default: 334 | return BotSpawn.DeterminePMCFactionBasedOnRatio(sptUsec, sptBear); 335 | } 336 | } 337 | 338 | private EPlayerSide GetPMCSide(WildSpawnType wildSpawnType, WildSpawnType sptUsec, WildSpawnType sptBear) 339 | { 340 | switch (wildSpawnType) 341 | { 342 | case WildSpawnType.pmcUSEC: 343 | return EPlayerSide.Usec; 344 | case WildSpawnType.pmcBEAR: 345 | return EPlayerSide.Bear; 346 | default: 347 | return EPlayerSide.Usec; 348 | } 349 | } 350 | 351 | private List GetDifficultiesForSetting(string difficultySetting) 352 | { 353 | switch (difficultySetting) 354 | { 355 | case "asonline": 356 | return new List { BotDifficulty.easy, BotDifficulty.normal, BotDifficulty.hard }; 357 | case "easy": 358 | return new List { BotDifficulty.easy }; 359 | case "normal": 360 | return new List { BotDifficulty.normal }; 361 | case "hard": 362 | return new List { BotDifficulty.hard }; 363 | case "impossible": 364 | return new List { BotDifficulty.impossible }; 365 | default: 366 | Logger.LogError("Unsupported difficulty setting: " + difficultySetting); 367 | return new List(); 368 | } 369 | } 370 | 371 | private void Update() 372 | { 373 | timeSinceLastReplenish += Time.deltaTime; 374 | if (timeSinceLastReplenish >= DefaultPluginVars.replenishInterval.Value && !isReplenishing) 375 | { 376 | timeSinceLastReplenish = 0f; 377 | ReplenishAllBots(this.GetCancellationTokenOnDestroy()).Forget(); 378 | } 379 | } 380 | 381 | private async UniTask ReplenishAllBots(CancellationToken cancellationToken) 382 | { 383 | isReplenishing = true; 384 | 385 | var tasks = new List(); 386 | var botsNeedingReplenishment = BotInfos.Where(NeedReplenishment).ToList(); 387 | 388 | int singleBotsCount = 0; 389 | int groupBotsCount = 0; 390 | 391 | foreach (var botInfo in botsNeedingReplenishment) 392 | { 393 | if (botInfo.IsGroup && groupBotsCount < 1) 394 | { 395 | #if DEBUG 396 | Logger.LogWarning($"Replenishing group bot: {botInfo.SpawnType} {botInfo.Difficulty} {botInfo.Side} Count: {botInfo.GroupSize}"); 397 | #endif 398 | tasks.Add(CreateBot(botInfo, true, botInfo.GroupSize, cancellationToken)); 399 | groupBotsCount++; 400 | } 401 | else if (!botInfo.IsGroup && singleBotsCount < 3) 402 | { 403 | #if DEBUG 404 | Logger.LogWarning($"Replenishing single bot: {botInfo.SpawnType} {botInfo.Difficulty} {botInfo.Side} Count: 1"); 405 | #endif 406 | tasks.Add(CreateBot(botInfo, false, 1, cancellationToken)); 407 | singleBotsCount++; 408 | } 409 | 410 | if (singleBotsCount >= 3 && groupBotsCount >= 1) 411 | break; 412 | } 413 | 414 | if (tasks.Count > 0) 415 | { 416 | await UniTask.WhenAll(tasks); 417 | } 418 | 419 | isReplenishing = false; 420 | } 421 | 422 | private static bool NeedReplenishment(PrepBotInfo botInfo) 423 | { 424 | return botInfo.Bots == null || botInfo.Bots.Profiles.Count == 0; 425 | } 426 | 427 | internal static async UniTask CreateBot(PrepBotInfo botInfo, bool isGroup, int groupSize, CancellationToken cancellationToken) 428 | { 429 | var botData = new IProfileData(botInfo.Side, botInfo.SpawnType, botInfo.Difficulty, 0f, null); 430 | #if DEBUG 431 | Logger.LogDebug($"Creating bot: Type={botInfo.SpawnType}, Difficulty={botInfo.Difficulty}, Side={botInfo.Side}, GroupSize={groupSize}"); 432 | #endif 433 | BotCreationDataClass bot = await BotCreationDataClass.Create(botData, botCreator, groupSize, botSpawnerClass); 434 | if (bot == null || bot.Profiles == null || !bot.Profiles.Any()) 435 | { 436 | #if DEBUG 437 | Logger.LogError($"Failed to create or properly initialize bot for {botInfo.SpawnType}"); 438 | #endif 439 | return; 440 | } 441 | 442 | botInfo.Bots = bot; 443 | #if DEBUG 444 | Logger.LogDebug($"Bot created and assigned successfully: {bot.Profiles.Count} profiles loaded."); 445 | #endif 446 | } 447 | 448 | public static BotCreationDataClass FindCachedBots(WildSpawnType spawnType, BotDifficulty difficulty, int targetCount) 449 | { 450 | if (DonutsBotPrep.BotInfos == null) 451 | { 452 | Logger.LogError("BotInfos is null"); 453 | return null; 454 | } 455 | 456 | try 457 | { 458 | // Find the bot info that matches the spawn type and difficulty 459 | var botInfo = DonutsBotPrep.BotInfos.FirstOrDefault(b => b.SpawnType == spawnType && b.Difficulty == difficulty && b.Bots != null && b.Bots.Profiles.Count == targetCount); 460 | 461 | if (botInfo != null) 462 | { 463 | return botInfo.Bots; 464 | } 465 | 466 | Logger.LogWarning($"No cached bots found for spawn type {spawnType}, difficulty {difficulty}, and target count {targetCount}"); 467 | return null; 468 | } 469 | catch (Exception ex) 470 | { 471 | Logger.LogError($"Exception in FindCachedBots: {ex.Message}\n{ex.StackTrace}"); 472 | return null; 473 | } 474 | } 475 | 476 | public static List GetWildSpawnData(WildSpawnType spawnType, BotDifficulty botDifficulty) 477 | { 478 | return BotInfos 479 | .Where(b => b.SpawnType == spawnType && b.Difficulty == botDifficulty) 480 | .Select(b => b.Bots) 481 | .ToList(); 482 | } 483 | 484 | internal static WildSpawnType? GetOriginalSpawnTypeForBot(BotOwner bot) 485 | { 486 | var originalProfile = OriginalBotSpawnTypes.First(profile => profile.Key == bot.Profile.Id); 487 | 488 | if (originalProfile.Key != null) 489 | { 490 | #if DEBUG 491 | Logger.LogWarning("Found original profile for bot " + bot.Profile.Nickname + " as " + originalProfile.Value.ToString()); 492 | #endif 493 | return originalProfile.Value; 494 | } 495 | else 496 | { 497 | #if DEBUG 498 | Logger.LogWarning("Could not find original profile for bot " + bot.Profile.Nickname); 499 | #endif 500 | return null; 501 | } 502 | } 503 | 504 | private void OnDestroy() 505 | { 506 | if (botSpawnerClass != null) 507 | { 508 | botSpawnerClass.OnBotRemoved -= BotSpawnerClass_OnBotRemoved; 509 | botSpawnerClass.OnBotCreated -= BotSpawnerClass_OnBotCreated; 510 | } 511 | 512 | if (mainplayer != null) 513 | { 514 | mainplayer.BeingHitAction -= Mainplayer_BeingHitAction; 515 | } 516 | 517 | isReplenishing = false; 518 | timeSinceLastReplenish = 0; 519 | IsBotPreparationComplete = false; 520 | 521 | gameWorld = null; 522 | botCreator = null; 523 | botSpawnerClass = null; 524 | mainplayer = null; 525 | OriginalBotSpawnTypes = null; 526 | BotInfos = null; 527 | botSpawnInfos = null; 528 | 529 | #if DEBUG 530 | Logger.LogWarning("DonutsBotPrep component cleaned up and disabled."); 531 | #endif 532 | } 533 | } 534 | } 535 | --------------------------------------------------------------------------------