diff --git a/project/Terkoiz.Freecam/Freecam.cs b/project/Terkoiz.Freecam/Freecam.cs index 8125da9..b9d11fc 100644 --- a/project/Terkoiz.Freecam/Freecam.cs +++ b/project/Terkoiz.Freecam/Freecam.cs @@ -1,4 +1,5 @@ -using UnityEngine; +using JetBrains.Annotations; +using UnityEngine; namespace Terkoiz.Freecam { @@ -13,7 +14,8 @@ namespace Terkoiz.Freecam { public bool IsActive = false; - void Update() + [UsedImplicitly] + public void Update() { if (!IsActive) { @@ -43,35 +45,41 @@ namespace Terkoiz.Freecam transform.position += (-transform.forward * movementSpeed * Time.deltaTime); } - if (Input.GetKey(KeyCode.Q)) + if (FreecamPlugin.CameraHeightMovement.Value) { - transform.position += (transform.up * movementSpeed * Time.deltaTime); - } + if (Input.GetKey(KeyCode.Q)) + { + transform.position += (transform.up * movementSpeed * Time.deltaTime); + } - if (Input.GetKey(KeyCode.E)) - { - transform.position += (-transform.up * movementSpeed * Time.deltaTime); - } + if (Input.GetKey(KeyCode.E)) + { + transform.position += (-transform.up * movementSpeed * Time.deltaTime); + } - if (Input.GetKey(KeyCode.R) || Input.GetKey(KeyCode.PageUp)) - { - transform.position += (Vector3.up * movementSpeed * Time.deltaTime); - } + if (Input.GetKey(KeyCode.R) || Input.GetKey(KeyCode.PageUp)) + { + transform.position += (Vector3.up * movementSpeed * Time.deltaTime); + } - if (Input.GetKey(KeyCode.F) || Input.GetKey(KeyCode.PageDown)) - { - transform.position += (-Vector3.up * movementSpeed * Time.deltaTime); + if (Input.GetKey(KeyCode.F) || Input.GetKey(KeyCode.PageDown)) + { + transform.position += (-Vector3.up * movementSpeed * Time.deltaTime); + } } float newRotationX = transform.localEulerAngles.y + Input.GetAxis("Mouse X") * FreecamPlugin.CameraLookSensitivity.Value; float newRotationY = transform.localEulerAngles.x - Input.GetAxis("Mouse Y") * FreecamPlugin.CameraLookSensitivity.Value; transform.localEulerAngles = new Vector3(newRotationY, newRotationX, 0f); - float axis = Input.GetAxis("Mouse ScrollWheel"); - if (axis != 0) + if (FreecamPlugin.CameraMousewheelZoom.Value) { - var zoomSensitivity = fastMode ? FreecamPlugin.CameraFastZoomSpeed.Value : FreecamPlugin.CameraZoomSpeed.Value; - transform.position += transform.forward * axis * zoomSensitivity; + float axis = Input.GetAxis("Mouse ScrollWheel"); + if (axis != 0) + { + var zoomSensitivity = fastMode ? FreecamPlugin.CameraFastZoomSpeed.Value : FreecamPlugin.CameraZoomSpeed.Value; + transform.position += transform.forward * axis * zoomSensitivity; + } } } } diff --git a/project/Terkoiz.Freecam/FreecamController.cs b/project/Terkoiz.Freecam/FreecamController.cs index a2a54e4..e48d1b0 100644 --- a/project/Terkoiz.Freecam/FreecamController.cs +++ b/project/Terkoiz.Freecam/FreecamController.cs @@ -1,23 +1,55 @@ -using Comfort.Common; +using System.Reflection; +using Comfort.Common; using EFT; +using EFT.CameraControl; using EFT.UI; +using HarmonyLib; +using JetBrains.Annotations; +using MonoMod.RuntimeDetour; using UnityEngine; namespace Terkoiz.Freecam { public class FreecamController : MonoBehaviour { - private GameObject mainCamera; - private Freecam freeCamScript; + private GameObject _mainCamera; + private Freecam _freeCamScript; - private BattleUIScreen playerUi; - private bool uiHidden; + private BattleUIScreen _playerUi; + private bool _uiHidden; - // TODO MAYBE: - // Hide player weapon + private MethodInfo _playerMoveMethod; + private MethodInfo _playerRotateMethod; + private Detour _moveDetour; + private Detour _rotateDetour; + + private Vector3? _lastPosition; + private Quaternion? _lastRotation; + + // TODO: // Hide version number UI element - // FreeCam controller support (camera could be smoother with an analog stick, apparently) + // Button to toggle between camera and player movement + // Independent FoV setting for Freecam mode (_mainCamera.GetComponent().fieldOfView = ...) + + [UsedImplicitly] + public void Start() + { + // We locate and get the MethodInfo for the Move method that we will later detour to prevent the player from moving during Freecam + _playerMoveMethod = typeof(Player).GetMethod("Move"); + if (_playerMoveMethod == null) + { + FreecamPlugin.Logger.LogError("Failed to locate the Player.Move method!"); + } + + // Same deal here, but for player rotation + _playerRotateMethod = typeof(Player).GetMethod("Rotate"); + if (_playerRotateMethod == null) + { + FreecamPlugin.Logger.LogError("Failed to locate the Player.Rotate method!"); + } + } + [UsedImplicitly] public void Update() { if (FreecamPlugin.ToggleUi.Value.IsDown()) @@ -49,36 +81,33 @@ namespace Terkoiz.Freecam } // If we don't have the main camera object cached, go look for it in the scene - if (mainCamera == null) + if (_mainCamera == null) { - // Finding a GameObject by name directly is apparantly better than searching for objects of a type. + // Finding a GameObject by name directly is apparently better than searching for objects of a type. // 'FPS Camera' is our main camera instance - so we just grab that - mainCamera = GameObject.Find("FPS Camera"); - if (mainCamera == null) + _mainCamera = GameObject.Find("FPS Camera"); + if (_mainCamera == null) { - Debug.LogError("Terkoiz.Freecam: ERROR: Failed to locate main camera"); + FreecamPlugin.Logger.LogError("Failed to locate main camera"); PreloaderUI.Instance.Console.AddLog("ERROR: Failed to locate main camera", "FreeCam"); return; } } // Create the Freecam script and attach it to the main camera - if (freeCamScript == null) + if (_freeCamScript == null) { - freeCamScript = mainCamera.AddComponent(); + ClearDetours(); + _freeCamScript = _mainCamera.AddComponent(); } - // We disable the player's GameObject, which prevents the player from moving and interacting with the world while we're in Freecam mode - // Also, since the camera position keeps getting updated to where the player is, disabling the player's GameObject also stops this behaviour - if (!freeCamScript.IsActive) + if (!_freeCamScript.IsActive) { - localPlayer.gameObject.SetActive(false); - freeCamScript.IsActive = true; + SetPlayerToFreecamMode(localPlayer); } else { - freeCamScript.IsActive = false; - localPlayer.gameObject.SetActive(true); + SetPlayerToFirstPersonMode(localPlayer); } } @@ -94,19 +123,19 @@ namespace Terkoiz.Freecam } // If we don't have the main camera cached, it means we have yet to enter Freecam mode and there is nowhere to teleport to - if (mainCamera == null) + if (_mainCamera == null) { return; } - // We basically do what ToggleCamera() does, but with extra code inbetween - if (freeCamScript.IsActive) + // Move the player to the camera's current position and switch to First Person mode + if (_freeCamScript.IsActive) { - freeCamScript.IsActive = false; // We grab the camera's position, but we subtract a bit off the Y axis, because the players coordinate origin is at the feet - var position = new Vector3(mainCamera.transform.position.x, mainCamera.transform.position.y - 1.8f, mainCamera.transform.position.z); - localPlayer.gameObject.transform.SetPositionAndRotation(position, mainCamera.transform.rotation); - localPlayer.gameObject.SetActive(true); + var position = new Vector3(_mainCamera.transform.position.x, _mainCamera.transform.position.y - 1.8f, _mainCamera.transform.position.z); + localPlayer.gameObject.transform.SetPositionAndRotation(position, Quaternion.Euler(0, _mainCamera.transform.rotation.y, 0)); + // localPlayer.gameObject.transform.SetPositionAndRotation(position, _mainCamera.transform.rotation); + SetPlayerToFirstPersonMode(localPlayer); } } @@ -122,26 +151,97 @@ namespace Terkoiz.Freecam } // If we don't have the UI Component cached, go look for it in the scene - if (playerUi == null) + if (_playerUi == null) { - playerUi = GameObject.Find("BattleUIScreen").GetComponent(); + _playerUi = GameObject.Find("BattleUIScreen").GetComponent(); - if (playerUi == null) + if (_playerUi == null) { + FreecamPlugin.Logger.LogError("Failed to locate player UI"); PreloaderUI.Instance.Console.AddLog("ERROR: Failed to locate player UI", "FreeCam"); return; } } - playerUi.gameObject.SetActive(uiHidden); - uiHidden = !uiHidden; + _playerUi.gameObject.SetActive(_uiHidden); + _uiHidden = !_uiHidden; + } + + /// + /// A helper method to set the Player into Freecam mode + /// + /// + private void SetPlayerToFreecamMode(Player localPlayer) + { + // We set the player to third person mode, but then we want set the camera to freecam mode + // This means our character will be fully visible, while letting the camera move freely + // Setting the player point of view directly to Freecam seems to hide the head and arms of the character, which is not desirable + + localPlayer.PointOfView = EPointOfView.ThirdPerson; + + // Get the PlayerBody reference. It's a protected field, so we have to use traverse to fetch it + var playerBody = Traverse.Create(localPlayer).Field("_playerBody").Value; + if (playerBody != null) + { + // We tell the PlayerBody class that it's in FreeCamera mode, and force an update of the Camera Controller view mode + // Setting the PointOfView.Value directly skips all the code that would usually change how the player body is rendered + playerBody.PointOfView.Value = EPointOfView.FreeCamera; + localPlayer.GetComponent().UpdatePointOfView(); + + // All we really needed to do, was trigger the UpdatePointOfView method and have it update to the FreeCam state + // There's no easy way of doing this without patching the method, and even then it might be a bloated solution + } + else + { + FreecamPlugin.Logger.LogError("Failed to get the PlayerBody field"); + PreloaderUI.Instance.Console.AddLog("ERROR: Failed to get the PlayerBody field", "FreeCam"); + } + + // Detour the Player.Move method into an empty one, preventing the character from moving during Freecam mode + if (_playerMoveMethod != null) + { + _moveDetour = new Detour(_playerMoveMethod, typeof(FreecamController).GetMethod(nameof(DisabledMove))); + } + + // Same deal here, but for player rotation + if (_playerRotateMethod != null) + { + _rotateDetour = new Detour(_playerRotateMethod, typeof(FreecamController).GetMethod(nameof(DisabledRotate))); + } + + if (FreecamPlugin.CameraRememberLastPosition.Value && _lastPosition != null && _lastRotation != null) + { + _mainCamera.transform.position = _lastPosition.Value; + _mainCamera.transform.rotation = _lastRotation.Value; + } + + _freeCamScript.IsActive = true; + } + + /// + /// A helper method to reset the player view back to First Person + /// + /// + private void SetPlayerToFirstPersonMode(Player localPlayer) + { + _freeCamScript.IsActive = false; + + if (FreecamPlugin.CameraRememberLastPosition.Value) + { + _lastPosition = _mainCamera.transform.position; + _lastRotation = _mainCamera.transform.rotation; + } + + ClearDetours(); + + localPlayer.PointOfView = EPointOfView.FirstPerson; } /// /// Gets the current instance if it's available /// /// Local instance; returns null if the game is not in raid - private static Player GetLocalPlayerFromWorld() + private Player GetLocalPlayerFromWorld() { // If the GameWorld instance is null or has no RegisteredPlayers, it most likely means we're not in a raid var gameWorld = Singleton.Instance; @@ -150,8 +250,51 @@ namespace Terkoiz.Freecam return null; } - // One of the RegisterdPlayers will have the IsYourPlayer flag set, which will be our own Player instance + // One of the RegisteredPlayers will have the IsYourPlayer flag set, which will be our own Player instance return gameWorld.RegisteredPlayers.Find(p => p.IsYourPlayer); } + + [UsedImplicitly] + public void OnDestroy() + { + _freeCamScript.IsActive = false; + + // Making sure no stray Detours are left + ClearDetours(); + + _lastPosition = null; + _lastRotation = null; + } + + public void ClearDetours() + { + if (_moveDetour != null) + { + _moveDetour.Dispose(); + _moveDetour = null; + } + + if (_rotateDetour != null) + { + _rotateDetour.Dispose(); + _rotateDetour = null; + } + } + + /// + /// An empty method that's used to detour the player Move method, preventing movement during Freecam mode + /// + [UsedImplicitly] + public static void DisabledMove(Player self, Vector2 direction) + { + } + + /// + /// An empty method that's used to detour the player Rotate method, preventing rotation during Freecam mode + /// + [UsedImplicitly] + public static void DisabledRotate(Player self, Vector2 deltaRotation, bool ignoreClamp = false) + { + } } } \ No newline at end of file diff --git a/project/Terkoiz.Freecam/FreecamPlugin.cs b/project/Terkoiz.Freecam/FreecamPlugin.cs index 389835d..1127c4b 100644 --- a/project/Terkoiz.Freecam/FreecamPlugin.cs +++ b/project/Terkoiz.Freecam/FreecamPlugin.cs @@ -1,12 +1,16 @@ using BepInEx; using BepInEx.Configuration; +using BepInEx.Logging; +using JetBrains.Annotations; using UnityEngine; namespace Terkoiz.Freecam { - [BepInPlugin("com.terkoiz.freecam", "Terkoiz.Freecam", "1.1.1")] + [BepInPlugin("com.terkoiz.freecam", "Terkoiz.Freecam", "1.2.0")] public class FreecamPlugin : BaseUnityPlugin { + internal new static ManualLogSource Logger { get; private set; } + private static GameObject HookObject; // Keyboard shortcut config entries @@ -23,13 +27,23 @@ namespace Terkoiz.Freecam internal static ConfigEntry CameraZoomSpeed; internal static ConfigEntry CameraFastZoomSpeed; - private void Awake() + private const string TogglesSectionName = "Toggles"; + internal static ConfigEntry CameraHeightMovement; + internal static ConfigEntry CameraMousewheelZoom; + internal static ConfigEntry CameraRememberLastPosition; + + // TODO: Hook into camera OnDestroy to run the OnDestroy from FreecamController + + [UsedImplicitly] + internal void Start() { + Logger = base.Logger; + InitConfiguration(); HookObject = new GameObject(); HookObject.AddComponent(); - Object.DontDestroyOnLoad(HookObject); + DontDestroyOnLoad(HookObject); } private void InitConfiguration() @@ -91,6 +105,25 @@ namespace Terkoiz.Freecam new ConfigDescription( "Amount to zoom the camera when using the mouse wheel while holding Shift", new AcceptableValueRange(0.01f, 1000f))); + + CameraHeightMovement = Config.Bind( + TogglesSectionName, + "CameraHeightMovementKeys", + true, + "Enables or disables the camera height movement keys, which default to Q, E, R, F." + + " \nUseful to disable if you want to let your character lean in Freecam mode"); + + CameraMousewheelZoom = Config.Bind( + TogglesSectionName, + "CameraMousewheelZoom", + true, + "Enables or disables camera movement on mousewheel scroll. Just in case you find it annoying."); + + CameraRememberLastPosition = Config.Bind( + TogglesSectionName, + "CameraRememberLastPosition", + false, + "If enabled, returning to Freecam mode will put the camera to it's last position which was saved when exiting Freecam mode."); } } } diff --git a/project/Terkoiz.Freecam/Terkoiz.Freecam.csproj b/project/Terkoiz.Freecam/Terkoiz.Freecam.csproj index b28ec79..04b8d39 100644 --- a/project/Terkoiz.Freecam/Terkoiz.Freecam.csproj +++ b/project/Terkoiz.Freecam/Terkoiz.Freecam.csproj @@ -13,6 +13,8 @@ 512 true + + true @@ -39,11 +41,14 @@ + + ..\packages\HarmonyX.2.7.0\lib\net45\0Harmony.dll + ..\Shared\Hollowed\Assembly-CSharp.dll - - ..\packages\BepInEx.BaseLib.5.4.17\lib\net35\BepInEx.dll + + ..\packages\BepInEx.BaseLib.5.4.19\lib\net35\BepInEx.dll ..\Shared\Managed\Comfort.dll @@ -51,6 +56,24 @@ ..\Shared\Managed\ItemComponent.Types.dll + + ..\packages\Mono.Cecil.0.11.4\lib\net40\Mono.Cecil.dll + + + ..\packages\Mono.Cecil.0.11.4\lib\net40\Mono.Cecil.Mdb.dll + + + ..\packages\Mono.Cecil.0.11.4\lib\net40\Mono.Cecil.Pdb.dll + + + ..\packages\Mono.Cecil.0.11.4\lib\net40\Mono.Cecil.Rocks.dll + + + ..\packages\MonoMod.RuntimeDetour.21.12.13.1\lib\net452\MonoMod.RuntimeDetour.dll + + + ..\packages\MonoMod.Utils.21.12.13.1\lib\net452\MonoMod.Utils.dll + ..\Shared\Managed\UnityEngine.dll @@ -76,4 +99,11 @@ + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + \ No newline at end of file diff --git a/project/Terkoiz.Freecam/packages.config b/project/Terkoiz.Freecam/packages.config index 14409c9..47859cc 100644 --- a/project/Terkoiz.Freecam/packages.config +++ b/project/Terkoiz.Freecam/packages.config @@ -1,4 +1,9 @@  - + + + + + + \ No newline at end of file