using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using Comfort.Common; using EFT; using EFT.UI.Settings; using System; using System.Collections; using UnityEngine; //Assembly-CSharp.dll //Aki.Reflection.dll //BepInEx.dll //Comfort.dll //UnityEngine.dll //UnityEngine.CoreModule.dll namespace notGreg.UniformAim { [BepInPlugin("com.notGreg.UniformAim", "notGreg's Uniform Aim for Tarkov", "3.5.0")] [BepInDependency("RealismMod", BepInDependency.DependencyFlags.SoftDependency)] public class Plugin : BaseUnityPlugin { ConfigEntry configExponent; ConfigEntry configSens; public static bool isRealismModPresent = Chainloader.PluginInfos.ContainsKey("RealismMod"); void Awake() { configExponent = Config.Bind("General", "Coefficient", 133, new ConfigDescription("", new AcceptableValueRange(10, 500))); configSens = Config.Bind("General", "Sensitivity", 1.0f, new ConfigDescription("", new AcceptableValueRange(0.01f, 2.0f))); if (!isRealismModPresent) { new get_AimingSensitivityPatch().Enable(); } else { Logger.LogInfo("RealismMod detected! Abandoning aimingSens patch...\nMake sure to use the compatibility plugin!"); } } Player mainPlayer; int inGameFOV; float inGameAimedSens; public static float aimingSens = 1.0f; //this function can be replaced by FixedUpdate() at 50Hz (adjustable via Time.fixedDeltaTime) //FixedUpdate() proved to be slightly more reliable in the past, however it had other unintended effects on the game. void FixedUpdate() { //check if the world instance exists //Logger.LogInfo("Checking world instance"); if (Singleton.Instance == null) { return; } //grab the game status of the existing instance //Logger.LogInfo("Checking game status"); GameStatus currentGameStatus = Singleton.Instance.Status; //check if the game has started and if the player exists. If the player is aiming, update the scoped sensitivity (executed every frame while aiming) //Logger.LogInfo("Checking GameStatus and isAiming"); //Logger.LogInfo($"GameStatus: {currentGameStatus}"); if (currentGameStatus == GameStatus.Started && mainPlayer != null) { //Logger.LogInfo($"isAiming? {mainPlayer.HandsController.IsAiming}"); if (mainPlayer.HandsController.IsAiming) { aimingSens = calculateSensitivity(); } return; } //Logger.LogInfo("Switch on GameStatus"); switch (currentGameStatus) { case GameStatus.Started: { mainPlayer = getLocalPlayer(); //Logger.LogInfo($"Subscribing to onAimingChanged event"); subscribeOnAimingChanged(); //Logger.LogInfo("TryGetCameras coroutines"); StartCoroutine(tryGetMainCamera()); StartCoroutine(tryGetScopeCamera()); break; } case GameStatus.SoftStopping: case GameStatus.Stopped: { mainPlayer = null; break; } default: { break; } } } //this function grabs the Field of View from the settings. This is the function that most often needs patching after update. //Appropriate class can usually be found by searching for the ClearSettings function. //SharedGameSettingsClass settingsLibrary = SharedGameSettingsClass.Instance; // Futureproofing for 3.5.1 and onwards int getInGameFOV() { //int fov = Singleton.Instance.Game.Settings.FieldOfView; //SPT-AKI 3.2.3 //int fov = Singleton.Instance.Game.Settings.FieldOfView; //SPT-AKI 3.3.0 //int fov = Singleton.Instance.Game.Settings.FieldOfView; //SPT-AKI 3.4.1 int fov = Singleton.Instance.Game.Settings.FieldOfView; //SPT-AKI 3.5.0 //int fov = settingsLibrary.Game.Settings.FieldOfView; return fov; } //this function grabs the Aiming Sensitivity from the settings. This is the function that most often needs patching after update. //Appropriate class can usually be found by searching for the ClearSettings function. float getInGameAimSens() { //float sens = Singleton.Instance.Control.Settings.MouseAimingSensitivity; //SPT-AKI 3.2.* //float sens = Singleton.Instance.Control.Settings.MouseAimingSensitivity; //SPT-AKI 3.3.0 //float sens = Singleton.Instance.Control.Settings.MouseAimingSensitivity; //SPT-AKI 3.4.1 float sens = Singleton.Instance.Control.Settings.MouseAimingSensitivity; //SPT-AKI 3.5.0 //float sens = settingsLibrary.Control.Settings.MouseAimingSensitivity; //Logger.LogInfo($"In-game AimSens: {sens}"); return sens; } Player getLocalPlayer() { //Logger.LogInfo("Setting local player..."); return Singleton.Instance.RegisteredPlayers.Find(p => p.IsYourPlayer); } WaitForSecondsRealtime myDelay = new WaitForSecondsRealtime(1f); Camera mainCamera; Camera scopeCamera; //this coroutine attempts to find the FPS Camera in the scene. IEnumerator tryGetMainCamera() { string cameraName = "FPS Camera"; if (GameObject.Find(cameraName) != null) { mainCamera = GameObject.Find(cameraName).GetComponent(); //Logger.LogInfo($"{mainCamera.name} found!"); } else { //Logger.LogMessage($"Camera \"{cameraName}\" not found, rescheduling..."); yield return myDelay; StartCoroutine(tryGetMainCamera()); yield break; } yield return null; } //this coroutine attempts to find existing baseOpticCamera in the scene. The state of this camera is used to determine whether the player is using a magnified optic or not. IEnumerator tryGetScopeCamera() { string cameraName = "BaseOpticCamera(Clone)"; if (GameObject.Find(cameraName) != null) { scopeCamera = GameObject.Find(cameraName).GetComponent(); //Logger.LogInfo($"{scopeCamera.name} found!"); } yield break; } //figure out whether the player is using a magnified optic or not. Return the FOV of the sight. Camera determineCurrentAimedFOV() { //Logger.LogInfo("Get current aiming mod"); //Logger.LogInfo($"MainPlayer {mainPlayer.name}"); //Logger.LogInfo($"ProcWeapAnim {mainPlayer.ProceduralWeaponAnimation}"); //Logger.LogInfo($"CurrAimMod {mainPlayer.ProceduralWeaponAnimation.CurrentAimingMod}"); if (mainPlayer.ProceduralWeaponAnimation.CurrentAimingMod == null) { return mainCamera; } var currentAimingMod = mainPlayer.ProceduralWeaponAnimation.CurrentAimingMod; //get the name of the currently active scope //string scopeName = currentAimingMod.Item.Template.Name; string scopeName = currentAimingMod.Item.Name; //Logger.LogInfo($"Scope name: {scopeName}"); //scopeMode determines the scope being used (e.g. main magnified optic at index 0, backup RDS at index 1) //scopeIndex determines the mode of the scope being used (e.g. x1 magnification at index 0, x4 magnification at index 1) //there are exceptions to this rule thanks to BSG's inconsistency, some of them are patched below var scopeIndex = currentAimingMod.SelectedScopeIndex; var scopeMode = currentAimingMod.SelectedScopeMode; //Logger.LogInfo("Index: " + scopeIndex + "\nMode: " + scopeMode); //patches for specific scopes, matches item name switch (scopeName) { case "tactical_mp155_kalashnikov_ultima_camera": { //Logger.LogInfo("MP-155 Thermal"); return mainCamera; } //SAM-SWAT's Leupold D-Evo optic patch. The modes on this scope are reversed. Causes detection based on baseOpticCamera to fail as the magnified optic camera is always active case "scope_leupold_d_evo": { if (scopeMode == 0) { //Logger.LogInfo($"Leupold D-EVO BUIS FOV: {mainCamera.fieldOfView}"); return mainCamera; } else { //Logger.LogInfo($"Leupold D-EVO Scope FOV: {scopeCamera.fieldOfView}"); return scopeCamera; } } case "scope_base_trijicon_acog_ta11_3,5x35": { if (scopeIndex == 1) { //Logger.LogInfo($"G36 Rail sight"); return mainCamera; } else { return scopeCamera; } } default: { if (scopeCamera == null || scopeCamera.isActiveAndEnabled == false) { //Logger.LogInfo($"Non-magnified: {scopeName} FOV: {mainCamera.fieldOfView}"); return mainCamera; } else { //Logger.LogInfo($"Magnified: {scopeName} FOV: {scopeCamera.fieldOfView}"); return scopeCamera; } } } } float calculateSensitivity() { //Logger.LogInfo("calculateSensitivity()"); //grab vertical hipfire field of view (FOV set by the user in the setting), then convert it to horizontal FOV //convert degrees to radians float hipFOV = Mathf.Deg2Rad * Camera.VerticalToHorizontalFieldOfView(inGameFOV, mainCamera.aspect); //grab current field of view while aiming, then convert it to horizontal FOV //convert degrees to radians float aimedFOV = Mathf.Deg2Rad * Camera.VerticalToHorizontalFieldOfView(determineCurrentAimedFOV().fieldOfView, mainCamera.aspect); //exponent applied to the ratio of aimedFOV to hipFOV, causes sights to become relatively faster or slower as zoom increases float exponent = 100f / configExponent.Value; float tanRatio = (float)(Mathf.Tan(aimedFOV / 2) / Mathf.Tan(hipFOV / 2)); float sensitivity = (float)Math.Pow(tanRatio, exponent) * inGameAimedSens; //Logger.LogInfo($"Sensitivity: {sensitivity}"); return sensitivity * configSens.Value; } void subscribeOnAimingChanged() { //HandsChangedEvent triggers whenever player changes weapons //without it the patch would cease to work as expected when weapons were changed mainPlayer.HandsChangedEvent += (handsArgs) => { //onAimingChanged triggers whenever the player starts or stops aiming. mainPlayer.HandsController.OnAimingChanged += (aimArgs) => { inGameFOV = getInGameFOV(); inGameAimedSens = getInGameAimSens(); StartCoroutine(tryGetScopeCamera()); }; }; } } }