using Comfort.Common; using EFT; using EFT.CameraControl; using EFT.UI; using HarmonyLib; using JetBrains.Annotations; using UnityEngine; namespace Terkoiz.Freecam { public class FreecamController : MonoBehaviour { private GameObject _mainCamera; private Freecam _freeCamScript; private EftBattleUIScreen _playerUi; private bool _uiHidden; private GamePlayerOwner _gamePlayerOwner; private Vector3? _lastPosition; private Quaternion? _lastRotation; [UsedImplicitly] public void Start() { // Get Main Camera _mainCamera = GetLocalPlayerFromWorld().GetComponent().Camera.gameObject; if (_mainCamera == null) { FreecamPlugin.Logger.LogError("Failed to locate main camera"); return; } // Get Player UI _playerUi = Singleton.Instance.EftBattleUIScreen; if (_playerUi == null) { FreecamPlugin.Logger.LogError("Failed to locate player UI"); return; } // Add Freecam script to main camera in scene _freeCamScript = _mainCamera.AddComponent(); if (_freeCamScript == null) { FreecamPlugin.Logger.LogError("Failed to add Freecam script to Camera"); } // Get GamePlayerOwner component _gamePlayerOwner = GetLocalPlayerFromWorld().GetComponentInChildren(); if (_gamePlayerOwner == null) { FreecamPlugin.Logger.LogError("Failed to locate GamePlayerOwner"); } } [UsedImplicitly] public void Update() { if (FreecamPlugin.ToggleUi.Value.IsDown()) { ToggleUi(); } if (FreecamPlugin.ToggleFreecamMode.Value.IsDown()) { ToggleCamera(); } if (FreecamPlugin.ToggleFreecamControls.Value.IsDown()) { ToggleCameraControls(); } if (FreecamPlugin.TeleportToCamera.Value.IsDown()) { MovePlayerToCamera(); } } /// /// Toggles the Freecam mode /// private void ToggleCamera() { // Get our own Player instance. Null means we're not in a raid var localPlayer = GetLocalPlayerFromWorld(); if (localPlayer == null) return; if (!_freeCamScript.IsActive) { SetPlayerToFreecamMode(localPlayer); } else { SetPlayerToFirstPersonMode(localPlayer); } } /// /// When triggered during Freecam mode, teleports the player to where the camera was and exits Freecam mode /// private void MovePlayerToCamera() { var localPlayer = GetLocalPlayerFromWorld(); if (localPlayer == null) return; // Move the player to the camera's current position and switch to First Person mode if (_freeCamScript.IsActive) { // Tell the fall damage patch that we just teleported. Used for the "smart" fall damage prevention feature FallDamagePatch.HasTeleported = true; // 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, Quaternion.Euler(0, _mainCamera.transform.rotation.y, 0)); // localPlayer.gameObject.transform.SetPositionAndRotation(position, _mainCamera.transform.rotation); SetPlayerToFirstPersonMode(localPlayer); } } /// /// Hides the main UI (health, stamina, stance, hotbar, etc.) /// private void ToggleUi() { // Check if we're currently in a raid if (GetLocalPlayerFromWorld() == null) return; _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"); } // Instead of Detouring, just turn off _gamePlayerOwner which takes the input _gamePlayerOwner.enabled = false; 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; } // re-enable _gamePlayerOwner _gamePlayerOwner.enabled = true; localPlayer.PointOfView = EPointOfView.FirstPerson; } /// /// A helper method to toggle the Freecam Camera Controls /// private void ToggleCameraControls() { if (_freeCamScript.IsActive) { _freeCamScript.IsActive = false; _gamePlayerOwner.enabled = true; } else { _freeCamScript.IsActive = true; _gamePlayerOwner.enabled = false; } } /// /// Gets the current instance if it's available /// /// Local instance; returns null if the game is not in raid 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; if (gameWorld == null || gameWorld.MainPlayer == null) return null; // One of the RegisteredPlayers will have the IsYourPlayer flag set, which will be our own Player instance return gameWorld.MainPlayer; } [UsedImplicitly] public void OnDestroy() { // Destroy FreeCamScript before FreeCamController if exists Destroy(_freeCamScript); } } }